diff --git a/CLAUDE.md b/CLAUDE.md index 0a5e65b8b..2c9ecd4ac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,16 +25,33 @@ For coding conventions, see [`fuz-stack`](../fuz-stack/CLAUDE.md). ## Development Stage -Early development, v0.0.1. Breaking changes are expected and welcome. No authentication — development use only. All state is in-memory (no database yet). The Hono/Node.js backend is a reference implementation that may be replaced by a Rust daemon (`fuzd`). +Early development, v0.0.1. Breaking changes are expected and welcome. No authentication — development use only. All state is in-memory (no database yet). The Hono/Node.js backend is a reference implementation that may be replaced by a Rust daemon (`fuzd`). Deno is a shortcut for the CLI and production server — long-term both migrate to Rust fuz/fuzd. See [GitHub issues](https://github.com/fuzdev/zzz/issues) for planned work. +## CLI + +zzz has a Deno-compiled CLI binary for daemon management and browser launching. +See [src/lib/zzz/CLAUDE.md](src/lib/zzz/CLAUDE.md) for full CLI architecture. + +```bash +zzz # start daemon if needed, open browser +zzz ~/dev/ # open workspace at ~/dev/ +zzz daemon start # start daemon (foreground) +zzz daemon status # show daemon info +zzz init # initialize ~/.zzz/ +``` + +The global daemon runs on port 4460 with state at `~/.zzz/`. Built via +`gro_plugin_deno_compile` (see `gro.config.ts` and `deno.json`). + ## Docs - [docs/architecture.md](docs/architecture.md) — Action system, Cell system, content model, data flow - [docs/development.md](docs/development.md) — Development workflow, extension points, patterns - [docs/providers.md](docs/providers.md) — AI provider integration, adding new providers - [src/lib/server/CLAUDE.md](src/lib/server/CLAUDE.md) — Backend server architecture, providers, security +- [src/lib/zzz/CLAUDE.md](src/lib/zzz/CLAUDE.md) — CLI architecture, commands, runtime abstraction ## Repository Structure @@ -43,16 +60,24 @@ src/ ├── lib/ # Published as @fuzdev/zzz │ ├── server/ # Backend (Hono/Node.js reference impl) │ │ ├── backend.ts -│ │ ├── server.ts +│ │ ├── server.ts # Node.js entry (dev mode) +│ │ ├── server_deno.ts # Deno entry (production/CLI) │ │ ├── backend_action_handlers.ts │ │ ├── backend_provider_*.ts # Ollama, Claude, ChatGPT, Gemini │ │ ├── scoped_fs.ts │ │ ├── security.ts │ │ └── backend_action_types.gen.ts │ │ +│ ├── zzz/ # CLI (Deno compiled binary) +│ │ ├── main.ts # Entry point (deno compile target) +│ │ ├── cli.ts # Arg parsing wrapper +│ │ ├── cli_config.ts # ~/.zzz/config.json +│ │ ├── runtime/ # ZzzRuntime abstraction +│ │ ├── cli/ # CLI infrastructure +│ │ └── commands/ # init, daemon, open, status +│ │ │ ├── *.svelte.ts # Cell state classes (26 classes) │ ├── action_specs.ts # All 20 action spec definitions -│ ├── action_spec.ts # ActionSpec schema │ ├── action_event.ts # Action lifecycle state machine │ ├── action_peer.ts # Symmetric send/receive │ ├── cell.svelte.ts # Base Cell class @@ -233,6 +258,7 @@ Each action is a plain object with Zod schemas for input/output: ```typescript export const diskfile_update_action_spec = { method: 'diskfile_update', + description: 'Write content to a file on disk', kind: 'request_response', initiator: 'frontend', auth: 'public', @@ -285,7 +311,7 @@ The `.zzz/` directory stores app data. Configured via `PUBLIC_ZZZ_DIR`. | ------------ | -------------------------------------- | | `state/` | Persistent data (completions logs) | | `cache/` | Regenerable data, safe to delete | -| `run/` | Runtime ephemeral (server.json: PID, port) | +| `run/` | Runtime ephemeral (daemon.json: PID, port) | All filesystem access goes through `ScopedFs` — path validation, no symlinks, absolute paths only. @@ -332,6 +358,14 @@ From `src/lib/server/.env.development.example`: ## fuz_app -zzz is the primary source for the Cell and Action patterns that will become the `fuz_app` package — a shared foundation for Fuz ecosystem apps. +zzz is the reference implementation for Cell and Action patterns. ActionSpec +types have been extracted to `@fuzdev/fuz_app` — zzz imports them from +`@fuzdev/fuz_app/actions/action_spec.js` and `@fuzdev/fuz_app/actions/action_registry.js`. +Cell patterns and the full SAES runtime (ActionEvent, ActionPeer, transports) +remain in zzz until a second consumer needs them (DA-5). + +The CLI and daemon lifecycle use `@fuzdev/fuz_app/cli/*` helpers: `DaemonInfo` +schema, `write_daemon_info`, `read_daemon_info`, `is_daemon_running`, +`stop_daemon`. The server writes `~/.zzz/run/daemon.json` (not `server.json`). -Last updated: 2026-02-10 +Last updated: 2026-02-24 diff --git a/deno.json b/deno.json new file mode 100644 index 000000000..700bd3569 --- /dev/null +++ b/deno.json @@ -0,0 +1,31 @@ +{ + "nodeModulesDir": "manual", + "unstable": ["sloppy-imports"], + "exclude": ["**/*.test.ts", "**/*.svelte.ts", "**/*.gen.ts", "src/test/"], + "tasks": { + "dev:start": "NODE_ENV=development deno run --allow-all src/lib/zzz/main.ts daemon start", + "install": "gro build && mkdir -p ~/.zzz/bin && cp dist_cli/zzz ~/.zzz/bin/zzz", + "check": "deno check src/lib/zzz/**/*.ts" + }, + "imports": { + "@std/": "jsr:@std/", + "esm-env": "npm:esm-env@^1", + "hono": "npm:hono@^4", + "svelte": "npm:svelte@^5", + "zod": "npm:zod@^4", + "@electric-sql/pglite": "npm:@electric-sql/pglite@^0.3", + "@fuzdev/fuz_app/": "../fuz_app/src/lib/", + "@fuzdev/fuz_util/": "npm:/@fuzdev/fuz_util@^0.52.0/", + "@fuzdev/gro/": "npm:/@fuzdev/gro@^0.195.2/", + "ollama": "npm:ollama@^0.6", + "@anthropic-ai/sdk": "npm:@anthropic-ai/sdk@^0.71.2", + "openai": "npm:openai@^6.10.0", + "@google/generative-ai": "npm:@google/generative-ai@^0.24.1" + }, + "fmt": { + "useTabs": true, + "lineWidth": 100, + "indentWidth": 2, + "singleQuote": true + } +} diff --git a/docs/architecture.md b/docs/architecture.md index 9a3b059a6..86d348092 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -12,52 +12,52 @@ Every action is a plain object with Zod schemas. Defined in `src/lib/action_spec ```typescript export const completion_create_action_spec = { - method: 'completion_create', - kind: 'request_response', - initiator: 'frontend', - auth: 'public', - side_effects: true, - input: z.strictObject({ - completion_request: CompletionRequest, - _meta: z.looseObject({progressToken: Uuid.optional()}).optional(), - }), - output: z.strictObject({ - completion_response: CompletionResponse, - _meta: z.looseObject({progressToken: Uuid.optional()}).optional(), - }), - async: true, + method: 'completion_create', + kind: 'request_response', + initiator: 'frontend', + auth: 'public', + side_effects: true, + input: z.strictObject({ + completion_request: CompletionRequest, + _meta: z.looseObject({progressToken: Uuid.optional()}).optional(), + }), + output: z.strictObject({ + completion_response: CompletionResponse, + _meta: z.looseObject({progressToken: Uuid.optional()}).optional(), + }), + async: true, } satisfies ActionSpecUnion; ``` ### Action Kinds -| Kind | Phases | Transport | Use | -|------|--------|-----------|-----| -| `request_response` | `send_request` → `receive_request` → `send_response` → `receive_response` | HTTP or WebSocket | Standard RPC | -| `remote_notification` | `send` → `receive` | WebSocket only | Streaming progress (backend → frontend) | -| `local_call` | `execute` | None | Frontend-only UI actions | +| Kind | Phases | Transport | Use | +| --------------------- | ------------------------------------------------------------------------- | ----------------- | --------------------------------------- | +| `request_response` | `send_request` → `receive_request` → `send_response` → `receive_response` | HTTP or WebSocket | Standard RPC | +| `remote_notification` | `send` → `receive` | WebSocket only | Streaming progress (backend → frontend) | +| `local_call` | `execute` | None | Frontend-only UI actions | ### Action Spec Fields -| Field | Type | Values | -|-------|------|--------| -| `method` | `string` | Action name (e.g. `'completion_create'`) | -| `kind` | `ActionKind` | `'request_response'` \| `'remote_notification'` \| `'local_call'` | -| `initiator` | `ActionInitiator` | `'frontend'` \| `'backend'` \| `'both'` | -| `auth` | `ActionAuth \| null` | `'public'` \| `'authorize'` \| `null` | -| `side_effects` | `boolean \| null` | Whether action mutates state | -| `input` | `z.ZodType` | Zod schema for request params | -| `output` | `z.ZodType` | Zod schema for response | -| `async` | `boolean` | Whether handler is async | +| Field | Type | Values | +| -------------- | -------------------- | ----------------------------------------------------------------- | +| `method` | `string` | Action name (e.g. `'completion_create'`) | +| `kind` | `ActionKind` | `'request_response'` \| `'remote_notification'` \| `'local_call'` | +| `initiator` | `ActionInitiator` | `'frontend'` \| `'backend'` \| `'both'` | +| `auth` | `ActionAuth \| null` | `'public'` \| `'authenticate'` \| `null` | +| `side_effects` | `boolean \| null` | Whether action mutates state | +| `input` | `z.ZodType` | Zod schema for request params | +| `output` | `z.ZodType` | Zod schema for response | +| `async` | `boolean` | Whether handler is async | ### Core Components -| Component | File | Purpose | -|-----------|------|---------| -| `ActionSpec` | `action_spec.ts` | Action metadata schema | -| `ActionEvent` | `action_event.ts` | Lifecycle state machine (initial → parsed → handling → handled/failed) | -| `ActionPeer` | `action_peer.ts` | Send/receive on both sides | -| `ActionRegistry` | `action_registry.ts` | Type-safe action lookup | +| Component | File | Purpose | +| ---------------- | -------------------- | ---------------------------------------------------------------------- | +| `ActionSpec` | `action_spec.ts` | Action metadata schema | +| `ActionEvent` | `action_event.ts` | Lifecycle state machine (initial → parsed → handling → handled/failed) | +| `ActionPeer` | `action_peer.ts` | Send/receive on both sides | +| `ActionRegistry` | `action_registry.ts` | Type-safe action lookup | ### Action Event Lifecycle @@ -77,37 +77,43 @@ Frontend and backend register handlers per action per phase: ```typescript // Frontend (frontend_action_handlers.ts) export const frontend_action_handlers: FrontendActionHandlers = { - completion_create: { - send_request: ({data: {input}}) => { - console.log('sending prompt:', input.completion_request.prompt); - }, - receive_response: ({app, data: {input, output}}) => { - const progress_token = input._meta?.progressToken; - if (progress_token) { - const turn = app.cell_registry.all.get(progress_token); - if (turn instanceof Turn) { - turn.content = to_completion_response_text(output.completion_response) || ''; - turn.response = output.completion_response; - } - } - }, - receive_error: ({data: {error}}) => { - console.error('completion failed:', error); - }, - }, + completion_create: { + send_request: ({data: {input}}) => { + console.log('sending prompt:', input.completion_request.prompt); + }, + receive_response: ({app, data: {input, output}}) => { + const progress_token = input._meta?.progressToken; + if (progress_token) { + const turn = app.cell_registry.all.get(progress_token); + if (turn instanceof Turn) { + turn.content = to_completion_response_text(output.completion_response) || ''; + turn.response = output.completion_response; + } + } + }, + receive_error: ({data: {error}}) => { + console.error('completion failed:', error); + }, + }, }; // Backend (server/backend_action_handlers.ts) export const backend_action_handlers: BackendActionHandlers = { - completion_create: { - receive_request: async ({backend, data: {input}}) => { - const {prompt, provider_name, model, completion_messages} = input.completion_request; - const progress_token = input._meta?.progressToken; - const provider = backend.lookup_provider(provider_name); - const handler = provider.get_handler(!!progress_token); - return await handler({model, prompt, completion_messages, completion_options, progress_token}); - }, - }, + completion_create: { + receive_request: async ({backend, data: {input}}) => { + const {prompt, provider_name, model, completion_messages} = input.completion_request; + const progress_token = input._meta?.progressToken; + const provider = backend.lookup_provider(provider_name); + const handler = provider.get_handler(!!progress_token); + return await handler({ + model, + prompt, + completion_messages, + completion_options, + progress_token, + }); + }, + }, }; ``` @@ -117,10 +123,10 @@ Actions are transport-agnostic via the `Transport` interface (`transports.ts`): ```typescript interface Transport { - transport_name: TransportName; - send(message: JsonrpcRequest): Promise; - send(message: JsonrpcNotification): Promise; - is_ready: () => boolean; + transport_name: TransportName; + send(message: JsonrpcRequest): Promise; + send(message: JsonrpcNotification): Promise; + is_ready: () => boolean; } ``` @@ -139,28 +145,28 @@ MCP-compatible subset, no batching: ### All 20 Actions -| Method | Kind | Initiator | Purpose | -|--------|------|-----------|---------| -| `ping` | `request_response` | `both` | Health check | -| `session_load` | `request_response` | `frontend` | Load initial session data | -| `filer_change` | `remote_notification` | `backend` | File system change notification | -| `diskfile_update` | `request_response` | `frontend` | Write file content | -| `diskfile_delete` | `request_response` | `frontend` | Delete a file | -| `directory_create` | `request_response` | `frontend` | Create a directory | -| `completion_create` | `request_response` | `frontend` | Start AI completion | -| `completion_progress` | `remote_notification` | `backend` | Stream completion chunks | -| `ollama_progress` | `remote_notification` | `backend` | Model operation progress | -| `toggle_main_menu` | `local_call` | `frontend` | Toggle main menu UI | -| `ollama_list` | `request_response` | `frontend` | List local models | -| `ollama_ps` | `request_response` | `frontend` | List running models | -| `ollama_show` | `request_response` | `frontend` | Show model details | -| `ollama_pull` | `request_response` | `frontend` | Pull model | -| `ollama_delete` | `request_response` | `frontend` | Delete model | -| `ollama_copy` | `request_response` | `frontend` | Copy model | -| `ollama_create` | `request_response` | `frontend` | Create model | -| `ollama_unload` | `request_response` | `frontend` | Unload model from memory | -| `provider_load_status` | `request_response` | `frontend` | Check provider availability | -| `provider_update_api_key` | `request_response` | `frontend` | Update provider API key | +| Method | Kind | Initiator | Purpose | +| ------------------------- | --------------------- | ---------- | ------------------------------- | +| `ping` | `request_response` | `both` | Health check | +| `session_load` | `request_response` | `frontend` | Load initial session data | +| `filer_change` | `remote_notification` | `backend` | File system change notification | +| `diskfile_update` | `request_response` | `frontend` | Write file content | +| `diskfile_delete` | `request_response` | `frontend` | Delete a file | +| `directory_create` | `request_response` | `frontend` | Create a directory | +| `completion_create` | `request_response` | `frontend` | Start AI completion | +| `completion_progress` | `remote_notification` | `backend` | Stream completion chunks | +| `ollama_progress` | `remote_notification` | `backend` | Model operation progress | +| `toggle_main_menu` | `local_call` | `frontend` | Toggle main menu UI | +| `ollama_list` | `request_response` | `frontend` | List local models | +| `ollama_ps` | `request_response` | `frontend` | List running models | +| `ollama_show` | `request_response` | `frontend` | Show model details | +| `ollama_pull` | `request_response` | `frontend` | Pull model | +| `ollama_delete` | `request_response` | `frontend` | Delete model | +| `ollama_copy` | `request_response` | `frontend` | Copy model | +| `ollama_create` | `request_response` | `frontend` | Create model | +| `ollama_unload` | `request_response` | `frontend` | Unload model from memory | +| `provider_load_status` | `request_response` | `frontend` | Check provider availability | +| `provider_update_api_key` | `request_response` | `frontend` | Update provider API key | ## Cell System @@ -201,8 +207,8 @@ export abstract class Cell implements Cel ```typescript interface CellOptions { - app: Frontend; // Root app state reference - json?: z.input; // Initial JSON data (parsed by schema) + app: Frontend; // Root app state reference + json?: z.input; // Initial JSON data (parsed by schema) } ``` @@ -213,36 +219,36 @@ Real example from `chat.svelte.ts`: ```typescript // 1. Schema with CellJson base — every field has .default() export const ChatJson = CellJson.extend({ - name: z.string().default(''), - thread_ids: z.array(Uuid).default(() => []), - main_input: z.string().default(''), - view_mode: z.enum(['simple', 'multi']).default('simple'), - selected_thread_id: Uuid.nullable().default(null), + name: z.string().default(''), + thread_ids: z.array(Uuid).default(() => []), + main_input: z.string().default(''), + view_mode: z.enum(['simple', 'multi']).default('simple'), + selected_thread_id: Uuid.nullable().default(null), }).meta({cell_class_name: 'Chat'}); // 2. Class: $state for schema fields, $derived for computed export class Chat extends Cell { - name: string = $state()!; - thread_ids: Array = $state()!; - main_input: string = $state()!; - view_mode: ChatViewMode = $state()!; - selected_thread_id: Uuid | null = $state()!; - - readonly threads: Array = $derived.by(() => { - const result: Array = []; - for (const id of this.thread_ids) { - const thread = this.app.threads.items.by_id.get(id); - if (thread) result.push(thread); - } - return result; - }); - - readonly enabled_threads = $derived(this.threads.filter((t) => t.enabled)); - - constructor(options: ChatOptions) { - super(ChatJson, options); - this.init(); // Must call at end - } + name: string = $state()!; + thread_ids: Array = $state()!; + main_input: string = $state()!; + view_mode: ChatViewMode = $state()!; + selected_thread_id: Uuid | null = $state()!; + + readonly threads: Array = $derived.by(() => { + const result: Array = []; + for (const id of this.thread_ids) { + const thread = this.app.threads.items.by_id.get(id); + if (thread) result.push(thread); + } + return result; + }); + + readonly enabled_threads = $derived(this.threads.filter((t) => t.enabled)); + + constructor(options: ChatOptions) { + super(ChatJson, options); + this.init(); // Must call at end + } } ``` @@ -277,12 +283,17 @@ All cell classes are registered in `cell_classes.ts`. Frontend iterates and regi ```typescript // cell_classes.ts — add new classes here export const cell_classes = { - Parts, Chat, Chats, Thread, Threads, Turn, /* ... 26 total */ + Parts, + Chat, + Chats, + Thread, + Threads, + Turn /* ... 26 total */, } satisfies Record>; // frontend.svelte.ts — auto-registers all classes for (const constructor of Object.values(cell_classes)) { - this.cell_registry.register(constructor); + this.cell_registry.register(constructor); } // Lookup by ID at runtime @@ -303,9 +314,9 @@ Prompt → parts: Array (reusable content templates) ### Parts -| Type | Class | Content source | -|------|-------|----------------| -| Text | `TextPart` | `content: string` stored directly | +| Type | Class | Content source | +| -------- | -------------- | ------------------------------------------------------ | +| Text | `TextPart` | `content: string` stored directly | | Diskfile | `DiskfilePart` | `path: DiskfilePath` → reads from disk or editor state | ### Turns @@ -314,17 +325,20 @@ Conversation messages with role: ```typescript class Turn extends Cell { - part_ids: Array = $state()!; - role: CompletionRole = $state()!; // 'user' | 'assistant' | 'system' - request: CompletionRequest | undefined = $state.raw(); - response: CompletionResponse | undefined = $state.raw(); - - readonly content: string = $derived( - this.parts.map(p => p.content).filter(Boolean).join('\n\n') - ); - readonly pending: boolean = $derived( - this.role === 'assistant' && this.is_content_empty && !this.response - ); + part_ids: Array = $state()!; + role: CompletionRole = $state()!; // 'user' | 'assistant' | 'system' + request: CompletionRequest | undefined = $state.raw(); + response: CompletionResponse | undefined = $state.raw(); + + readonly content: string = $derived( + this.parts + .map((p) => p.content) + .filter(Boolean) + .join('\n\n'), + ); + readonly pending: boolean = $derived( + this.role === 'assistant' && this.is_content_empty && !this.response, + ); } ``` @@ -404,31 +418,31 @@ Queryable reactive collections with multiple index types. From `indexed_collecti ```typescript class IndexedCollection { - readonly by_id: SvelteMap = new SvelteMap(); - readonly values: Array = $derived(Array.from(this.by_id.values())); - readonly size: number = $derived(this.by_id.size); + readonly by_id: SvelteMap = new SvelteMap(); + readonly values: Array = $derived(Array.from(this.by_id.values())); + readonly size: number = $derived(this.by_id.size); } ``` ### Index Types -| Type | Cardinality | Example | -|------|-------------|---------| -| `single` | One key → one item | `by('name', 'gpt-5')` | -| `multi` | One key → many items | `where('provider_name', 'ollama')` | +| Type | Cardinality | Example | +| --------- | --------------------- | ---------------------------------- | +| `single` | One key → one item | `by('name', 'gpt-5')` | +| `multi` | One key → many items | `where('provider_name', 'ollama')` | | `derived` | Computed sorted array | `derived_index('ordered_by_name')` | -| `dynamic` | Runtime-computed | Custom queries | +| `dynamic` | Runtime-computed | Custom queries | ### Index Definition ```typescript interface IndexDefinition { - key: string; - type?: 'single' | 'multi' | 'derived' | 'dynamic'; - extractor?: (item: T) => any; - compute: (collection: IndexedCollection) => TResult; - onadd?: (result: TResult, item: T, collection: IndexedCollection) => TResult; - onremove?: (result: TResult, item: T, collection: IndexedCollection) => TResult; + key: string; + type?: 'single' | 'multi' | 'derived' | 'dynamic'; + extractor?: (item: T) => any; + compute: (collection: IndexedCollection) => TResult; + onadd?: (result: TResult, item: T, collection: IndexedCollection) => TResult; + onremove?: (result: TResult, item: T, collection: IndexedCollection) => TResult; } ``` @@ -437,27 +451,27 @@ interface IndexDefinition { ```typescript // Create with indexes const items = new IndexedCollection({ - indexes: [ - create_single_index({key: 'name', extractor: m => m.name}), - create_multi_index({key: 'provider_name', extractor: m => m.provider_name}), - create_derived_index({key: 'ordered_by_name', sort: (a, b) => a.name.localeCompare(b.name)}), - ], + indexes: [ + create_single_index({key: 'name', extractor: (m) => m.name}), + create_multi_index({key: 'provider_name', extractor: (m) => m.provider_name}), + create_derived_index({key: 'ordered_by_name', sort: (a, b) => a.name.localeCompare(b.name)}), + ], }); // Query -items.by('name', 'gpt-5'); // single → Model | undefined -items.where('provider_name', 'ollama'); // multi → Array -items.derived_index('ordered_by_name'); // derived → Array +items.by('name', 'gpt-5'); // single → Model | undefined +items.where('provider_name', 'ollama'); // multi → Array +items.derived_index('ordered_by_name'); // derived → Array ``` ## Filesystem Two separate concerns: -| Concern | Env Var | Purpose | -|---------|---------|---------| -| App directory | `PUBLIC_ZZZ_DIR` | Zzz's own data (`.zzz/state/`, `.zzz/cache/`, `.zzz/run/`) | -| Scoped dirs | `PUBLIC_ZZZ_SCOPED_DIRS` | User file access (comma-separated paths) | +| Concern | Env Var | Purpose | +| ------------- | ------------------------ | ---------------------------------------------------------- | +| App directory | `PUBLIC_ZZZ_DIR` | Zzz's own data (`.zzz/state/`, `.zzz/cache/`, `.zzz/run/`) | +| Scoped dirs | `PUBLIC_ZZZ_SCOPED_DIRS` | User file access (comma-separated paths) | ### ScopedFs @@ -467,6 +481,6 @@ All filesystem operations go through `ScopedFs` (`server/scoped_fs.ts`). Securit Each scoped directory gets a `Filer` watcher. File changes are broadcast to clients via `filer_change` notifications over WebSocket. -### Server Info +### Daemon Info -`run/server.json` tracks the running server (PID, port, version). Written atomically on startup, removed on clean shutdown (SIGINT/SIGTERM). Stale detection via `process.kill(pid, 0)`. +`run/daemon.json` tracks the running server (PID, port, version). Written atomically on startup via `@fuzdev/fuz_app/cli/daemon.js`, removed on clean shutdown (SIGINT/SIGTERM). Stale detection via `kill -0`. diff --git a/gro.config.ts b/gro.config.ts new file mode 100644 index 000000000..bdc0ca30a --- /dev/null +++ b/gro.config.ts @@ -0,0 +1,27 @@ +import type {CreateGroConfig} from '@fuzdev/gro'; +import {gro_plugin_deno_compile} from '@fuzdev/gro/gro_plugin_deno_compile.js'; + +// eslint-disable-next-line @typescript-eslint/require-await +const config: CreateGroConfig = async (base_config) => { + const base_plugins = base_config.plugins; + base_config.plugins = async (ctx) => { + const plugins = await base_plugins(ctx); + return [ + ...plugins, + gro_plugin_deno_compile({ + entry: 'src/lib/zzz/main.ts', + output_name: 'zzz', + flags: [ + '--no-check', + '--sloppy-imports', + '--include', + '../../blake3/crates/blake3_wasm/pkg/deno', // embeds WASM binary for blake3 + ], + }), + ]; + }; + + return base_config; +}; + +export default config; diff --git a/package-lock.json b/package-lock.json index e2b6b2b0c..fcc1c2dff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,28 +10,30 @@ "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.71.2", - "@fuzdev/gro": "^0.195.2", + "@fuzdev/gro": "^0.197.0", "@google/generative-ai": "^0.24.1", "@hono/node-server": "^1.19.6", "@hono/node-ws": "^1.2.0", "date-fns": "^4.1.0", "esm-env": "^1.2.2", - "hono": "^4.10.7", + "hono": "^4.12.5", "openai": "^6.10.0", "zod": "^4.3.6" }, "devDependencies": { "@changesets/changelog-git": "^0.2.1", + "@fuzdev/fuz_app": "file:../fuz_app", "@fuzdev/fuz_code": "^0.45.1", - "@fuzdev/fuz_css": "^0.53.1", + "@fuzdev/fuz_css": "^0.55.0", "@fuzdev/fuz_ui": "^0.185.2", - "@fuzdev/fuz_util": "^0.52.0", + "@fuzdev/fuz_util": "^0.53.4", "@jridgewell/trace-mapping": "^0.3.31", - "@ryanatkn/eslint-config": "^0.9.0", + "@ryanatkn/eslint-config": "^0.10.1", "@sveltejs/adapter-node": "^5.4.0", "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.50.1", + "@sveltejs/kit": "^2.53.4", "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@types/deno": "^2.5.0", "@types/estree": "^1.0.8", "@types/node": "^24.10.1", "@webref/css": "^8.2.0", @@ -42,7 +44,7 @@ "ollama": "^0.6.3", "prettier": "^3.7.4", "prettier-plugin-svelte": "^3.4.1", - "svelte": "^5.49.1", + "svelte": "^5.53.7", "svelte-check": "^4.3.5", "svelte2tsx": "^0.7.47", "tslib": "^2.8.1", @@ -62,6 +64,77 @@ "svelte": "^5" } }, + "../fuz_app": { + "name": "@fuzdev/fuz_app", + "version": "0.0.1", + "dev": true, + "license": "MIT", + "devDependencies": { + "@electric-sql/pglite": "^0.3.15", + "@fuzdev/blake3_wasm": "^0.1.0", + "@fuzdev/fuz_code": "^0.45.1", + "@fuzdev/fuz_css": "^0.55.0", + "@fuzdev/fuz_ui": "^0.186.0", + "@fuzdev/fuz_util": "^0.53.4", + "@fuzdev/gro": "^0.197.0", + "@jridgewell/trace-mapping": "^0.3.31", + "@node-rs/argon2": "^2.0.2", + "@ryanatkn/eslint-config": "^0.10.1", + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.53.4", + "@sveltejs/package": "^2.5.7", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@types/estree": "^1.0.8", + "@types/pg": "^8.16.0", + "@webref/css": "^8.2.0", + "eslint": "^9.39.1", + "eslint-plugin-svelte": "^3.13.1", + "esm-env": "^1.2.2", + "hono": "^4.12.5", + "jsdom": "^28.1.0", + "magic-string": "^0.30.21", + "pg": "^8.18.0", + "prettier": "^3.7.4", + "prettier-plugin-svelte": "^3.4.1", + "svelte": "^5.53.7", + "svelte-check": "^4.3.6", + "svelte2tsx": "^0.7.47", + "tslib": "^2.8.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.48.1", + "vitest": "^4.0.15", + "zimmerframe": "^1.1.4", + "zod": "^4.3.6" + }, + "engines": { + "node": ">=22.15" + }, + "peerDependencies": { + "@electric-sql/pglite": ">=0.2", + "@fuzdev/blake3_wasm": "0.1.0", + "@fuzdev/fuz_util": ">=0.50.1", + "@node-rs/argon2": ">=2", + "@sveltejs/kit": "^2", + "hono": ">=4", + "pg": ">=8", + "svelte": "^5", + "zod": ">=4" + }, + "peerDependenciesMeta": { + "@electric-sql/pglite": { + "optional": true + }, + "@fuzdev/blake3_wasm": { + "optional": true + }, + "@node-rs/argon2": { + "optional": true + }, + "pg": { + "optional": true + } + } + }, "node_modules/@acemir/cssom": { "version": "0.9.24", "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.24.tgz", @@ -915,6 +988,23 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fuzdev/blake3_wasm": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@fuzdev/blake3_wasm/-/blake3_wasm-0.1.0.tgz", + "integrity": "sha512-EU5uUcSX55Li3IXi1NiBDoVlxCN8ip9wqAhVZlMBEUa+cFQtLL6Z8GpYjlWy0KosLmxy2Z9WQv49PAkiAzFppg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://www.ryanatkn.com/funding" + } + }, + "node_modules/@fuzdev/fuz_app": { + "resolved": "../fuz_app", + "link": true + }, "node_modules/@fuzdev/fuz_code": { "version": "0.45.1", "resolved": "https://registry.npmjs.org/@fuzdev/fuz_code/-/fuz_code-0.45.1.tgz", @@ -954,9 +1044,9 @@ } }, "node_modules/@fuzdev/fuz_css": { - "version": "0.53.1", - "resolved": "https://registry.npmjs.org/@fuzdev/fuz_css/-/fuz_css-0.53.1.tgz", - "integrity": "sha512-O5HuwLo7Oa8Gp3Iu4djKSaqzA+ALz8G309rGbhf7K95osMSFbW3mNmk/yMUj54DF+YRNdpRZGmqcRPNq8+em8w==", + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@fuzdev/fuz_css/-/fuz_css-0.55.0.tgz", + "integrity": "sha512-0aalwQoE5MjSiRp59eRKcK/T121n+8x6lL4B63sIq/raUhZ7At8dWYy7NFBFgrVHFHPLk9m/2vt8hFO2mrqwGA==", "dev": true, "license": "MIT", "engines": { @@ -966,6 +1056,7 @@ "url": "https://www.ryanatkn.com/funding" }, "peerDependencies": { + "@fuzdev/blake3_wasm": "^0.1.0", "@fuzdev/fuz_util": ">=0.52.0", "@fuzdev/gro": ">=0.195.0", "@sveltejs/acorn-typescript": "^1", @@ -975,6 +1066,9 @@ "zod": "^4" }, "peerDependenciesMeta": { + "@fuzdev/blake3_wasm": { + "optional": true + }, "@fuzdev/fuz_util": { "optional": true }, @@ -1051,9 +1145,9 @@ } }, "node_modules/@fuzdev/fuz_util": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@fuzdev/fuz_util/-/fuz_util-0.52.0.tgz", - "integrity": "sha512-zQHfgn2AdNDeauwjVqfdA8tCZoHWaZGssgMYa/PinnQofI0mnHu8haloFaMuxyDLgBePyiACEirEtlBse2zSDw==", + "version": "0.53.4", + "resolved": "https://registry.npmjs.org/@fuzdev/fuz_util/-/fuz_util-0.53.4.tgz", + "integrity": "sha512-pkb0vsEviCnVv3Oc9ZPYUkkRxtg6CInFvm4S3AcXTdJv6T8efzwkQh7pqPAEVxVT3ZM1Tse2Ej7MEbxK477j2Q==", "license": "MIT", "engines": { "node": ">=22.15" @@ -1062,6 +1156,7 @@ "url": "https://www.ryanatkn.com/funding" }, "peerDependencies": { + "@fuzdev/blake3_wasm": "^0.1.0", "@types/estree": "^1", "@types/node": "^24", "esm-env": "^1.2.2", @@ -1069,6 +1164,9 @@ "zod": "^4.0.14" }, "peerDependenciesMeta": { + "@fuzdev/blake3_wasm": { + "optional": true + }, "@types/estree": { "optional": true }, @@ -1087,9 +1185,9 @@ } }, "node_modules/@fuzdev/gro": { - "version": "0.195.2", - "resolved": "https://registry.npmjs.org/@fuzdev/gro/-/gro-0.195.2.tgz", - "integrity": "sha512-hxxu4M2xLzJbr8bfwVUq/7io9Yzb1woTvnm5w7YUO6yHB6wcoqcVNfq23lnSlZY/8zEC899dynfjyEHfcbZUwA==", + "version": "0.197.0", + "resolved": "https://registry.npmjs.org/@fuzdev/gro/-/gro-0.197.0.tgz", + "integrity": "sha512-8su6n1a7UCOJUWIwRPIr88hmLlP1lKwEdQJieP0T8lVLKdrP/wMA7TrkMUPOxxKzfKcONx6eWO6hrCNrPrVPKA==", "license": "MIT", "dependencies": { "chokidar": "^5.0.0", @@ -1115,7 +1213,8 @@ "vitest": "^3 || ^4" }, "peerDependencies": { - "@fuzdev/fuz_util": ">=0.52.0", + "@fuzdev/blake3_wasm": "^0.1.0", + "@fuzdev/fuz_util": ">=0.53.0", "@sveltejs/kit": "^2", "esbuild": "^0.27.0", "svelte": "^5", @@ -1959,9 +2058,9 @@ ] }, "node_modules/@ryanatkn/eslint-config": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@ryanatkn/eslint-config/-/eslint-config-0.9.0.tgz", - "integrity": "sha512-RF42tZfJo2CYE4E3clQRBm9bVHMpL5ErR3HfWaxbiuL1aGraehegsiXMsr1L4BiKpSP55ZO8vvCr1ibUaSRIrQ==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@ryanatkn/eslint-config/-/eslint-config-0.10.1.tgz", + "integrity": "sha512-fHQ5PyFriflVj/fiF9m4SoUnipyK/Of522HL3+YA5TD2lKdJueA5c4wxucxkuFanuZ1FvsCBjGN/wMHO94HNHA==", "dev": true, "license": "Unlicense", "dependencies": { @@ -2022,9 +2121,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.50.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.50.1.tgz", - "integrity": "sha512-XRHD2i3zC4ukhz2iCQzO4mbsts081PAZnnMAQ7LNpWeYgeBmwMsalf0FGSwhFXBbtr2XViPKnFJBDCckWqrsLw==", + "version": "2.53.4", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.53.4.tgz", + "integrity": "sha512-iAIPEahFgDJJyvz8g0jP08KvqnM6JvdW8YfsygZ+pMeMvyM2zssWMltcsotETvjSZ82G3VlitgDtBIvpQSZrTA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -2033,13 +2132,12 @@ "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", - "devalue": "^5.6.2", + "devalue": "^5.6.3", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", - "sade": "^1.8.1", - "set-cookie-parser": "^2.6.0", + "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "bin": { @@ -2050,10 +2148,10 @@ }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", - "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "peerDependenciesMeta": { "@opentelemetry/api": { @@ -2138,6 +2236,13 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@types/deno": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@types/deno/-/deno-2.5.0.tgz", + "integrity": "sha512-g8JS38vmc0S87jKsFzre+0ZyMOUDHPVokEJymSCRlL57h6f/FdKPWBXgdFh3Z8Ees9sz11qt9VWELU9Y9ZkiVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2168,6 +2273,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.48.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz", @@ -2619,9 +2730,9 @@ "license": "Python-2.0" }, "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -2911,9 +3022,9 @@ } }, "node_modules/devalue": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz", - "integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz", + "integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==", "license": "MIT" }, "node_modules/dotenv": { @@ -3397,9 +3508,9 @@ } }, "node_modules/hono": { - "version": "4.10.7", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.10.7.tgz", - "integrity": "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw==", + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", + "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -3775,7 +3886,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -4310,7 +4421,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "mri": "^1.1.0" @@ -4360,9 +4471,9 @@ } }, "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.0.1.tgz", + "integrity": "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==", "devOptional": true, "license": "MIT" }, @@ -4475,20 +4586,21 @@ } }, "node_modules/svelte": { - "version": "5.49.1", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.49.1.tgz", - "integrity": "sha512-jj95WnbKbXsXXngYj28a4zx8jeZx50CN/J4r0CEeax2pbfdsETv/J1K8V9Hbu3DCXnpHz5qAikICuxEooi7eNQ==", + "version": "5.53.7", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.7.tgz", + "integrity": "sha512-uxck1KI7JWtlfP3H6HOWi/94soAl23jsGJkBzN2BAWcQng0+lTrRNhxActFqORgnO9BHVd1hKJhG+ljRuIUWfQ==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", - "aria-query": "^5.3.1", + "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", - "devalue": "^5.6.2", + "devalue": "^5.6.3", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", diff --git a/package.json b/package.json index a4122e377..be9e78df6 100644 --- a/package.json +++ b/package.json @@ -37,16 +37,18 @@ }, "devDependencies": { "@changesets/changelog-git": "^0.2.1", + "@fuzdev/fuz_app": "file:../fuz_app", "@fuzdev/fuz_code": "^0.45.1", - "@fuzdev/fuz_css": "^0.53.1", + "@fuzdev/fuz_css": "^0.55.0", "@fuzdev/fuz_ui": "^0.185.2", - "@fuzdev/fuz_util": "^0.52.0", + "@fuzdev/fuz_util": "^0.53.4", "@jridgewell/trace-mapping": "^0.3.31", - "@ryanatkn/eslint-config": "^0.9.0", + "@ryanatkn/eslint-config": "^0.10.1", "@sveltejs/adapter-node": "^5.4.0", "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.50.1", + "@sveltejs/kit": "^2.53.4", "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@types/deno": "^2.5.0", "@types/estree": "^1.0.8", "@types/node": "^24.10.1", "@webref/css": "^8.2.0", @@ -57,7 +59,7 @@ "ollama": "^0.6.3", "prettier": "^3.7.4", "prettier-plugin-svelte": "^3.4.1", - "svelte": "^5.49.1", + "svelte": "^5.53.7", "svelte-check": "^4.3.5", "svelte2tsx": "^0.7.47", "tslib": "^2.8.1", @@ -68,13 +70,13 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.71.2", - "@fuzdev/gro": "^0.195.2", + "@fuzdev/gro": "^0.197.0", "@google/generative-ai": "^0.24.1", "@hono/node-server": "^1.19.6", "@hono/node-ws": "^1.2.0", "date-fns": "^4.1.0", "esm-env": "^1.2.2", - "hono": "^4.10.7", + "hono": "^4.12.5", "openai": "^6.10.0", "zod": "^4.3.6" }, diff --git a/src/lib/ActionListitem.svelte b/src/lib/ActionListitem.svelte index 0faeb5564..57edad652 100644 --- a/src/lib/ActionListitem.svelte +++ b/src/lib/ActionListitem.svelte @@ -22,7 +22,7 @@ - {/if} - {/snippet} - - -{#snippet children_default(popover: Popover)} - {#if children} - {@render children(popover, () => confirm(popover))} - {:else} - - {/if} -{/snippet} - - diff --git a/src/lib/Dashboard.svelte b/src/lib/Dashboard.svelte index f0021eddb..69bffd360 100644 --- a/src/lib/Dashboard.svelte +++ b/src/lib/Dashboard.svelte @@ -10,9 +10,16 @@ import {logo_zzz} from './logos.js'; import NavLink from './NavLink.svelte'; import Glyph from './Glyph.svelte'; - import {GLYPH_ARROW_LEFT, GLYPH_ARROW_RIGHT, GLYPH_PROJECT, GLYPH_TAB} from './glyphs.js'; + import { + GLYPH_ARROW_LEFT, + GLYPH_ARROW_RIGHT, + GLYPH_DESK, + GLYPH_PROJECT, + GLYPH_TAB, + } from './glyphs.js'; import {frontend_context} from './frontend.svelte.js'; import {main_nav_items_default, to_nav_link_href} from './nav.js'; + import {DESK_WIDTH} from './DeskMenu.svelte'; // TODO dashboard should be mounted with Markdown @@ -26,6 +33,7 @@ const SIDEBAR_WIDTH_MAX = 180; const sidebar_width = $derived(app.ui.show_sidebar ? SIDEBAR_WIDTH_MAX : 0); + const desk_width = $derived(app.ui.show_desk_menu && app.ui.desk_pinned ? DESK_WIDTH : 0); let futureclicks = $state(0); const FUTURECLICKS = 3; @@ -75,7 +83,18 @@ { - if (e.key === '`' && !is_editable(e.target)) { + if ( + e.key === 'Escape' && + app.ui.show_desk_menu && + !app.ui.desk_pinned && + !is_editable(e.target) + ) { + app.ui.toggle_desk_menu(false); + swallow(e); + } else if (e.key === '~' && !is_editable(e.target)) { + app.ui.toggle_desk_menu(); + swallow(e); + } else if (e.key === '`' && !is_editable(e.target)) { app.ui.toggle_sidebar(); swallow(e); } @@ -83,10 +102,15 @@ /> -
+
{@render children()}
@@ -163,11 +187,24 @@ + + + {#if !app.ui.show_desk_menu} + + {/if}
diff --git a/src/lib/DashboardPrompts.svelte b/src/lib/DashboardPrompts.svelte index 38bc26ada..37d4a0fa1 100644 --- a/src/lib/DashboardPrompts.svelte +++ b/src/lib/DashboardPrompts.svelte @@ -2,8 +2,8 @@ import {fade} from 'svelte/transition'; import CopyToClipboard from '@fuzdev/fuz_ui/CopyToClipboard.svelte'; import {random_item} from '@fuzdev/fuz_util/random.js'; + import ConfirmButton from '@fuzdev/fuz_app/ui/ConfirmButton.svelte'; - import ConfirmButton from './ConfirmButton.svelte'; import Glyph from './Glyph.svelte'; import PartView from './PartView.svelte'; import { diff --git a/src/lib/DeskMenu.svelte b/src/lib/DeskMenu.svelte new file mode 100644 index 000000000..c7578a3d6 --- /dev/null +++ b/src/lib/DeskMenu.svelte @@ -0,0 +1,132 @@ + + + + +{#if app.ui.show_desk_menu} + +{/if} + + diff --git a/src/lib/DiskfileActions.svelte b/src/lib/DiskfileActions.svelte index 91e7f8509..3f7d7c6c9 100644 --- a/src/lib/DiskfileActions.svelte +++ b/src/lib/DiskfileActions.svelte @@ -2,8 +2,8 @@ import CopyToClipboard from '@fuzdev/fuz_ui/CopyToClipboard.svelte'; import PasteFromClipboard from '@fuzdev/fuz_ui/PasteFromClipboard.svelte'; import {slide} from 'svelte/transition'; + import ConfirmButton from '@fuzdev/fuz_app/ui/ConfirmButton.svelte'; - import ConfirmButton from './ConfirmButton.svelte'; import {frontend_context} from './frontend.svelte.js'; import type {Diskfile} from './diskfile.svelte.js'; import ClearRestoreButton from './ClearRestoreButton.svelte'; diff --git a/src/lib/DiskfileEditorView.svelte b/src/lib/DiskfileEditorView.svelte index 4a0062488..29291458a 100644 --- a/src/lib/DiskfileEditorView.svelte +++ b/src/lib/DiskfileEditorView.svelte @@ -68,7 +68,7 @@ token_count={editor_state.current_token_count} placeholder={GLYPH_PLACEHOLDER + ' ' + diskfile.path_relative} readonly={false} - attrs={{class: 'height:100% border_radius_0'}} + attrs={{class: 'height:100% border-radius:0'}} onsave={async (value) => { await app.diskfiles.update(diskfile.path, value); }} diff --git a/src/lib/DiskfileHistoryView.svelte b/src/lib/DiskfileHistoryView.svelte index 95c0399b1..7d161f74c 100644 --- a/src/lib/DiskfileHistoryView.svelte +++ b/src/lib/DiskfileHistoryView.svelte @@ -1,8 +1,8 @@ - - -
- {#if button} - {@render button(popover)} - {:else} - - {/if} - - {#if popover.visible} -
- {@render popover_content(popover)} -
- {/if} -
diff --git a/src/lib/SocketMessageQueue.svelte b/src/lib/SocketMessageQueue.svelte index 68e9fae4d..dee13a8b4 100644 --- a/src/lib/SocketMessageQueue.svelte +++ b/src/lib/SocketMessageQueue.svelte @@ -3,12 +3,12 @@ import {format} from 'date-fns'; import {SvelteMap} from 'svelte/reactivity'; import CopyToClipboard from '@fuzdev/fuz_ui/CopyToClipboard.svelte'; + import ConfirmButton from '@fuzdev/fuz_app/ui/ConfirmButton.svelte'; + import PopoverButton from '@fuzdev/fuz_app/ui/PopoverButton.svelte'; import type {Socket, QueuedMessage, FailedMessage} from './socket.svelte.js'; import Glyph from './Glyph.svelte'; import {GLYPH_RETRY, GLYPH_REMOVE, GLYPH_INFO} from './glyphs.js'; - import ConfirmButton from './ConfirmButton.svelte'; - import PopoverButton from './PopoverButton.svelte'; import {format_timestamp} from './time_helpers.js'; import {DURATION_SM} from './helpers.js'; diff --git a/src/lib/ThreadListitem.svelte b/src/lib/ThreadListitem.svelte index 7d6b08d49..759983de2 100644 --- a/src/lib/ThreadListitem.svelte +++ b/src/lib/ThreadListitem.svelte @@ -1,8 +1,9 @@