From dfd01bd1f58a0d2f11cbc53fe1721267c0aed5e5 Mon Sep 17 00:00:00 2001 From: ogarciarevett Date: Mon, 1 Jun 2026 17:44:02 +0200 Subject: [PATCH 1/6] feat(ui): chatbot home + editable pipeline templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The post-onboarding home is a simple chatbot; the canvas demotes to a side activity panel. Builds on the shipped orchestration + live-trace backbone (consumed, not modified). specs/chatbot-home.md (#9 + #4). Storage (migration 007_chat_home): chat_sessions, chat_turns, pipeline_templates + a turns index, plus 6 synchronous Store methods (createSession, appendTurn, listSessions, listTurns, getTemplate, upsertTemplate). chat_turns.run_id links an assistant turn to the run that produced it (transcript bubble == activity-tree root). Router pipeline (packages/pipelines/router/): a chat message is a manual scheduler.run("router", {params}) (the EXISTING run path — no new execution). The handler classifies via ctx.complete to one label, maps it through a FIXED ALLOWLIST to a registered handler id, and ctx.spawns it; an unmapped/free-form label produces a clarify turn (no spawn, no dynamic handler id, preserving no-eval). The router merges the target's editable template default_params UNDER the user message, so an edited template configures its runs. Routes + WS: POST /api/chat, GET /api/chat/sessions, GET /api/chat/sessions/:id/turns, GET /api/pipelines, GET|PUT /api/pipelines/:id/template; a chat: WS topic next to the backbone's agent: (one socket, UUID-guarded). Client: transcript home + demoted activity panel (reuses the runTree render) + a templates screen; prefers-reduced-motion + WCAG-AA honored. Security: a minimal out-of-band approval-token module (vesper-core/src/approval/, CSPRNG single-use) gates PUT /template; POST /api/approval/request mints a code and prints it to the daemon TTY (out-of-band — never in the HTTP response, so a local app can mint but not read it). POST /api/chat is isLocalRequest-only (deliberate parity with the existing run route). No provider SDK. 724 tests / 0 fail; Biome clean; no new tsc errors. No Linear issue (workspace issue-capped) — cycle-log.md + this commit are the record (Rule 11 fallback). --- cycle-log.md | 39 +++ packages/pipelines/index.ts | 44 ++- packages/pipelines/router/handler.test.ts | 279 ++++++++++++++++ packages/pipelines/router/handler.ts | 174 ++++++++++ .../vesper-cli/src/commands/daemon-run.ts | 13 +- packages/vesper-core/src/approval/errors.ts | 17 + packages/vesper-core/src/approval/index.ts | 4 + .../vesper-core/src/approval/tokens.test.ts | 129 ++++++++ packages/vesper-core/src/approval/tokens.ts | 125 +++++++ packages/vesper-core/src/index.ts | 1 + .../vesper-core/src/scheduler/context.test.ts | 18 + packages/vesper-core/src/storage/index.ts | 8 + .../vesper-core/src/storage/migrations.ts | 30 ++ .../vesper-core/src/storage/store.test.ts | 216 ++++++++++++ packages/vesper-core/src/storage/store.ts | 182 ++++++++++ packages/vesper-core/src/storage/types.ts | 96 ++++++ packages/vesper-ui/src/client/chat-types.ts | 43 +++ packages/vesper-ui/src/client/chat.ts | 231 +++++++++++++ packages/vesper-ui/src/client/index.html | 173 +++++++++- packages/vesper-ui/src/client/main.ts | 39 ++- packages/vesper-ui/src/client/templates.ts | 311 ++++++++++++++++++ packages/vesper-ui/src/server/server.test.ts | 293 +++++++++++++++++ packages/vesper-ui/src/server/server.ts | 261 ++++++++++++++- 23 files changed, 2709 insertions(+), 17 deletions(-) create mode 100644 packages/pipelines/router/handler.test.ts create mode 100644 packages/pipelines/router/handler.ts create mode 100644 packages/vesper-core/src/approval/errors.ts create mode 100644 packages/vesper-core/src/approval/index.ts create mode 100644 packages/vesper-core/src/approval/tokens.test.ts create mode 100644 packages/vesper-core/src/approval/tokens.ts create mode 100644 packages/vesper-ui/src/client/chat-types.ts create mode 100644 packages/vesper-ui/src/client/chat.ts create mode 100644 packages/vesper-ui/src/client/templates.ts diff --git a/cycle-log.md b/cycle-log.md index abd2087..0275d51 100644 --- a/cycle-log.md +++ b/cycle-log.md @@ -651,3 +651,42 @@ the still-blocked forge sandbox (it executes no LLM-generated code). - DEFERRED (per spec Out of Scope): applying `fix_proposal`s (software-engineer pipeline); authoring pipeline CODE (forge, blocked on the sandbox); `NETWORK_FETCH`; a RAG/embedding index over signals; an elder-surface approval tile; auto-running skill-train on a newly acquired skill. + +## Chatbot home + editable pipeline templates (#9 + #4) — SHIPPED + +`specs/chatbot-home.md`. The post-onboarding HOME is a simple chatbot; the canvas demotes to a side +activity panel. Built on the SHIPPED orchestration+trace backbone (consumed, not modified). No Linear +issue (issue-capped) -> specs/ + this entry + the commit are the record (Rule 11). Built by a +Backend->Client->Review workflow; the review's 2 real HIGH gaps were then fixed by the lead. + +- **Storage (migration `007_chat_home`):** `chat_sessions`, `chat_turns`, `pipeline_templates` + index; + 6 synchronous `Store` methods (createSession, appendTurn, listSessions, listTurns, getTemplate, + upsertTemplate) mirroring the existing JSON/assert helpers. `chat_turns.run_id` links an assistant + turn to the run that produced it (transcript bubble == activity-tree root, same data two ways). +- **Router pipeline (`packages/pipelines/router/`):** a chat message is a manual `scheduler.run("router", + {params})` (the EXISTING run path — no new execution). The handler classifies via `ctx.complete` to ONE + label, maps it through a FIXED ALLOWLIST to a registered handler id, and `ctx.spawn`s it; an + unmapped/free-form label -> a clarify turn (NO spawn, no dynamic id — preserves no-eval). caps + [CLI_INVOKE, WRITE_STORAGE, SPAWN_SUBAGENT]. +- **Routes + WS:** `POST /api/chat`, `GET /api/chat/sessions`, `GET /api/chat/sessions/:id/turns`, + `GET /api/pipelines`, `GET|PUT /api/pipelines/:id/template`; a `chat:` WS topic next to the + backbone's `agent:` (one socket, UUID-guarded). Client: transcript home + demoted activity + panel (reuses the runTree render) + a templates screen; reduced-motion + WCAG-AA honored. +- **Security:** a minimal out-of-band approval-token module (`vesper-core/src/approval/`, CSPRNG + single-use) gates `PUT /template`; `POST /api/approval/request` mints a code and prints it to the daemon + TTY (out-of-band — never in the HTTP response, so a local app can mint but not read it). The future + `security-hardening.md` adopts this seam. `POST /api/chat` is isLocalRequest-only (deliberate parity + with the existing run route, so the canvas Run button still works). +- **Lead fixes over the workflow output** (2 real HIGHs the review caught): (1) `mint()` had NO production + caller -> added the `/api/approval/request` mint path + test, so template editing actually works + end-to-end; (2) the router ignored template `default_params` -> it now MERGES the target's editable + default_params UNDER the user message (injected via `registerPipelines({getDefaultParams})` -> daemon + wires `store.getTemplate`), so an edited template configures its runs (#4). + router/server tests. +- 724 tests / 0 fail (+ chatbot suite + the 2 fix tests); Biome clean; no NEW tsc errors (same 16 + pre-existing); no provider SDKs. +- NOTED (not blocking): `PUT /template` persists prompt/params only — schedule/caps stay editable via + `vesper schedule` (the spec's Design-Decisions/Acceptance contradict each other; took the conservative + path). Migration `007_chat_home` takes the next free id; the umbrella ledger's planning reservation + (007=rag) shifts to 008/009 for rag/eval (gitignored planning doc, reconciled at their build). +- DEFERRED (per spec Out of Scope): the security-hardening §C token formalization; multi-session history + UX; capability editing from the templates UI; token-level streaming. diff --git a/packages/pipelines/index.ts b/packages/pipelines/index.ts index c8d13ee..6ca0314 100644 --- a/packages/pipelines/index.ts +++ b/packages/pipelines/index.ts @@ -12,6 +12,7 @@ import { type Capability, type HandlerRegistry, type RegisterTaskInput, + type RunParams, type Scheduler, SchedulerError, type TaskHandler, @@ -28,6 +29,13 @@ import { orchestratorDemoHandler, orchestratorDemoTaskInput, } from "./orchestrator-demo/handler.ts"; +import { + makeRouterHandler, + ROUTE_ALLOWLIST, + ROUTER_HANDLER_ID, + routerHandler, + routerTaskInput, +} from "./router/handler.ts"; import { SELFTEST_HANDLER_ID, selftestHandler, selftestTaskInput } from "./selftest/handler.ts"; import { SKILL_TRAIN_HANDLER_ID, @@ -44,6 +52,10 @@ export { ORCHESTRATOR_DEMO_HANDLER_ID, orchestratorDemoHandler, orchestratorDemoTaskInput, + ROUTE_ALLOWLIST, + ROUTER_HANDLER_ID, + routerHandler, + routerTaskInput, SELFTEST_HANDLER_ID, SKILL_TRAIN_HANDLER_ID, selftestHandler, @@ -71,6 +83,14 @@ export const PIPELINES: readonly PipelineDescriptor[] = [ handler: selftestHandler, taskInput: selftestTaskInput, }, + // The chatbot-home dispatcher: a chat message is a manual run of this pipeline. It + // classifies the wish via the CLI and spawns one allowlisted built-in. Adds + // CLI_INVOKE + WRITE_STORAGE + SPAWN_SUBAGENT to the host grant union. + { + handlerId: ROUTER_HANDLER_ID, + handler: routerHandler, + taskInput: routerTaskInput, + }, { handlerId: SKILL_TRAIN_HANDLER_ID, handler: skillTrainHandler, @@ -126,9 +146,29 @@ export function grantedCapabilities(): Capability[] { * so a daemon restart backfills grants for tasks persisted before per-task grants * existed. No grant writing happens here; that would duplicate the ceiling check. */ -export function registerPipelines(scheduler: Scheduler, registry: HandlerRegistry): void { +/** Host-injected wiring for built-in pipelines (e.g. the router's template reader). */ +export interface RegisterPipelinesOptions { + /** + * Resolves a target handler's editable template `default_params` so the `router` + * merges them into spawn params (#4). When omitted, the router uses no defaults + * (the built-in handler), so non-daemon callers and tests behave unchanged. + */ + readonly getDefaultParams?: (handlerId: string) => RunParams; +} + +export function registerPipelines( + scheduler: Scheduler, + registry: HandlerRegistry, + options: RegisterPipelinesOptions = {}, +): void { for (const descriptor of PIPELINES) { - registry.register(descriptor.handlerId, descriptor.handler); + // The daemon injects the template reader into the router so edited templates take + // effect; every other handler registers as declared. + const handler = + descriptor.handlerId === ROUTER_HANDLER_ID && options.getDefaultParams !== undefined + ? makeRouterHandler({ getDefaultParams: options.getDefaultParams }) + : descriptor.handler; + registry.register(descriptor.handlerId, handler); // Spawn-only descriptors (no taskInput) register the handler only. if (descriptor.taskInput === undefined) continue; diff --git a/packages/pipelines/router/handler.test.ts b/packages/pipelines/router/handler.test.ts new file mode 100644 index 0000000..6472bad --- /dev/null +++ b/packages/pipelines/router/handler.test.ts @@ -0,0 +1,279 @@ +import { Database } from "bun:sqlite"; +import { describe, expect, test } from "bun:test"; +import { rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { + CompleteResult, + PipelineContext, + RunOutcome, + SubAgentDescriptor, + SubAgentHandle, +} from "@vesper/core"; +import { HandlerRegistry, openStore, Scheduler, type Store } from "@vesper/core"; +import { grantedCapabilities, registerPipelines } from "../index.ts"; +import { + makeRouterHandler, + ROUTE_ALLOWLIST, + ROUTER_HANDLER_ID, + routerTaskInput, +} from "./handler.ts"; + +// --------------------------------------------------------------------------- +// Fake PipelineContext — records complete prompts, spawn descriptors, and runs. +// --------------------------------------------------------------------------- + +interface FakeContext { + readonly ctx: PipelineContext; + readonly completePrompts: string[]; + readonly spawned: SubAgentDescriptor[]; + readonly recordedRuns: Array<{ status: string; summary: string }>; + readonly progress: string[]; +} + +function makeFakeContext(options: { + readonly params?: Record; + /** Text the fake `complete` returns (the classifier label). */ + readonly classifyReply?: string; + /** Status the spawned child resolves with. */ + readonly childStatus?: string; + /** When true, the spawned child's `done` rejects (handler must tolerate it). */ + readonly childRejects?: boolean; +}): FakeContext { + const completePrompts: string[] = []; + const spawned: SubAgentDescriptor[] = []; + const recordedRuns: Array<{ status: string; summary: string }> = []; + const progress: string[] = []; + + const ctx: PipelineContext = { + task: { + id: "router", + kind: "manual", + schedule_expr: "", + handler_id: "router", + enabled: true, + last_run_at: null, + last_error: null, + max_runs_per_day: null, + max_concurrent: null, + max_duration_ms: null, + runs_today: 0, + runs_today_date: null, + attempt_count: 0, + next_attempt_at: null, + required_capabilities: ["CLI_INVOKE", "WRITE_STORAGE", "SPAWN_SUBAGENT"], + }, + now: new Date(2025, 0, 1), + params: options.params ?? {}, + runId: "router-run", + parentRunId: null, + async complete(prompt): Promise { + completePrompts.push(prompt); + const text = options.classifyReply ?? "none"; + return { text, exit_code: 0, raw_stdout: text, raw_stderr: "", duration_ms: 1 }; + }, + recordRun({ status, summary }) { + recordedRuns.push({ status, summary }); + return "router-run"; + }, + emitProgress(e) { + progress.push(e.message); + }, + spawn(descriptor): SubAgentHandle { + spawned.push(descriptor); + const outcome: RunOutcome = { + taskId: descriptor.handlerId, + runId: "child-run", + status: options.childStatus ?? "ok", + summary: "child done", + cli: null, + durationMs: 1, + }; + return { + runId: "child-run", + handlerId: descriptor.handlerId, + label: descriptor.label, + done: options.childRejects + ? Promise.reject(new Error("child failed")) + : Promise.resolve(outcome), + }; + }, + readSignals() { + throw new Error("readSignals is not supported in this fake context"); + }, + }; + + return { ctx, completePrompts, spawned, recordedRuns, progress }; +} + +// --------------------------------------------------------------------------- +// routerHandler — classify + dispatch + no-eval fallback +// --------------------------------------------------------------------------- + +describe("routerHandler", () => { + test("an empty message produces a clarify turn with no spawn and no CLI call", async () => { + const fake = makeFakeContext({ params: { message: " " } }); + await makeRouterHandler()(fake.ctx); + + expect(fake.completePrompts).toHaveLength(0); + expect(fake.spawned).toHaveLength(0); + expect(fake.recordedRuns).toHaveLength(1); + expect(fake.recordedRuns[0]?.status).toBe("clarify"); + }); + + test("a mapped label spawns the allowlisted handler id with the message", async () => { + const fake = makeFakeContext({ + params: { message: "run the self test" }, + classifyReply: "selftest", + }); + await makeRouterHandler()(fake.ctx); + + expect(fake.completePrompts).toHaveLength(1); + expect(fake.spawned).toHaveLength(1); + expect(fake.spawned[0]?.handlerId).toBe(ROUTE_ALLOWLIST.selftest); + expect(fake.spawned[0]?.params?.message).toBe("run the self test"); + expect(fake.spawned[0]?.capabilities).toEqual(["WRITE_STORAGE"]); + expect(fake.recordedRuns[0]?.status).toBe("ok"); + }); + + test("merges the target template default_params UNDER the user message (#4)", async () => { + const fake = makeFakeContext({ + params: { message: "run the self test" }, + classifyReply: "selftest", + }); + await makeRouterHandler({ + // The injected reader supplies the target handler's editable default_params; + // a template-provided `message` must NOT override the real user message. + getDefaultParams: (handlerId) => + handlerId === ROUTE_ALLOWLIST.selftest ? { tone: "concise", message: "IGNORED" } : {}, + })(fake.ctx); + + expect(fake.spawned).toHaveLength(1); + expect(fake.spawned[0]?.params?.tone).toBe("concise"); + expect(fake.spawned[0]?.params?.message).toBe("run the self test"); + }); + + test("the classifier reply is normalised (case/whitespace/punctuation tolerated)", async () => { + const fake = makeFakeContext({ params: { message: "x" }, classifyReply: " SELFTEST.\n" }); + await makeRouterHandler()(fake.ctx); + expect(fake.spawned[0]?.handlerId).toBe(ROUTE_ALLOWLIST.selftest); + }); + + test("an unmapped label produces a clarify turn and NEVER spawns (no-eval invariant)", async () => { + const fake = makeFakeContext({ params: { message: "do my taxes" }, classifyReply: "taxes" }); + await makeRouterHandler()(fake.ctx); + + expect(fake.spawned).toHaveLength(0); + expect(fake.recordedRuns[0]?.status).toBe("clarify"); + }); + + test("a free-form handler-id reply is refused (cannot inject an arbitrary id)", async () => { + // The model returns a string that looks like a real handler id but is NOT a label key. + const fake = makeFakeContext({ + params: { message: "x" }, + classifyReply: "rm -rf; orchestrator-demo", + }); + await makeRouterHandler()(fake.ctx); + expect(fake.spawned).toHaveLength(0); + expect(fake.recordedRuns[0]?.status).toBe("clarify"); + }); + + test('the literal "none" reply maps to a clarify turn', async () => { + const fake = makeFakeContext({ params: { message: "x" }, classifyReply: "none" }); + await makeRouterHandler()(fake.ctx); + expect(fake.spawned).toHaveLength(0); + expect(fake.recordedRuns[0]?.status).toBe("clarify"); + }); + + test("a child failure is tolerated — the router records 'partial', not a throw", async () => { + const fake = makeFakeContext({ + params: { message: "x" }, + classifyReply: "selftest", + childRejects: true, + }); + await makeRouterHandler()(fake.ctx); + expect(fake.spawned).toHaveLength(1); + expect(fake.recordedRuns[0]?.status).toBe("partial"); + }); + + test("a custom allowlist drives dispatch (handler is configurable)", async () => { + const fake = makeFakeContext({ params: { message: "x" }, classifyReply: "greet" }); + await makeRouterHandler({ allowlist: { greet: "selftest" } })(fake.ctx); + expect(fake.spawned[0]?.handlerId).toBe("selftest"); + }); + + test("the classify prompt enumerates the allowlist labels and fences the message", async () => { + const fake = makeFakeContext({ params: { message: "SECRET" }, classifyReply: "none" }); + await makeRouterHandler()(fake.ctx); + const prompt = fake.completePrompts[0] ?? ""; + expect(prompt).toContain("selftest"); + expect(prompt).toContain("orchestrate"); + expect(prompt).toContain("SECRET"); + }); +}); + +// --------------------------------------------------------------------------- +// Registration — the router is installed with the right capabilities. +// --------------------------------------------------------------------------- + +describe("router registration", () => { + let path: string; + let db: Database; + let store: Store; + + function setup(): { registry: HandlerRegistry; scheduler: Scheduler } { + path = join(tmpdir(), `vesper-router-${crypto.randomUUID()}.db`); + openStore(path).close(); + db = new Database(path); + store = openStore(path); + const registry = new HandlerRegistry(); + const scheduler = new Scheduler({ db, registry, grants: grantedCapabilities() }); + registerPipelines(scheduler, registry); + return { registry, scheduler }; + } + + function teardown(): void { + db.close(); + store.close(); + try { + rmSync(path, { force: true }); + rmSync(`${path}-shm`, { force: true }); + rmSync(`${path}-wal`, { force: true }); + } catch { + // ignore + } + } + + test("registers a manual router task requiring CLI_INVOKE + WRITE_STORAGE + SPAWN_SUBAGENT", () => { + const { scheduler } = setup(); + try { + const task = scheduler.list().find((t) => t.id === "router"); + expect(task).toBeDefined(); + expect(task?.kind).toBe("manual"); + expect(task?.required_capabilities).toContain("CLI_INVOKE"); + expect(task?.required_capabilities).toContain("WRITE_STORAGE"); + expect(task?.required_capabilities).toContain("SPAWN_SUBAGENT"); + } finally { + teardown(); + } + }); + + test("the router's spawn targets are all registered handlers (allowlist is resolvable)", () => { + const { registry } = setup(); + try { + for (const handlerId of Object.values(ROUTE_ALLOWLIST)) { + expect(registry.has(handlerId)).toBe(true); + } + expect(registry.has(ROUTER_HANDLER_ID)).toBe(true); + } finally { + teardown(); + } + }); + + test("the host grant union covers the router task input capabilities", () => { + const granted = grantedCapabilities(); + for (const cap of routerTaskInput.required_capabilities ?? []) { + expect(granted).toContain(cap); + } + }); +}); diff --git a/packages/pipelines/router/handler.ts b/packages/pipelines/router/handler.ts new file mode 100644 index 0000000..fa2837e --- /dev/null +++ b/packages/pipelines/router/handler.ts @@ -0,0 +1,174 @@ +/** + * The `router` pipeline — the chatbot-home dispatcher. + * + * A chat message is a manual run of THIS pipeline through the existing run path + * (`POST /api/chat` -> `scheduler.run("router", { params })`). The handler reads the + * user's message from `ctx.params.message`, asks the user's authenticated CLI + * (`ctx.complete`, CLI_INVOKE — Hard rule 12, no provider SDK) to CLASSIFY it to ONE + * label, maps that label through a FIXED ALLOWLIST to a registered handler id, and + * `ctx.spawn`s that pipeline as a sub-agent (the live tree under the transcript turn). + * + * SAFETY (non-negotiable): the handler id is NEVER taken from the model's free-form + * text. The classifier returns a label; only a label present in {@link ROUTE_ALLOWLIST} + * resolves to a handler id. An unmapped/unknown/free-form label produces a + * clarifying-question turn (a recorded run, NO spawn, NO dynamic handler id) — this + * preserves the no-dynamic-eval invariant the scheduler relies on. + * + * Capabilities: `CLI_INVOKE` (classify), `WRITE_STORAGE` (record + emit trace), + * `SPAWN_SUBAGENT` (dispatch). All asserted at the context boundary before any effect. + */ + +import type { Capability, RegisterTaskInput, RunParams, TaskHandler } from "@vesper/core"; + +/** Allowlisted handler id referenced by the `router` task. */ +export const ROUTER_HANDLER_ID = "router"; + +/** + * The fixed label -> handler-id allowlist. The classifier may ONLY pick a key here; + * the value is the registered handler id the router spawns. Anything outside this map + * (including a free-form id the model might emit) is refused and becomes a clarifying + * turn. Built-in spawn targets only — flagship pipelines extend this map as they land. + */ +export const ROUTE_ALLOWLIST: Readonly> = { + selftest: "selftest", + orchestrate: "orchestrator-demo", +} as const; + +/** The capability the spawned child is granted — `WRITE_STORAGE` only (it records + traces). */ +const CHILD_CAPABILITIES: readonly Capability[] = ["WRITE_STORAGE"]; + +/** Max characters of the user message embedded in the classify prompt (bound the prompt). */ +const MESSAGE_MAX_LENGTH = 2_000; + +/** Read the user message from params; empty string when absent/non-string. */ +function readMessage(params: RunParams): string { + const raw = params.message; + return typeof raw === "string" ? raw.slice(0, MESSAGE_MAX_LENGTH) : ""; +} + +/** + * Build the classify prompt: the model must answer with EXACTLY one label from the + * allowlist (or the literal `none`). The allowlist is interpolated so the model knows + * the closed set; the user's message is fenced so it cannot rewrite the instruction. + */ +function buildClassifyPrompt(message: string, labels: readonly string[]): string { + return [ + "You are a strict intent classifier for a local automation runtime.", + `Choose EXACTLY ONE label from this closed set: ${labels.join(", ")}, none.`, + 'Answer with the single label only — no punctuation, no explanation. Use "none" when', + "the request matches no label or is ambiguous.", + "", + "User request:", + "<<<", + message, + ">>>", + ].join("\n"); +} + +/** + * Normalise the model's reply to a label key: trim, lowercase, and keep only the first + * token of word characters. A reply that is not an exact allowlist key resolves to null + * (treated as `none`) — the model can never inject an arbitrary handler id this way. + */ +function resolveLabel(reply: string, allowlist: Readonly>): string | null { + const token = + reply + .trim() + .toLowerCase() + .match(/[a-z0-9_-]+/)?.[0] ?? ""; + return Object.hasOwn(allowlist, token) ? token : null; +} + +/** Dependencies that make the router handler unit-testable; all default to the built-ins. */ +export interface RouterHandlerOptions { + /** The label -> handler-id allowlist. Defaults to {@link ROUTE_ALLOWLIST}. */ + readonly allowlist?: Readonly>; + /** + * Returns the editable template `default_params` for a target handler id, which the + * router MERGES under the user message into the spawn params (so an edited pipeline + * template actually affects its runs — #4). Host-injected (the daemon wires it to + * `store.getTemplate`); defaults to no defaults when absent (tests / non-daemon). + */ + readonly getDefaultParams?: (handlerId: string) => RunParams; +} + +/** + * Build the router handler. The default export {@link routerHandler} uses the built-in + * allowlist; tests inject a custom allowlist to assert dispatch + the no-eval fallback. + */ +export function makeRouterHandler(options: RouterHandlerOptions = {}): TaskHandler { + const allowlist = options.allowlist ?? ROUTE_ALLOWLIST; + const getDefaultParams = options.getDefaultParams; + const labels = Object.keys(allowlist); + + return async (ctx) => { + const message = readMessage(ctx.params); + + if (message.trim().length === 0) { + ctx.emitProgress({ kind: "step", message: "empty message — asking for clarification" }); + ctx.recordRun({ + status: "clarify", + summary: "I did not catch that — could you say what you would like me to do?", + }); + return; + } + + ctx.emitProgress({ kind: "step", message: "classifying request" }); + const result = await ctx.complete(buildClassifyPrompt(message, labels)); + const label = resolveLabel(result.text, allowlist); + + // No-eval fallback: an unmapped/free-form label NEVER becomes a handler id. + if (label === null) { + ctx.emitProgress({ + kind: "step", + message: "no matching pipeline — asking for clarification", + }); + ctx.recordRun({ + status: "clarify", + summary: + "I am not sure which automation fits that. Could you rephrase, or tell me the task " + + "in a few words?", + }); + return; + } + + const handlerId = allowlist[label] as string; + ctx.emitProgress({ + kind: "spawn", + message: `dispatching to "${handlerId}"`, + data: { label, handlerId }, + }); + + // Merge the target's editable template default_params UNDER the user message, so an + // edited template configures its runs (#4) without ever overriding the message. + const templateParams = getDefaultParams?.(handlerId) ?? {}; + const handle = ctx.spawn({ + handlerId, + label, + params: { ...templateParams, message }, + capabilities: CHILD_CAPABILITIES, + }); + const childOutcome = await handle.done.catch(() => null); + + ctx.recordRun({ + status: childOutcome?.status === "ok" ? "ok" : "partial", + summary: `routed to ${handlerId} (run ${handle.runId})`, + }); + }; +} + +/** The built-in router handler (default allowlist). */ +export const routerHandler: TaskHandler = makeRouterHandler(); + +/** + * Manual task wiring for the `router` pipeline. Requires `CLI_INVOKE` (classify), + * `WRITE_STORAGE` (record + emit trace), and `SPAWN_SUBAGENT` (dispatch). + */ +export const routerTaskInput: RegisterTaskInput = { + id: "router", + kind: "manual", + schedule_expr: "", + handler_id: ROUTER_HANDLER_ID, + max_duration_ms: 120_000, + required_capabilities: ["CLI_INVOKE", "WRITE_STORAGE", "SPAWN_SUBAGENT"], +}; diff --git a/packages/vesper-cli/src/commands/daemon-run.ts b/packages/vesper-cli/src/commands/daemon-run.ts index 1ff23fb..7d6f81d 100644 --- a/packages/vesper-cli/src/commands/daemon-run.ts +++ b/packages/vesper-cli/src/commands/daemon-run.ts @@ -1,6 +1,7 @@ import { Database } from "bun:sqlite"; import { mkdir } from "node:fs/promises"; import { + ApprovalTokenStore, DEFAULT_AGENT_MATCHERS, detectAvailableCLIs, HandlerRegistry, @@ -67,21 +68,29 @@ export const daemonRunCommand: Command = { complete, redactSummaries: config.storage?.redactRunSummaries === true, }); - registerPipelines(scheduler, registry); + // The UI store is opened first so the router can read editable template + // default_params through it (#4) — an edited template then affects its runs. + const uiStore = openStore(dbPath()); + registerPipelines(scheduler, registry, { + getDefaultParams: (handlerId) => uiStore.getTemplate(handlerId)?.defaultParams ?? {}, + }); // Host the Vesper World UI in-process (one runtime): the UI reads this // scheduler + storage directly and gets live run events off its EventBus. // Agent-presence detection uses the built-in allowlist plus any matchers // the user added under `presence.matchers` in config; `presence.pollMs` // overrides the scan interval. - const uiStore = openStore(dbPath()); const presenceMatchers = [...DEFAULT_AGENT_MATCHERS, ...(config.presence?.matchers ?? [])]; + // Out-of-band approval tokens for privileged config mutations (template edits). + // In-memory + per-process: a daemon restart invalidates every outstanding code. + const approvalTokens = new ApprovalTokenStore(); const ui = await startUiServer({ scheduler, store: uiStore, seed: machineFingerprint(), port: uiPort(), detectPresences: presenceDetectorFor(presenceMatchers), + approvalTokens, ...(config.presence?.pollMs !== undefined ? { presencePollMs: config.presence.pollMs } : {}), ...(config.ui?.theme !== undefined ? { defaultTheme: config.ui.theme } : {}), }); diff --git a/packages/vesper-core/src/approval/errors.ts b/packages/vesper-core/src/approval/errors.ts new file mode 100644 index 0000000..7ced691 --- /dev/null +++ b/packages/vesper-core/src/approval/errors.ts @@ -0,0 +1,17 @@ +/** The reason an approval-token operation failed. */ +export type ApprovalErrorReason = "not_found" | "expired" | "already_used"; + +/** + * Thrown by the approval-token store when a token cannot be verified — it was + * never minted, has expired, or was already consumed (single-use). Carries a + * typed `reason` so a route can map it to the right HTTP status. + */ +export class ApprovalError extends Error { + readonly reason: ApprovalErrorReason; + + constructor(reason: ApprovalErrorReason, message: string, options?: { cause?: unknown }) { + super(message, options); + this.name = "ApprovalError"; + this.reason = reason; + } +} diff --git a/packages/vesper-core/src/approval/index.ts b/packages/vesper-core/src/approval/index.ts new file mode 100644 index 0000000..080d510 --- /dev/null +++ b/packages/vesper-core/src/approval/index.ts @@ -0,0 +1,4 @@ +export type { ApprovalErrorReason } from "./errors.ts"; +export { ApprovalError } from "./errors.ts"; +export type { ApprovalTokenStoreOptions } from "./tokens.ts"; +export { ApprovalTokenStore, DEFAULT_TOKEN_TTL_MS } from "./tokens.ts"; diff --git a/packages/vesper-core/src/approval/tokens.test.ts b/packages/vesper-core/src/approval/tokens.test.ts new file mode 100644 index 0000000..0932609 --- /dev/null +++ b/packages/vesper-core/src/approval/tokens.test.ts @@ -0,0 +1,129 @@ +import { describe, expect, test } from "bun:test"; +import { ApprovalError } from "./errors.ts"; +import { ApprovalTokenStore } from "./tokens.ts"; + +/** A deterministic clock seam whose current value the test controls. */ +function fixedClock(start = 1_000): { now: () => number; advance: (ms: number) => void } { + let t = start; + return { + now: () => t, + advance: (ms) => { + t += ms; + }, + }; +} + +describe("ApprovalTokenStore", () => { + test("mint returns a non-empty lowercase-hex code", () => { + const store = new ApprovalTokenStore(); + const code = store.mint(); + expect(code).toMatch(/^[0-9a-f]+$/); + expect(code.length).toBeGreaterThanOrEqual(16); + }); + + test("mint produces distinct codes (CSPRNG)", () => { + const store = new ApprovalTokenStore(); + const codes = new Set([store.mint(), store.mint(), store.mint()]); + expect(codes.size).toBe(3); + }); + + test("verify succeeds once for a valid code", () => { + const store = new ApprovalTokenStore(); + const code = store.mint(); + expect(() => store.verify(code)).not.toThrow(); + }); + + test("verify is single-use — a replay throws already_used", () => { + const store = new ApprovalTokenStore(); + const code = store.mint(); + store.verify(code); + try { + store.verify(code); + throw new Error("expected verify to throw"); + } catch (err) { + expect(err).toBeInstanceOf(ApprovalError); + expect((err as ApprovalError).reason).toBe("already_used"); + } + }); + + test("verify of an unknown code throws not_found", () => { + const store = new ApprovalTokenStore(); + try { + store.verify("deadbeef"); + throw new Error("expected verify to throw"); + } catch (err) { + expect(err).toBeInstanceOf(ApprovalError); + expect((err as ApprovalError).reason).toBe("not_found"); + } + }); + + test("verify of an expired code throws expired", () => { + const clock = fixedClock(); + const store = new ApprovalTokenStore({ ttlMs: 100, now: clock.now }); + const code = store.mint(); + clock.advance(101); + try { + store.verify(code); + throw new Error("expected verify to throw"); + } catch (err) { + expect(err).toBeInstanceOf(ApprovalError); + expect((err as ApprovalError).reason).toBe("expired"); + } + }); + + test("a code is valid right up to (but not at) its TTL boundary", () => { + const clock = fixedClock(); + const store = new ApprovalTokenStore({ ttlMs: 100, now: clock.now }); + const code = store.mint(); + clock.advance(99); + expect(store.isValid(code)).toBe(true); + clock.advance(1); // now == expiresAt + expect(store.isValid(code)).toBe(false); + }); + + test("isValid does not consume the code (verify still succeeds afterwards)", () => { + const store = new ApprovalTokenStore(); + const code = store.mint(); + expect(store.isValid(code)).toBe(true); + expect(store.isValid(code)).toBe(true); + expect(() => store.verify(code)).not.toThrow(); + }); + + test("isValid is false for unknown/used codes", () => { + const store = new ApprovalTokenStore(); + expect(store.isValid("nope")).toBe(false); + const code = store.mint(); + store.verify(code); + expect(store.isValid(code)).toBe(false); + }); + + test("prune drops expired and used entries (later verify is not_found)", () => { + const clock = fixedClock(); + const store = new ApprovalTokenStore({ ttlMs: 100, now: clock.now }); + const expiring = store.mint(); + clock.advance(101); + store.prune(); + try { + store.verify(expiring); + throw new Error("expected verify to throw"); + } catch (err) { + expect((err as ApprovalError).reason).toBe("not_found"); + } + }); + + test("ttl is clamped to a minimum of 1ms", () => { + const clock = fixedClock(); + const store = new ApprovalTokenStore({ ttlMs: 0, now: clock.now }); + const code = store.mint(); + // With ttl clamped to >=1, the code is valid at mint time (now < expiresAt). + expect(store.isValid(code)).toBe(true); + }); + + test("injected randomBytes seam is used (deterministic code)", () => { + const store = new ApprovalTokenStore({ + randomBytes: (out) => out.fill(0xab), + }); + const code = store.mint(); + expect(code).toBe("ab".repeat(code.length / 2)); + }); +}); diff --git a/packages/vesper-core/src/approval/tokens.ts b/packages/vesper-core/src/approval/tokens.ts new file mode 100644 index 0000000..74f8783 --- /dev/null +++ b/packages/vesper-core/src/approval/tokens.ts @@ -0,0 +1,125 @@ +/** + * Out-of-band approval tokens for privileged, out-of-band mutations. + * + * The daemon mints a short, single-use code (CSPRNG) with a short TTL; a privileged + * route (e.g. `PUT /api/pipelines/:id/template`) requires the caller to present a + * valid, unexpired, unconsumed code. This is the minimal self-contained module the + * future `security-hardening.md` adopts — it adds a SECOND factor over `isLocalRequest` + * so a malicious local script cannot silently rewrite a pipeline's config without the + * code the daemon surfaced to the operator out-of-band. + * + * The store is in-memory by design: tokens are ephemeral and per-daemon-process; a + * restart invalidates every outstanding code (fail-closed). No token is ever persisted + * or logged. `crypto.getRandomValues` is the CSPRNG seam; `() => Date.now()` is the + * clock seam (injectable for deterministic tests). + */ + +import { ApprovalError } from "./errors.ts"; + +/** Default time-to-live for a minted token (5 minutes). */ +export const DEFAULT_TOKEN_TTL_MS = 5 * 60 * 1_000; + +/** Number of random bytes behind a code; 12 bytes -> 24 lowercase hex chars. */ +const TOKEN_BYTES = 12; + +/** A live token entry: when it expires, and whether it was already consumed. */ +interface TokenEntry { + readonly expiresAt: number; + used: boolean; +} + +/** Options for {@link ApprovalTokenStore}. */ +export interface ApprovalTokenStoreOptions { + /** Token lifetime in ms. Defaults to {@link DEFAULT_TOKEN_TTL_MS}. Clamped to >= 1. */ + readonly ttlMs?: number; + /** Clock seam (ms since epoch). Defaults to `Date.now`. Inject for tests. */ + readonly now?: () => number; + /** + * CSPRNG seam — fills the given buffer with random bytes. Defaults to + * `crypto.getRandomValues`. Inject ONLY for tests; production must use a CSPRNG. + */ + readonly randomBytes?: (out: Uint8Array) => void; +} + +/** Lower-hex encode a byte buffer (no separators). */ +function toHex(bytes: Uint8Array): string { + let out = ""; + for (const b of bytes) { + out += b.toString(16).padStart(2, "0"); + } + return out; +} + +/** + * In-memory store of single-use, short-TTL approval codes. + * + * - `mint()` returns a fresh CSPRNG code and records its expiry. + * - `verify(code)` consumes the code: it succeeds exactly once for a valid, + * unexpired code and throws {@link ApprovalError} otherwise (`not_found`, + * `expired`, `already_used`). Verifying marks the code used so a replay fails. + */ +export class ApprovalTokenStore { + readonly #tokens = new Map(); + readonly #ttlMs: number; + readonly #now: () => number; + readonly #randomBytes: (out: Uint8Array) => void; + + constructor(options: ApprovalTokenStoreOptions = {}) { + this.#ttlMs = Math.max(1, options.ttlMs ?? DEFAULT_TOKEN_TTL_MS); + this.#now = options.now ?? (() => Date.now()); + this.#randomBytes = options.randomBytes ?? ((out) => crypto.getRandomValues(out)); + } + + /** Mint a fresh single-use code and return it. The raw code is never persisted to disk. */ + mint(): string { + const buf = new Uint8Array(TOKEN_BYTES); + this.#randomBytes(buf); + const code = toHex(buf); + this.#tokens.set(code, { expiresAt: this.#now() + this.#ttlMs, used: false }); + return code; + } + + /** + * Consume `code`. Returns nothing on success (the code is now spent). Throws + * {@link ApprovalError}: + * - `not_found` when the code was never minted (or was purged after expiry); + * - `expired` when the code is past its TTL (it is dropped); + * - `already_used` when the code was previously verified (replay). + */ + verify(code: string): void { + const entry = this.#tokens.get(code); + if (entry === undefined) { + throw new ApprovalError("not_found", "approval code is not recognised"); + } + if (this.#now() >= entry.expiresAt) { + this.#tokens.delete(code); + throw new ApprovalError("expired", "approval code has expired"); + } + if (entry.used) { + throw new ApprovalError("already_used", "approval code was already used"); + } + entry.used = true; + } + + /** + * Non-consuming check used by routes that want a boolean. Returns true iff the + * code is valid, unexpired, and unused — but does NOT mark it used. Prefer + * {@link verify} on the mutation path so the code is single-use. + */ + isValid(code: string): boolean { + const entry = this.#tokens.get(code); + if (entry === undefined) return false; + if (this.#now() >= entry.expiresAt) return false; + return !entry.used; + } + + /** Drop expired/used entries so the map does not grow unbounded. */ + prune(): void { + const now = this.#now(); + for (const [code, entry] of this.#tokens) { + if (entry.used || now >= entry.expiresAt) { + this.#tokens.delete(code); + } + } + } +} diff --git a/packages/vesper-core/src/index.ts b/packages/vesper-core/src/index.ts index 398b9f8..05d7fdb 100644 --- a/packages/vesper-core/src/index.ts +++ b/packages/vesper-core/src/index.ts @@ -1,6 +1,7 @@ // @vesper/core — host runtime public surface. // Modules are re-exported here as they land through the Foundation feature loop. +export * from "./approval/index.ts"; export * from "./auto-evolve/index.ts"; export * from "./capabilities/index.ts"; export * from "./cli/index.ts"; diff --git a/packages/vesper-core/src/scheduler/context.test.ts b/packages/vesper-core/src/scheduler/context.test.ts index 8b97bf0..961e764 100644 --- a/packages/vesper-core/src/scheduler/context.test.ts +++ b/packages/vesper-core/src/scheduler/context.test.ts @@ -89,6 +89,24 @@ function makeStore(): { getTaskGrant() { return null; }, + // Chat-home Store methods (migration 007): unused by the context double, stubbed + // so the mock still satisfies the widened Store interface. + createSession() { + return "session-id"; + }, + appendTurn() { + return "turn-id"; + }, + listSessions() { + return []; + }, + listTurns() { + return []; + }, + getTemplate() { + return null; + }, + upsertTemplate() {}, close() {}, }; return { store, finished, events }; diff --git a/packages/vesper-core/src/storage/index.ts b/packages/vesper-core/src/storage/index.ts index 278353b..581ab65 100644 --- a/packages/vesper-core/src/storage/index.ts +++ b/packages/vesper-core/src/storage/index.ts @@ -4,11 +4,18 @@ export { openStore } from "./store.ts"; export type { AppendEventInput, AppendRunEventInput, + AppendTurnInput, + ChatSessionRow, + ChatTurnRole, + ChatTurnRow, + CreateSessionInput, EventRow, FinishRunInput, ListEventsOptions, ListRunEventsOptions, ListRunsOptions, + ListTurnsOptions, + PipelineTemplateRow, RecordRunInput, RunEventKind, RunEventRow, @@ -18,4 +25,5 @@ export type { Store, TaskGrant, UpsertTaskGrantInput, + UpsertTemplateInput, } from "./types.ts"; diff --git a/packages/vesper-core/src/storage/migrations.ts b/packages/vesper-core/src/storage/migrations.ts index 7e981a6..51340d5 100644 --- a/packages/vesper-core/src/storage/migrations.ts +++ b/packages/vesper-core/src/storage/migrations.ts @@ -108,4 +108,34 @@ export const MIGRATIONS: readonly Migration[] = [ CREATE INDEX IF NOT EXISTS idx_run_events_run ON run_events(run_id, ts); `, }, + { + // The chatbot-home surface: a chat session + transcript model and per-pipeline + // editable templates. Each assistant turn carries the `run_id` of the router run + // that produced it, so a transcript bubble and the live activity tree are the same + // data viewed two ways. Forward-only; appended AFTER 006. The `events` table stays + // the durable audit trail (every chat/template mutation also writes an event there). + id: "007_chat_home", + sql: ` + CREATE TABLE IF NOT EXISTS chat_sessions ( + id TEXT PRIMARY KEY NOT NULL, + ts INTEGER NOT NULL, + title TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS chat_turns ( + id TEXT PRIMARY KEY NOT NULL, + session_id TEXT NOT NULL, + ts INTEGER NOT NULL, + role TEXT NOT NULL, + text TEXT NOT NULL, + run_id TEXT + ); + CREATE INDEX IF NOT EXISTS idx_chat_turns_session ON chat_turns(session_id, ts); + CREATE TABLE IF NOT EXISTS pipeline_templates ( + handler_id TEXT PRIMARY KEY NOT NULL, + prompt TEXT NOT NULL DEFAULT '', + default_params TEXT NOT NULL DEFAULT '{}', + updated_at INTEGER NOT NULL + ); + `, + }, ]; diff --git a/packages/vesper-core/src/storage/store.test.ts b/packages/vesper-core/src/storage/store.test.ts index 01cbf8b..19b827a 100644 --- a/packages/vesper-core/src/storage/store.test.ts +++ b/packages/vesper-core/src/storage/store.test.ts @@ -889,3 +889,219 @@ describe("listRuns parentRunId filter and runTree", () => { expect(store.runTree("does-not-exist")).toBeNull(); }); }); + +// --------------------------------------------------------------------------- +// Chat home (migration 007_chat_home) +// --------------------------------------------------------------------------- + +describe("chat sessions and turns", () => { + let path: string; + let store: Store; + + beforeEach(() => { + path = tempDbPath(); + store = openStore(path); + }); + + afterEach(() => { + store.close(); + try { + rmSync(path, { force: true }); + rmSync(`${path}-shm`, { force: true }); + rmSync(`${path}-wal`, { force: true }); + } catch { + // ignore + } + }); + + test("createSession returns a generated id and listSessions reads it back", () => { + const id = store.createSession({ title: "first wish" }); + expect(typeof id).toBe("string"); + expect(id.length).toBeGreaterThan(0); + + const sessions = store.listSessions(); + expect(sessions).toHaveLength(1); + expect(sessions[0]?.id).toBe(id); + expect(sessions[0]?.title).toBe("first wish"); + expect(typeof sessions[0]?.ts).toBe("number"); + }); + + test("createSession honors a supplied id", () => { + const id = store.createSession({ id: "11111111-1111-4111-8111-111111111111", title: "x" }); + expect(id).toBe("11111111-1111-4111-8111-111111111111"); + }); + + test("listSessions is newest-first", () => { + const a = store.createSession({ title: "a" }); + const b = store.createSession({ title: "b" }); + const ids = store.listSessions().map((s) => s.id); + // b was created after a, so it sorts first (ts DESC). + expect(ids[0]).toBe(b); + expect(ids).toContain(a); + }); + + test("appendTurn persists user and assistant turns; listTurns is oldest-first", () => { + const session = store.createSession({ title: "t" }); + const userTurn = store.appendTurn({ sessionId: session, role: "user", text: "do a thing" }); + const asstTurn = store.appendTurn({ + sessionId: session, + role: "assistant", + text: "on it", + runId: "22222222-2222-4222-8222-222222222222", + }); + + const turns = store.listTurns({ sessionId: session }); + expect(turns.map((t) => t.id)).toEqual([userTurn, asstTurn]); + expect(turns[0]?.role).toBe("user"); + expect(turns[0]?.runId).toBeNull(); + expect(turns[1]?.role).toBe("assistant"); + expect(turns[1]?.runId).toBe("22222222-2222-4222-8222-222222222222"); + }); + + test("listTurns filters by afterTs and respects limit", () => { + const session = store.createSession({ title: "t" }); + store.appendTurn({ sessionId: session, role: "user", text: "one" }); + const all = store.listTurns({ sessionId: session }); + const firstTs = all[0]?.ts ?? 0; + + // afterTs strictly greater — the only turn (ts == firstTs) is excluded. + expect(store.listTurns({ sessionId: session, afterTs: firstTs })).toHaveLength(0); + + store.appendTurn({ sessionId: session, role: "assistant", text: "two" }); + store.appendTurn({ sessionId: session, role: "assistant", text: "three" }); + expect(store.listTurns({ sessionId: session, limit: 1 })).toHaveLength(1); + }); + + test("listTurns scopes to its session only", () => { + const s1 = store.createSession({ title: "s1" }); + const s2 = store.createSession({ title: "s2" }); + store.appendTurn({ sessionId: s1, role: "user", text: "a" }); + store.appendTurn({ sessionId: s2, role: "user", text: "b" }); + expect(store.listTurns({ sessionId: s1 })).toHaveLength(1); + expect(store.listTurns({ sessionId: s1 })[0]?.text).toBe("a"); + }); + + test("turns survive reopen (durable transcript)", () => { + const session = store.createSession({ title: "t" }); + store.appendTurn({ sessionId: session, role: "user", text: "persisted" }); + store.close(); + + const reopened = openStore(path); + const turns = reopened.listTurns({ sessionId: session }); + reopened.close(); + expect(turns).toHaveLength(1); + expect(turns[0]?.text).toBe("persisted"); + }); + + test("a corrupted role column is rejected on read (corruption guard)", () => { + const session = store.createSession({ title: "t" }); + // Write a row with an out-of-allowlist role directly. + const db = new Database(path); + db.run( + "INSERT INTO chat_turns (id, session_id, ts, role, text, run_id) VALUES ('bad', ?, 1, 'system', 'x', NULL)", + [session], + ); + db.close(); + expect(() => store.listTurns({ sessionId: session })).toThrow(StorageError); + }); +}); + +describe("pipeline_templates round-trip", () => { + let path: string; + let store: Store; + + beforeEach(() => { + path = tempDbPath(); + store = openStore(path); + }); + + afterEach(() => { + store.close(); + try { + rmSync(path, { force: true }); + rmSync(`${path}-shm`, { force: true }); + rmSync(`${path}-wal`, { force: true }); + } catch { + // ignore + } + }); + + test("getTemplate returns null before any upsert", () => { + expect(store.getTemplate("router")).toBeNull(); + }); + + test("upsertTemplate then getTemplate round-trips prompt + default params", () => { + store.upsertTemplate({ + handlerId: "router", + prompt: "classify strictly", + defaultParams: { tone: "warm", retries: 2 }, + }); + const t = store.getTemplate("router"); + expect(t).not.toBeNull(); + expect(t?.handlerId).toBe("router"); + expect(t?.prompt).toBe("classify strictly"); + expect(t?.defaultParams).toEqual({ tone: "warm", retries: 2 }); + expect(typeof t?.updatedAt).toBe("number"); + }); + + test("upsertTemplate updates an existing row (ON CONFLICT)", () => { + store.upsertTemplate({ handlerId: "router", prompt: "v1", defaultParams: {} }); + store.upsertTemplate({ handlerId: "router", prompt: "v2", defaultParams: { a: 1 } }); + const t = store.getTemplate("router"); + expect(t?.prompt).toBe("v2"); + expect(t?.defaultParams).toEqual({ a: 1 }); + }); + + test("template survives reopen", () => { + store.upsertTemplate({ handlerId: "router", prompt: "kept", defaultParams: {} }); + store.close(); + const reopened = openStore(path); + expect(reopened.getTemplate("router")?.prompt).toBe("kept"); + reopened.close(); + }); +}); + +describe("migration 007 — chat home", () => { + let path: string; + + beforeEach(() => { + path = tempDbPath(); + }); + + afterEach(() => { + try { + rmSync(path, { force: true }); + rmSync(`${path}-shm`, { force: true }); + rmSync(`${path}-wal`, { force: true }); + } catch { + // ignore + } + }); + + test("schema_migrations records 007 and reopen is idempotent (chat tables queryable)", () => { + const first = openStore(path); + first.close(); + const second = openStore(path); + second.close(); + + const db = new Database(path, { readonly: true }); + const ids = db + .query<{ id: string }, []>("SELECT id FROM schema_migrations") + .all() + .map((r) => r.id); + expect(() => db.query("SELECT count(*) FROM chat_sessions").get()).not.toThrow(); + expect(() => db.query("SELECT count(*) FROM chat_turns").get()).not.toThrow(); + expect(() => db.query("SELECT count(*) FROM pipeline_templates").get()).not.toThrow(); + db.close(); + + expect(ids).toContain("007_chat_home"); + expect(ids.filter((id) => id === "007_chat_home")).toHaveLength(1); + }); + + test("007 is sequenced AFTER 006 (forward-only ordering)", () => { + const idx006 = MIGRATIONS.findIndex((m) => m.id.startsWith("006")); + const idx007 = MIGRATIONS.findIndex((m) => m.id === "007_chat_home"); + expect(idx006).toBeGreaterThanOrEqual(0); + expect(idx007).toBeGreaterThan(idx006); + }); +}); diff --git a/packages/vesper-core/src/storage/store.ts b/packages/vesper-core/src/storage/store.ts index 686d339..686b23b 100644 --- a/packages/vesper-core/src/storage/store.ts +++ b/packages/vesper-core/src/storage/store.ts @@ -6,11 +6,18 @@ import { MIGRATIONS } from "./migrations.ts"; import type { AppendEventInput, AppendRunEventInput, + AppendTurnInput, + ChatSessionRow, + ChatTurnRole, + ChatTurnRow, + CreateSessionInput, EventRow, FinishRunInput, ListEventsOptions, ListRunEventsOptions, ListRunsOptions, + ListTurnsOptions, + PipelineTemplateRow, RecordRunInput, RunEventKind, RunEventRow, @@ -20,6 +27,7 @@ import type { Store, TaskGrant, UpsertTaskGrantInput, + UpsertTemplateInput, } from "./types.ts"; /** @@ -68,6 +76,31 @@ interface RawTaskGrantRow { granted_by: unknown; } +/** Raw shape returned for the `chat_sessions` table. */ +interface RawChatSessionRow { + id: unknown; + ts: unknown; + title: unknown; +} + +/** Raw shape returned for the `chat_turns` table. */ +interface RawChatTurnRow { + id: unknown; + session_id: unknown; + ts: unknown; + role: unknown; + text: unknown; + run_id: unknown; +} + +/** Raw shape returned for the `pipeline_templates` table. */ +interface RawPipelineTemplateRow { + handler_id: unknown; + prompt: unknown; + default_params: unknown; + updated_at: unknown; +} + function assertString(value: unknown, column: string): string { if (typeof value !== "string") { throw new StorageError( @@ -192,6 +225,43 @@ function toTaskGrant(raw: RawTaskGrantRow): TaskGrant { }; } +function toChatSessionRow(raw: RawChatSessionRow): ChatSessionRow { + return { + id: assertString(raw.id, "id"), + ts: assertNumber(raw.ts, "ts"), + title: assertString(raw.title, "title"), + }; +} + +/** Narrow a `chat_turns.role` column to the allowlisted union (corruption guard). */ +function assertChatTurnRole(value: unknown, column: string): ChatTurnRole { + const str = assertString(value, column); + if (str !== "user" && str !== "assistant") { + throw new StorageError("query_failed", `unrecognised chat turn role "${str}"`); + } + return str; +} + +function toChatTurnRow(raw: RawChatTurnRow): ChatTurnRow { + return { + id: assertString(raw.id, "id"), + sessionId: assertString(raw.session_id, "session_id"), + ts: assertNumber(raw.ts, "ts"), + role: assertChatTurnRole(raw.role, "role"), + text: assertString(raw.text, "text"), + runId: assertStringOrNull(raw.run_id, "run_id"), + }; +} + +function toPipelineTemplateRow(raw: RawPipelineTemplateRow): PipelineTemplateRow { + return { + handlerId: assertString(raw.handler_id, "handler_id"), + prompt: assertString(raw.prompt, "prompt"), + defaultParams: parsePayload(raw.default_params, "default_params"), + updatedAt: assertNumber(raw.updated_at, "updated_at"), + }; +} + /** {@link Store} backed by a `bun:sqlite` database. */ export class SqliteStore implements Store { readonly #db: Database; @@ -518,6 +588,118 @@ export class SqliteStore implements Store { } } + // ------------------------------------------------------------------------- + // Chat home (migration 007_chat_home) + // ------------------------------------------------------------------------- + + createSession(input: CreateSessionInput): string { + const id = input.id ?? crypto.randomUUID(); + const ts = Date.now(); + try { + this.#db + .query( + "INSERT INTO chat_sessions (id, ts, title) VALUES (?, ?, ?)", + ) + .run(id, ts, input.title); + } catch (cause) { + throw new StorageError("query_failed", "failed to create chat session", { cause }); + } + return id; + } + + appendTurn(input: AppendTurnInput): string { + const id = crypto.randomUUID(); + const ts = Date.now(); + const runId = input.runId ?? null; + try { + this.#db + .query( + "INSERT INTO chat_turns (id, session_id, ts, role, text, run_id) VALUES (?, ?, ?, ?, ?, ?)", + ) + .run(id, input.sessionId, ts, input.role, input.text, runId); + } catch (cause) { + throw new StorageError("query_failed", "failed to append chat turn", { cause }); + } + return id; + } + + listSessions(): ChatSessionRow[] { + try { + // `rowid DESC` breaks ts ties by insertion order so two sessions created in + // the same millisecond still sort newest-first deterministically. + const rows = this.#db + .query( + "SELECT id, ts, title FROM chat_sessions ORDER BY ts DESC, rowid DESC", + ) + .all(); + return rows.map(toChatSessionRow); + } catch (cause) { + if (cause instanceof StorageError) throw cause; + throw new StorageError("query_failed", "failed to list chat sessions", { cause }); + } + } + + listTurns(options: ListTurnsOptions): ChatTurnRow[] { + try { + const conditions: string[] = ["session_id = ?"]; + const params: (string | number)[] = [options.sessionId]; + + if (options.afterTs !== undefined) { + conditions.push("ts > ?"); + params.push(options.afterTs); + } + + const where = ` WHERE ${conditions.join(" AND ")}`; + const limitClause = options.limit !== undefined ? " LIMIT ?" : ""; + if (options.limit !== undefined) { + params.push(options.limit); + } + + // `rowid ASC` breaks ts ties by insertion order so turns appended in the same + // millisecond still read back oldest-first (user before assistant). + const sql = `SELECT id, session_id, ts, role, text, run_id FROM chat_turns${where} ORDER BY ts ASC, rowid ASC${limitClause}`; + const rows = this.#db.query(sql).all(...params); + return rows.map(toChatTurnRow); + } catch (cause) { + if (cause instanceof StorageError) throw cause; + throw new StorageError("query_failed", "failed to list chat turns", { cause }); + } + } + + getTemplate(handlerId: string): PipelineTemplateRow | null { + try { + const row = this.#db + .query( + `SELECT handler_id, prompt, default_params, updated_at + FROM pipeline_templates WHERE handler_id = ?`, + ) + .get(handlerId); + return row !== null ? toPipelineTemplateRow(row) : null; + } catch (cause) { + if (cause instanceof StorageError) throw cause; + throw new StorageError("query_failed", "failed to read pipeline template", { cause }); + } + } + + upsertTemplate(input: UpsertTemplateInput): void { + const updatedAt = Date.now(); + const defaultParamsJson = JSON.stringify(input.defaultParams); + try { + this.#db + .query( + `INSERT INTO pipeline_templates (handler_id, prompt, default_params, updated_at) + VALUES (?, ?, ?, ?) + ON CONFLICT(handler_id) DO UPDATE SET + prompt = excluded.prompt, + default_params = excluded.default_params, + updated_at = excluded.updated_at`, + ) + .run(input.handlerId, input.prompt, defaultParamsJson, updatedAt); + } catch (cause) { + throw new StorageError("query_failed", "failed to upsert pipeline template", { cause }); + } + } + close(): void { this.#db.close(); } diff --git a/packages/vesper-core/src/storage/types.ts b/packages/vesper-core/src/storage/types.ts index d90230f..f5fec41 100644 --- a/packages/vesper-core/src/storage/types.ts +++ b/packages/vesper-core/src/storage/types.ts @@ -128,6 +128,80 @@ export interface UpsertTaskGrantInput { readonly granted_at?: number; } +// --------------------------------------------------------------------------- +// Chat home (migration 007_chat_home) +// --------------------------------------------------------------------------- + +/** A row from the `chat_sessions` table — one conversation thread. */ +export interface ChatSessionRow { + readonly id: string; + /** Unix timestamp in milliseconds the session was created. */ + readonly ts: number; + readonly title: string; +} + +/** The role of a {@link ChatTurnRow}. */ +export type ChatTurnRole = "user" | "assistant"; + +/** + * A row from the `chat_turns` table — a single transcript bubble. An assistant + * turn carries the `runId` of the router run that produced it, so the same row + * renders both as a transcript bubble and as the root of the live activity tree. + */ +export interface ChatTurnRow { + readonly id: string; + readonly sessionId: string; + /** Unix timestamp in milliseconds the turn was appended. */ + readonly ts: number; + readonly role: ChatTurnRole; + readonly text: string; + /** The `runs` row id this assistant turn started, or null (user turns). */ + readonly runId: string | null; +} + +/** A row from the `pipeline_templates` table — a pipeline's editable prompt + params. */ +export interface PipelineTemplateRow { + readonly handlerId: string; + readonly prompt: string; + /** The deserialized default-params object the router merges into spawn params. */ + readonly defaultParams: Record; + /** Unix timestamp in milliseconds the template was last written. */ + readonly updatedAt: number; +} + +/** Input for {@link Store.createSession}. `id`/`ts` are generated when omitted. */ +export interface CreateSessionInput { + /** Pre-allocated session id (UUID); a fresh one is generated when omitted. */ + readonly id?: string; + readonly title: string; +} + +/** Input for {@link Store.appendTurn}. */ +export interface AppendTurnInput { + readonly sessionId: string; + readonly role: ChatTurnRole; + readonly text: string; + /** The `runs` row this turn started (assistant turns); omitted/null for user turns. */ + readonly runId?: string | null; +} + +/** Filters for {@link Store.listTurns}. */ +export interface ListTurnsOptions { + readonly sessionId: string; + /** Return only turns strictly after this timestamp (ts > afterTs). */ + readonly afterTs?: number; + /** Maximum number of rows to return (default: unlimited). */ + readonly limit?: number; +} + +/** Input for {@link Store.upsertTemplate}. */ +export interface UpsertTemplateInput { + readonly handlerId: string; + readonly prompt: string; + /** Default-params object; serialized to JSON on write. */ + readonly defaultParams: Record; +} + /** Optional filters for {@link Store.listEvents}. */ export interface ListEventsOptions { /** Return only events with this source. */ @@ -233,6 +307,28 @@ export interface Store { */ getTaskGrant(handlerId: string, contentHash?: string): TaskGrant | null; + // ------------------------------------------------------------------------- + // Chat home (migration 007_chat_home) + // ------------------------------------------------------------------------- + + /** Create a chat session and return its generated (or supplied) id. */ + createSession(input: CreateSessionInput): string; + + /** Append a transcript turn and return its generated id. */ + appendTurn(input: AppendTurnInput): string; + + /** List chat sessions newest-first (most recent activity at the top). */ + listSessions(): ChatSessionRow[]; + + /** List a session's turns oldest-first, optionally filtered by `afterTs`/`limit`. */ + listTurns(options: ListTurnsOptions): ChatTurnRow[]; + + /** Return the editable template for `handlerId`, or null if none was saved yet. */ + getTemplate(handlerId: string): PipelineTemplateRow | null; + + /** Insert or update a pipeline's editable template (prompt + default params). */ + upsertTemplate(input: UpsertTemplateInput): void; + /** * Close the underlying database connection. After this call the store must not be used. */ diff --git a/packages/vesper-ui/src/client/chat-types.ts b/packages/vesper-ui/src/client/chat-types.ts new file mode 100644 index 0000000..6f06d7f --- /dev/null +++ b/packages/vesper-ui/src/client/chat-types.ts @@ -0,0 +1,43 @@ +/** + * Client-side wire shapes for the chatbot-home + templates routes. These mirror the + * JSON the server serializes (see `server/server.ts`) — the browser bundle cannot + * import `@vesper/core` row types directly, so they are restated structurally here. + */ + +/** JSON of a `chat_sessions` row (`GET /api/chat/sessions`, newest-first). */ +export interface ChatSessionRow { + readonly id: string; + readonly ts: number; + readonly title: string; +} + +/** JSON of a `chat_turns` row (`GET /api/chat/sessions/:id/turns`). */ +export interface ChatTurnRow { + readonly id: string; + readonly sessionId: string; + readonly ts: number; + readonly role: "user" | "assistant"; + readonly text: string; + readonly runId: string | null; +} + +/** The editable-config view of a pipeline's `ScheduledTask` (`GET /api/pipelines`). */ +export interface PipelineConfig { + readonly id: string; + readonly handlerId: string; + readonly kind: string; + readonly scheduleExpr: string; + readonly enabled: boolean; + readonly maxRunsPerDay: number | null; + readonly maxConcurrent: number | null; + readonly maxDurationMs: number | null; + readonly requiredCapabilities: readonly string[]; +} + +/** Response of `GET /api/pipelines/:id/template`. */ +export interface PipelineTemplate { + readonly handlerId: string; + readonly prompt: string; + readonly defaultParams: Record; + readonly config: PipelineConfig; +} diff --git a/packages/vesper-ui/src/client/chat.ts b/packages/vesper-ui/src/client/chat.ts new file mode 100644 index 0000000..7056521 --- /dev/null +++ b/packages/vesper-ui/src/client/chat.ts @@ -0,0 +1,231 @@ +/// +import type { ChatSessionRow, ChatTurnRow } from "./chat-types.ts"; + +/** + * Dependencies the chat home borrows from {@link import("./main.ts")} — it REUSES + * the existing live socket + activity panel rather than opening a second transport. + */ +export interface ChatDeps { + /** Send a control frame on the existing `/api/live` socket (no-op when closed). */ + readonly wsSend: (payload: Record) => void; + /** Open the live activity panel for a run (the demoted canvas tree). */ + readonly openActivity: (runId: string) => void; + /** Surface a transient message via the shared toast. */ + readonly toast: (message: string) => void; +} + +/** A rendered transcript turn, keyed by its persisted turn id (for de-dupe). */ +interface RenderedTurn { + readonly id: string; + readonly role: ChatTurnRow["role"]; +} + +function el(id: string): T { + const node = document.getElementById(id); + if (node === null) throw new Error(`missing #${id}`); + return node as T; +} + +/** + * The chat home: the transcript column + input. A message is a manual run of the + * `router` pipeline through the EXISTING run path (`POST /api/chat`); the assistant + * turn carries the `runId` whose live tree the activity panel renders. The transcript + * streams over the SAME `/api/live` socket (a `chat:` topic) and backfills + * via `GET /api/chat/sessions/:id/turns` on load + reconnect. + */ +export class ChatHome { + readonly #deps: ChatDeps; + readonly #thread = el("chat-thread"); + readonly #empty = el("chat-empty"); + readonly #form = el("chat-form"); + readonly #text = el("chat-text"); + readonly #send = el("chat-send"); + + /** The active session id (null until the first message creates one). */ + #sessionId: string | null = null; + /** Highest turn ts we have rendered — the `afterTs` cursor for backfill. */ + #lastTs = 0; + /** Turn ids already in the DOM (de-dupe live frames against backfilled twins). */ + readonly #seen = new Map(); + /** True while a `POST /api/chat` is in flight (disables Send, shows a placeholder). */ + #sending = false; + + constructor(deps: ChatDeps) { + this.#deps = deps; + this.#form.addEventListener("submit", (e) => { + e.preventDefault(); + void this.#submit(); + }); + // Enter sends; Shift+Enter inserts a newline (keyboard-friendly composer). + this.#text.addEventListener("keydown", (e) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + void this.#submit(); + } + }); + this.#text.addEventListener("input", () => this.#autosize()); + } + + /** Grow the composer with its content, capped by CSS max-height. */ + #autosize(): void { + this.#text.style.height = "auto"; + this.#text.style.height = `${this.#text.scrollHeight}px`; + } + + /** Re-subscribe + re-backfill after a (re)connect, so no turn is missed. */ + onSocketOpen(): void { + if (this.#sessionId !== null) { + this.#deps.wsSend({ type: "subscribe", sessionId: this.#sessionId }); + void this.#backfill(); + } + } + + /** A `chat:turn` frame arrived on the live socket — append it if it's ours. */ + onLiveTurn(frame: { turnId?: unknown; runId?: unknown; role?: unknown; text?: unknown }): void { + if (typeof frame.turnId !== "string" || typeof frame.text !== "string") return; + const role = frame.role === "user" ? "user" : "assistant"; + const runId = typeof frame.runId === "string" ? frame.runId : null; + this.#renderTurn({ id: frame.turnId, role, text: frame.text, runId }); + } + + /** Submit the composer: POST /api/chat (the existing run path), then await frames. */ + async #submit(): Promise { + const message = this.#text.value.trim(); + if (message.length === 0 || this.#sending) return; + this.#sending = true; + this.#send.disabled = true; + this.#text.value = ""; + this.#autosize(); + // Optimistic user bubble (replaced by the persisted twin when its frame lands). + const optimisticId = `pending:${Date.now()}`; + this.#renderTurn({ id: optimisticId, role: "user", text: message, runId: null }); + const thinking = this.#renderPending(); + + try { + const res = await fetch("/api/chat", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify( + this.#sessionId === null ? { message } : { sessionId: this.#sessionId, message }, + ), + }); + const body = (await res.json()) as { + sessionId?: string; + turnId?: string; + runId?: string | null; + error?: string; + }; + thinking.remove(); + if (!res.ok || typeof body.sessionId !== "string") { + this.#deps.toast(body.error ?? "could not send"); + return; + } + // First message established a session — subscribe + adopt it. Frames for both + // turns are published server-side; the backfill is the durable safety net. + if (this.#sessionId === null) { + this.#sessionId = body.sessionId; + this.#deps.wsSend({ type: "subscribe", sessionId: body.sessionId }); + } + await this.#backfill(); + } catch { + thinking.remove(); + this.#deps.toast("could not send"); + } finally { + this.#sending = false; + this.#send.disabled = false; + this.#text.focus(); + } + } + + /** Fetch any turns after our cursor and render them (idempotent via #seen). */ + async #backfill(): Promise { + if (this.#sessionId === null) return; + try { + const url = `/api/chat/sessions/${encodeURIComponent(this.#sessionId)}/turns?afterTs=${this.#lastTs}`; + const res = await fetch(url); + if (!res.ok) return; + const turns = (await res.json()) as ChatTurnRow[]; + for (const t of turns) { + this.#renderTurn(t); + if (t.ts > this.#lastTs) this.#lastTs = t.ts; + } + } catch { + // transient; the next frame or reconnect recovers. + } + } + + /** Restore the most recent session's transcript on first load (if any exists). */ + async restoreLatest(): Promise { + try { + const res = await fetch("/api/chat/sessions"); + if (!res.ok) return; + const sessions = (await res.json()) as ChatSessionRow[]; + const latest = sessions[0]; // server returns newest-first + if (latest === undefined) return; + this.#sessionId = latest.id; + this.#deps.wsSend({ type: "subscribe", sessionId: latest.id }); + await this.#backfill(); + } catch { + // no prior sessions / transient — the empty state stays. + } + } + + /** Append a "thinking…" placeholder; returns it so the caller can remove it. */ + #renderPending(): HTMLElement { + const row = document.createElement("div"); + row.className = "bubble assistant pending"; + row.textContent = "thinking…"; + this.#thread.append(row); + this.#scrollToEnd(); + return row; + } + + /** Render (or, for the optimistic user turn, leave) one transcript bubble. */ + #renderTurn(turn: { + id: string; + role: ChatTurnRow["role"]; + text: string; + runId: string | null; + }): void { + if (this.#seen.has(turn.id)) return; // de-dupe replayed/twin frames. + // Drop the optimistic user bubble once the persisted user turn arrives. + if (turn.role === "user") this.#dropOptimistic(); + this.#empty.style.display = "none"; + + const row = document.createElement("div"); + row.className = `bubble ${turn.role}`; + row.dataset.turnId = turn.id; + const textNode = document.createElement("div"); + textNode.textContent = turn.text; + row.append(textNode); + + // An assistant turn carries the run that produced it — offer to watch it live. + if (turn.role === "assistant" && turn.runId !== null) { + const runId = turn.runId; + const watch = document.createElement("button"); + watch.type = "button"; + watch.className = "watch"; + watch.textContent = "Watch it work"; + watch.addEventListener("click", () => this.#deps.openActivity(runId)); + row.append(watch); + } + + this.#thread.append(row); + this.#seen.set(turn.id, { id: turn.id, role: turn.role }); + this.#scrollToEnd(); + } + + /** Remove the lone optimistic user bubble (id begins with `pending:`). */ + #dropOptimistic(): void { + const pending = this.#thread.querySelector('[data-turn-id^="pending:"]'); + if (pending !== null) { + const id = pending.dataset.turnId; + if (id !== undefined) this.#seen.delete(id); + pending.remove(); + } + } + + #scrollToEnd(): void { + this.#thread.scrollTop = this.#thread.scrollHeight; + } +} diff --git a/packages/vesper-ui/src/client/index.html b/packages/vesper-ui/src/client/index.html index 8829382..0c3ba69 100644 --- a/packages/vesper-ui/src/client/index.html +++ b/packages/vesper-ui/src/client/index.html @@ -244,13 +244,150 @@ .wizard .primary:hover { transform: translateY(-1px); box-shadow: 0 16px 40px rgba(124, 92, 255, 0.55); } .wizard .primary:active { transform: translateY(1px); } + /* ── Chatbot home ────────────────────────────────────────────────────── + The transcript is the HOME surface; the canvas world demotes to the side + activity panel. A view switch on shows exactly one of + chat / world / templates. The canvas (#scene) is always painted behind, + but only pointer-interactive when the world view is active. */ + body[data-view="chat"] #scene, + body[data-view="templates"] #scene { pointer-events: none; } + body[data-view="chat"] .header, + body[data-view="templates"] .header { background: linear-gradient(var(--bg) 60%, transparent); } + + /* Top navigation — switches the home surface. Glass pills, keyboard-reachable. */ + .nav { + position: fixed; top: 18px; right: 24px; z-index: 16; display: flex; gap: 8px; + pointer-events: auto; + } + .nav button { + min-height: 40px; padding: 8px 16px; border-radius: 999px; cursor: pointer; + font-family: var(--sans); font-size: 15px; font-weight: 600; color: var(--ink); + background: var(--surface); border: 1px solid var(--border); + -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur); + transition: background .12s ease, transform .12s ease; + } + .nav button:hover { background: var(--surface-strong); transform: translateY(-1px); } + .nav button[aria-current="true"] { background: var(--accent); color: var(--on-accent); border-color: transparent; } + .nav button:focus-visible { outline: 3px solid var(--accent-2); outline-offset: 2px; } + + /* Chat surface — a centered transcript column over the glass field. */ + .chat { + position: fixed; inset: 0; display: none; flex-direction: column; align-items: center; + padding: 84px 16px 0; z-index: 12; + } + body[data-view="chat"] .chat { display: flex; } + .chat-thread { + width: min(720px, 100%); flex: 1; overflow-y: auto; display: flex; flex-direction: column; + gap: 14px; padding: 8px 4px 18px; + } + .chat-empty { + margin: auto; text-align: center; color: var(--ink-soft); max-width: 460px; + } + .chat-empty .ce-mark { font-size: 34px; color: var(--accent); margin-bottom: 10px; } + .chat-empty h2 { font-family: var(--display); font-size: 28px; font-weight: 700; color: var(--ink); margin: 0 0 8px; } + .chat-empty p { font-size: 17px; line-height: 1.55; margin: 0; } + + .bubble { + max-width: 78%; padding: 13px 16px; border-radius: 18px; font-size: 17px; line-height: 1.5; + white-space: pre-wrap; word-break: break-word; border: 1px solid var(--border); + -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur); + } + .bubble.user { align-self: flex-end; background: var(--accent); color: var(--on-accent); border-color: transparent; border-bottom-right-radius: 6px; } + .bubble.assistant { align-self: flex-start; background: var(--surface-strong); color: var(--ink); border-bottom-left-radius: 6px; } + .bubble .watch { + display: inline-flex; align-items: center; gap: 6px; margin-top: 10px; padding: 7px 13px; + min-height: 36px; border-radius: 999px; border: 1px solid var(--border); cursor: pointer; + background: var(--tint); color: var(--ink); font-family: var(--sans); font-size: 14px; font-weight: 600; + } + .bubble .watch:hover { background: var(--surface); } + .bubble .watch:focus-visible { outline: 3px solid var(--accent-2); outline-offset: 2px; } + .bubble.pending { opacity: 0.7; font-style: italic; } + + .chat-input { + width: min(720px, 100%); display: flex; gap: 10px; align-items: flex-end; + padding: 14px 4px calc(18px + env(safe-area-inset-bottom)); + } + .chat-input textarea { + flex: 1; resize: none; min-height: 56px; max-height: 180px; padding: 15px 18px; + border-radius: 18px; border: 1px solid var(--border); background: var(--surface); + -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur); + color: var(--ink); font-family: var(--sans); font-size: 17px; line-height: 1.4; + } + .chat-input textarea::placeholder { color: var(--ink-soft); } + .chat-input textarea:focus-visible { outline: 3px solid var(--accent-2); outline-offset: 1px; } + .chat-input button { + flex: none; min-height: 56px; padding: 0 26px; border: none; border-radius: 18px; + font-family: var(--sans); font-size: 18px; font-weight: 700; color: var(--on-accent); + background: var(--accent); cursor: pointer; box-shadow: 0 8px 22px rgba(124, 92, 255, 0.32); + transition: background .12s ease, transform .12s ease; + } + .chat-input button:hover { background: var(--accent-2); transform: translateY(-1px); } + .chat-input button:active { transform: translateY(1px); } + .chat-input button:disabled { filter: grayscale(0.4) brightness(0.96); cursor: default; transform: none; } + .chat-input button:focus-visible { outline: 3px solid var(--accent-2); outline-offset: 2px; } + + /* When chat is the home, the activity panel docks to the right (the canvas is + behind); it keeps its own slide-in. */ + body[data-view="chat"] .activity { right: 22px; left: auto; transform: translateY(-50%) translateX(26px); } + body[data-view="chat"] .activity.open { transform: translateY(-50%) translateX(0); } + + /* ── Templates screen ────────────────────────────────────────────────── + List pipelines; edit prompt + default params + schedule/caps. Capability + edits stay OUT (code-defined). Privileged save needs an approval code. */ + .templates { + position: fixed; inset: 0; display: none; overflow-y: auto; z-index: 12; + padding: 84px 16px 40px; + } + body[data-view="templates"] .templates { display: block; } + .tpl-wrap { width: min(820px, 100%); margin: 0 auto; } + .tpl-wrap h2 { font-family: var(--display); font-size: 28px; font-weight: 700; color: var(--ink); margin: 0 0 6px; } + .tpl-wrap .tpl-sub { color: var(--ink-soft); font-size: 16px; margin: 0 0 22px; } + .tpl-list { display: flex; flex-direction: column; gap: 12px; } + .tpl-item { + border: 1px solid var(--border); border-radius: 18px; background: var(--surface); padding: 16px 18px; + -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur); + } + .tpl-item .tpl-head { display: flex; align-items: center; gap: 12px; cursor: pointer; } + .tpl-item .tpl-mark { width: 30px; height: 30px; flex: none; image-rendering: pixelated; } + .tpl-item .tpl-id { font-size: 18px; font-weight: 700; color: var(--ink); } + .tpl-item .tpl-kind { font-size: 13px; color: var(--ink-soft); margin-left: auto; } + .tpl-item .tpl-toggle { font-size: 22px; color: var(--ink-soft); flex: none; } + .tpl-body { display: none; margin-top: 16px; flex-direction: column; gap: 14px; } + .tpl-item.open .tpl-body { display: flex; } + .tpl-field label { display: block; font-size: 13px; text-transform: uppercase; letter-spacing: 1px; color: var(--ink-soft); margin-bottom: 6px; } + .tpl-field textarea, .tpl-field input { + width: 100%; padding: 11px 14px; border-radius: 12px; border: 1px solid var(--border); + background: var(--surface-strong); color: var(--ink); font-family: var(--sans); font-size: 16px; + } + .tpl-field textarea { resize: vertical; min-height: 72px; font-family: ui-monospace, monospace; font-size: 14px; } + .tpl-field textarea:focus-visible, .tpl-field input:focus-visible { outline: 3px solid var(--accent-2); outline-offset: 1px; } + .tpl-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } + .tpl-check { display: flex; align-items: center; gap: 8px; font-size: 16px; color: var(--ink); } + .tpl-check input { width: 20px; height: 20px; } + .tpl-save-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } + .tpl-save-row input[type="text"] { flex: 1; min-width: 160px; } + .tpl-save { + min-height: 48px; padding: 12px 24px; border: none; border-radius: 14px; cursor: pointer; + font-family: var(--sans); font-size: 16px; font-weight: 700; color: var(--on-accent); + background: var(--accent); box-shadow: 0 8px 22px rgba(124, 92, 255, 0.32); + } + .tpl-save:hover { background: var(--accent-2); } + .tpl-save:disabled { filter: grayscale(0.4) brightness(0.96); cursor: default; } + .tpl-save:focus-visible, .tpl-check input:focus-visible { outline: 3px solid var(--accent-2); outline-offset: 2px; } + .tpl-note { font-size: 14px; color: var(--ink-soft); margin: 4px 0 0; } + .tpl-note.err { color: var(--rose); } + .tpl-note.ok { color: var(--accent); } + .tpl-caps { font-size: 13px; color: var(--ink-soft); } + .tpl-caps code { background: var(--tint); padding: 2px 7px; border-radius: 6px; font-size: 12px; } + @media (prefers-reduced-motion: reduce) { - .card, .toast, .runbtn, .watchbtn, .activity, .wizard-scrim, .wizard .primary, .wizard .ghost, .gcard, .chip, .dot-i { transition: none; } + .card, .toast, .runbtn, .watchbtn, .activity, .wizard-scrim, .wizard .primary, .wizard .ghost, .gcard, .chip, .dot-i, + .nav button, .chat-input button, .chat-input textarea { transition: none; } .dot.working, .step.active { animation: none; } } - + + + + + +
+
+
+
+

What would you like done?

+

Type a wish below. Vesper picks the right helper, sets it to work, and you can watch it happen.

+
+
+
+ + + +
+
+ + +
+
+

Your helpers

+

Edit how each helper behaves — its instructions, default settings, schedule, and limits.

+
+
+
+
tap a helper to see what it's doing
diff --git a/packages/vesper-ui/src/client/main.ts b/packages/vesper-ui/src/client/main.ts index 4666555..b35884d 100644 --- a/packages/vesper-ui/src/client/main.ts +++ b/packages/vesper-ui/src/client/main.ts @@ -1,8 +1,10 @@ /// import type { Inhabitant, RunEventInfo, RunTreeInfo, SceneGraph } from "../world/types.ts"; import { resolveMark } from "./brand/index.ts"; +import { ChatHome } from "./chat.ts"; import type { HitRegion } from "./render.ts"; import { drawSprite, SPRITE_W, spriteFor } from "./sprite.ts"; +import { TemplatesScreen } from "./templates.ts"; import { resolveTheme } from "./theme/registry.ts"; import { pickThemeId, @@ -541,6 +543,8 @@ function connectLive(): void { // Re-open the panel after a reconnect: re-subscribes every node AND re-backfills // the steps missed during the disconnect window (not just a bare re-subscribe). if (activityRunId !== null) void openActivity(activityRunId); + // Re-subscribe + re-backfill the active chat session over the same socket. + chat.onSocketOpen(); }); ws.addEventListener("message", (ev) => { try { @@ -550,8 +554,15 @@ function connectLive(): void { kind?: string; event?: RunEventInfo; outcome?: { taskId?: string; runId?: string | null }; + turnId?: string; + role?: string; + text?: string; }; - if (msg.type === "run:completed" && msg.outcome?.taskId !== undefined) { + if (msg.type === "chat:turn") { + // A transcript turn arrived on the chat: topic — the chat home + // de-dupes against its backfilled twin, so a double-deliver is harmless. + chat.onLiveTurn(msg); + } else if (msg.type === "run:completed" && msg.outcome?.taskId !== undefined) { pops.set(msg.outcome.taskId, performance.now()); if (typeof msg.outcome.runId === "string") { latestRunByPipeline.set(msg.outcome.taskId, msg.outcome.runId); @@ -591,7 +602,33 @@ function frame(t: number): void { requestAnimationFrame(frame); } +// ── Chatbot home + Helpers (templates) ──────────────────────────────────────── +// The transcript is the HOME; the canvas world demotes to the activity panel. Both +// modules REUSE the live socket (wsSend) + the activity panel (openActivity) + the +// shared toast — no second transport. +const chat = new ChatHome({ wsSend, openActivity, toast }); +const templates = new TemplatesScreen({ toast }); + +// Top-nav view switch: chat (home) / world (canvas) / templates (Helpers). +type View = "chat" | "world" | "templates"; +const navButtons = Array.from(document.querySelectorAll(".nav button")); +function setView(view: View): void { + document.body.dataset.view = view; + for (const btn of navButtons) { + btn.setAttribute("aria-current", btn.dataset.view === view ? "true" : "false"); + } + if (view === "world") hint.style.opacity = "1"; + else hint.style.opacity = "0"; + if (view === "templates") void templates.ensureLoaded(); + if (view === "chat") el("chat-text").focus(); +} +for (const btn of navButtons) { + btn.addEventListener("click", () => setView((btn.dataset.view as View) ?? "chat")); +} + startWizard(); +setView("chat"); +void chat.restoreLatest(); void refreshWorld(); connectLive(); requestAnimationFrame(frame); diff --git a/packages/vesper-ui/src/client/templates.ts b/packages/vesper-ui/src/client/templates.ts new file mode 100644 index 0000000..0058bdc --- /dev/null +++ b/packages/vesper-ui/src/client/templates.ts @@ -0,0 +1,311 @@ +/// +import { resolveMark } from "./brand/index.ts"; +import type { PipelineConfig, PipelineTemplate } from "./chat-types.ts"; + +/** Dependencies the templates screen borrows from {@link import("./main.ts")}. */ +export interface TemplatesDeps { + /** Surface a transient message via the shared toast. */ + readonly toast: (message: string) => void; +} + +function el(id: string): T { + const node = document.getElementById(id); + if (node === null) throw new Error(`missing #${id}`); + return node as T; +} + +/** Draw a pipeline's real brand mark into a small inline canvas (mirrors main.ts). */ +function markCanvas(id: string, px: number): HTMLCanvasElement { + const c = document.createElement("canvas"); + c.width = px; + c.height = px; + const mctx = c.getContext("2d"); + if (mctx !== null) resolveMark(id).draw(mctx, px / 2, px / 2, px * 0.38); + return c; +} + +/** A nullable cap value rendered as a readable string ("unlimited" for null). */ +function capLabel(value: number | null): string { + return value === null ? "unlimited" : String(value); +} + +/** + * The Helpers screen (editable pipeline templates, spec #4). Lists every registered + * pipeline; expanding one reveals its editable prompt + default params (persisted via + * `PUT /api/pipelines/:id/template`, gated by the out-of-band approval code) plus a + * READ-ONLY view of its schedule, caps, and capabilities. Capability editing stays out + * of this UI (code-defined); the schedule/caps view is informational until the backend + * exposes a write path for them. + */ +export class TemplatesScreen { + readonly #deps: TemplatesDeps; + readonly #list = el("tpl-list"); + #loaded = false; + + constructor(deps: TemplatesDeps) { + this.#deps = deps; + } + + /** Load the pipeline list on first reveal; a no-op on subsequent opens. */ + async ensureLoaded(): Promise { + if (this.#loaded) return; + this.#loaded = true; + await this.reload(); + } + + /** (Re)fetch the pipeline list and render each as a collapsible editor. */ + async reload(): Promise { + try { + const res = await fetch("/api/pipelines"); + if (!res.ok) { + this.#deps.toast("could not load helpers"); + return; + } + const pipelines = (await res.json()) as PipelineConfig[]; + this.#list.replaceChildren(); + for (const p of pipelines) this.#list.append(this.#buildItem(p)); + } catch { + this.#deps.toast("could not load helpers"); + } + } + + /** Build one collapsible pipeline row (header toggles the editor body). */ + #buildItem(p: PipelineConfig): HTMLElement { + const item = document.createElement("div"); + item.className = "tpl-item"; + item.dataset.id = p.id; + + const head = document.createElement("div"); + head.className = "tpl-head"; + head.setAttribute("role", "button"); + head.setAttribute("tabindex", "0"); + head.setAttribute("aria-expanded", "false"); + const mark = markCanvas(p.id, 30); + mark.className = "tpl-mark"; + const id = document.createElement("span"); + id.className = "tpl-id"; + id.textContent = p.id; + const kind = document.createElement("span"); + kind.className = "tpl-kind"; + kind.textContent = `${p.kind}${p.enabled ? "" : " · disabled"}`; + const toggle = document.createElement("span"); + toggle.className = "tpl-toggle"; + toggle.textContent = "+"; + toggle.setAttribute("aria-hidden", "true"); + head.append(mark, id, kind, toggle); + + const body = document.createElement("div"); + body.className = "tpl-body"; + + let built = false; + const expand = (): void => { + const open = item.classList.toggle("open"); + head.setAttribute("aria-expanded", String(open)); + toggle.textContent = open ? "−" : "+"; + if (open && !built) { + built = true; + void this.#buildEditor(p, body); + } + }; + head.addEventListener("click", expand); + head.addEventListener("keydown", (e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + expand(); + } + }); + + item.append(head, body); + return item; + } + + /** Fetch the template + render the prompt/params editor and read-only config view. */ + async #buildEditor(p: PipelineConfig, body: HTMLElement): Promise { + let tpl: PipelineTemplate; + try { + const res = await fetch(`/api/pipelines/${encodeURIComponent(p.id)}/template`); + if (!res.ok) { + body.append(this.#note("could not load this helper's template", "err")); + return; + } + tpl = (await res.json()) as PipelineTemplate; + } catch { + body.append(this.#note("could not load this helper's template", "err")); + return; + } + + const promptField = this.#textField("Instructions (prompt)", tpl.prompt, 4); + const paramsField = this.#textField( + "Default settings (JSON)", + JSON.stringify(tpl.defaultParams, null, 2), + 5, + ); + + // Read-only config view (schedule, caps, capabilities) — informational. + const cfg = document.createElement("div"); + cfg.className = "tpl-caps"; + const sched = document.createElement("div"); + sched.append( + this.#kv("Schedule", tpl.config.scheduleExpr || "manual only"), + this.#kv("Enabled", tpl.config.enabled ? "yes" : "no"), + this.#kv("Runs/day", capLabel(tpl.config.maxRunsPerDay)), + this.#kv("Max concurrent", capLabel(tpl.config.maxConcurrent)), + this.#kv("Max duration (ms)", capLabel(tpl.config.maxDurationMs)), + ); + cfg.append(sched); + const caps = document.createElement("div"); + caps.style.marginTop = "8px"; + caps.append(document.createTextNode("Capabilities (code-defined): ")); + for (const c of tpl.config.requiredCapabilities) { + const code = document.createElement("code"); + code.textContent = c; + caps.append(code, document.createTextNode(" ")); + } + if (tpl.config.requiredCapabilities.length === 0) { + caps.append(document.createTextNode("none")); + } + cfg.append(caps); + + // Approval code + Save row. The PUT is privileged: it needs a single-use code + // minted out-of-band by the daemon (it is never served to the page). + const codeWrap = document.createElement("div"); + codeWrap.className = "tpl-field"; + const codeLabel = document.createElement("label"); + const codeId = `tpl-code-${p.id}`; + codeLabel.setAttribute("for", codeId); + codeLabel.textContent = "Approval code (from the Vesper daemon)"; + const codeInput = document.createElement("input"); + codeInput.type = "text"; + codeInput.id = codeId; + codeInput.autocomplete = "off"; + codeInput.placeholder = "paste the one-time code"; + codeWrap.append(codeLabel, codeInput); + + const saveRow = document.createElement("div"); + saveRow.className = "tpl-save-row"; + const save = document.createElement("button"); + save.type = "button"; + save.className = "tpl-save"; + save.textContent = "Save instructions"; + const note = this.#note("", ""); + saveRow.append(save, note); + + save.addEventListener("click", () => { + void this.#save(p.id, { + prompt: promptField.textarea.value, + paramsRaw: paramsField.textarea.value, + code: codeInput.value.trim(), + save, + note, + codeInput, + }); + }); + + body.append( + promptField.field, + paramsField.field, + cfg, + codeWrap, + saveRow, + this.#note( + "Schedule, limits, and capabilities are shown for reference; editing them is not yet available here.", + "", + ), + ); + } + + /** Validate + PUT the prompt/params; surface the result inline. */ + async #save( + pipelineId: string, + args: { + prompt: string; + paramsRaw: string; + code: string; + save: HTMLButtonElement; + note: HTMLElement; + codeInput: HTMLInputElement; + }, + ): Promise { + const { prompt, paramsRaw, code, save, note, codeInput } = args; + if (code.length === 0) { + this.#setNote(note, "an approval code is required to save", "err"); + codeInput.focus(); + return; + } + let defaultParams: Record; + try { + const parsed: unknown = paramsRaw.trim().length === 0 ? {} : JSON.parse(paramsRaw); + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + throw new Error("not an object"); + } + defaultParams = parsed as Record; + } catch { + this.#setNote(note, "default settings must be a JSON object", "err"); + return; + } + + save.disabled = true; + this.#setNote(note, "saving…", ""); + try { + const res = await fetch(`/api/pipelines/${encodeURIComponent(pipelineId)}/template`, { + method: "PUT", + headers: { "content-type": "application/json", "x-vesper-approval": code }, + body: JSON.stringify({ prompt, defaultParams }), + }); + if (res.ok) { + this.#setNote(note, "saved", "ok"); + codeInput.value = ""; // the code is single-use; force a fresh one next time. + this.#deps.toast(`${pipelineId} updated`); + } else { + const body = (await res.json().catch(() => ({}))) as { error?: string }; + this.#setNote(note, body.error ?? "save failed", "err"); + } + } catch { + this.#setNote(note, "save failed", "err"); + } finally { + save.disabled = false; + } + } + + /** A labelled textarea field; returns the wrapper + the textarea for reading. */ + #textField( + label: string, + value: string, + rows: number, + ): { field: HTMLElement; textarea: HTMLTextAreaElement } { + const field = document.createElement("div"); + field.className = "tpl-field"; + const lbl = document.createElement("label"); + const taId = `tpl-${label.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}-${Math.random().toString(36).slice(2, 7)}`; + lbl.setAttribute("for", taId); + lbl.textContent = label; + const textarea = document.createElement("textarea"); + textarea.id = taId; + textarea.rows = rows; + textarea.value = value; + field.append(lbl, textarea); + return { field, textarea }; + } + + /** A small key/value line for the read-only config view. */ + #kv(key: string, value: string): HTMLElement { + const line = document.createElement("div"); + const k = document.createElement("strong"); + k.textContent = `${key}: `; + line.append(k, document.createTextNode(value)); + return line; + } + + #note(text: string, kind: "" | "ok" | "err"): HTMLElement { + const note = document.createElement("p"); + note.className = `tpl-note${kind ? ` ${kind}` : ""}`; + note.textContent = text; + note.setAttribute("role", "status"); + return note; + } + + #setNote(note: HTMLElement, text: string, kind: "" | "ok" | "err"): void { + note.className = `tpl-note${kind ? ` ${kind}` : ""}`; + note.textContent = text; + } +} diff --git a/packages/vesper-ui/src/server/server.test.ts b/packages/vesper-ui/src/server/server.test.ts index 423ed48..ac82048 100644 --- a/packages/vesper-ui/src/server/server.test.ts +++ b/packages/vesper-ui/src/server/server.test.ts @@ -4,6 +4,7 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { + ApprovalTokenStore, CAPABILITIES, type CompleteFn, HandlerRegistry, @@ -300,3 +301,295 @@ describe("UI server", () => { ws.close(); }); }); + +// ─────────────────────────────────────────────────────────────────────────── +// Chatbot home + editable pipeline templates (chatbot-home spec) +// ─────────────────────────────────────────────────────────────────────────── + +describe("UI server — chat + templates", () => { + let cDir: string; + let cDb: Database; + let cStore: Store; + let cHandle: UiServerHandle; + let tokens: ApprovalTokenStore; + + beforeEach(async () => { + cDir = mkdtempSync(join(tmpdir(), "vesper-ui-chat-")); + const path = join(cDir, "vesper.db"); + openStore(path).close(); + cDb = new Database(path); + cStore = openStore(path); + + const registry = new HandlerRegistry(); + // A spawn-only child the router dispatches to. + registry.register("child", async (ctx) => { + ctx.emitProgress({ kind: "step", message: "working" }); + ctx.recordRun({ status: "ok", summary: "child summary" }); + }); + // A minimal router that classifies via complete() then spawns the child. + registry.register("router", async (ctx) => { + await ctx.complete("classify"); + const handle = ctx.spawn({ + handlerId: "child", + label: "child", + params: {}, + capabilities: ["WRITE_STORAGE"], + }); + const outcome = await handle.done.catch(() => null); + ctx.recordRun({ + status: outcome?.status === "ok" ? "ok" : "partial", + summary: "routed to child", + }); + }); + const scheduler = new Scheduler({ + db: cDb, + registry, + grants: CAPABILITIES, + complete: fakeComplete, + }); + scheduler.register({ + id: "router", + kind: "manual", + schedule_expr: "", + handler_id: "router", + required_capabilities: ["CLI_INVOKE", "WRITE_STORAGE", "SPAWN_SUBAGENT"], + }); + + tokens = new ApprovalTokenStore(); + cHandle = await startUiServer({ + scheduler, + store: cStore, + seed: "chat-seed", + port: 0, + approvalTokens: tokens, + }); + }); + + afterEach(() => { + cHandle.stop(); + cStore.close(); + cDb.close(); + rmSync(cDir, { recursive: true, force: true }); + }); + + const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + + test("POST /api/chat runs the router and returns sessionId/turnId/runId", async () => { + const res = await fetch(`${cHandle.url}/api/chat`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ message: "run a self test" }), + }); + expect(res.status).toBe(200); + const body = (await res.json()) as { sessionId: string; turnId: string; runId: string }; + expect(UUID_RE.test(body.sessionId)).toBe(true); + expect(body.runId).not.toBeNull(); + + // The transcript has the user turn + an assistant turn carrying the runId. + const turnsRes = await fetch( + `${cHandle.url}/api/chat/sessions/${body.sessionId}/turns?afterTs=0`, + ); + const turns = (await turnsRes.json()) as { + role: string; + text: string; + runId: string | null; + }[]; + expect(turns.map((t) => t.role)).toEqual(["user", "assistant"]); + expect(turns[1]?.runId).toBe(body.runId); + + // A chat audit event was written. + const sessions = (await (await fetch(`${cHandle.url}/api/chat/sessions`)).json()) as { + id: string; + }[]; + expect(sessions.map((s) => s.id)).toContain(body.sessionId); + }); + + test("POST /api/chat continues an existing session when sessionId is supplied", async () => { + const first = (await ( + await fetch(`${cHandle.url}/api/chat`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ message: "one" }), + }) + ).json()) as { sessionId: string }; + + await fetch(`${cHandle.url}/api/chat`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ message: "two", sessionId: first.sessionId }), + }); + + const turns = (await ( + await fetch(`${cHandle.url}/api/chat/sessions/${first.sessionId}/turns`) + ).json()) as unknown[]; + // 2 messages x (user + assistant) = 4 turns in ONE session. + expect(turns).toHaveLength(4); + const sessions = (await (await fetch(`${cHandle.url}/api/chat/sessions`)).json()) as unknown[]; + expect(sessions).toHaveLength(1); + }); + + test("POST /api/chat rejects an empty message (400)", async () => { + const res = await fetch(`${cHandle.url}/api/chat`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ message: " " }), + }); + expect(res.status).toBe(400); + }); + + test("POST /api/chat rejects a non-UUID sessionId (400)", async () => { + const res = await fetch(`${cHandle.url}/api/chat`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ message: "hi", sessionId: "not-a-uuid" }), + }); + expect(res.status).toBe(400); + }); + + test("POST /api/chat is rejected cross-origin (403, CSRF guard)", async () => { + const res = await fetch(`${cHandle.url}/api/chat`, { + method: "POST", + headers: { "content-type": "application/json", origin: "http://evil.example.com" }, + body: JSON.stringify({ message: "hi" }), + }); + expect(res.status).toBe(403); + }); + + test("GET /api/chat/sessions/:id/turns rejects a non-UUID id (400)", async () => { + const res = await fetch(`${cHandle.url}/api/chat/sessions/not-a-uuid/turns`); + expect(res.status).toBe(400); + }); + + test("a chat turn is pushed live to a chat: subscriber", async () => { + // Create the session first so we have an id to subscribe to. + const first = (await ( + await fetch(`${cHandle.url}/api/chat`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ message: "seed" }), + }) + ).json()) as { sessionId: string }; + + const ws = new WebSocket(`${cHandle.url.replace("http", "ws")}/api/live`); + await new Promise((resolve) => + ws.addEventListener("open", () => resolve(), { once: true }), + ); + ws.send(JSON.stringify({ type: "subscribe", sessionId: first.sessionId })); + // Give the subscribe control frame a tick to register. + await new Promise((r) => setTimeout(r, 20)); + + const got = new Promise<{ role: string; text: string }>((resolve) => { + ws.addEventListener("message", (e) => { + const m = JSON.parse(String(e.data)) as { type: string; role?: string; text?: string }; + if (m.type === "chat:turn" && m.role === "user" && m.text === "second message") { + resolve({ role: m.role, text: m.text }); + } + }); + }); + + await fetch(`${cHandle.url}/api/chat`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ message: "second message", sessionId: first.sessionId }), + }); + + const frame = await got; + expect(frame.role).toBe("user"); + ws.close(); + }); + + test("GET /api/pipelines lists registered tasks with their config", async () => { + const res = await fetch(`${cHandle.url}/api/pipelines`); + expect(res.status).toBe(200); + const pipelines = (await res.json()) as { id: string; handlerId: string }[]; + expect(pipelines.map((p) => p.id)).toContain("router"); + }); + + test("GET /api/pipelines/:id/template returns prompt + params + config", async () => { + const res = await fetch(`${cHandle.url}/api/pipelines/router/template`); + expect(res.status).toBe(200); + const body = (await res.json()) as { + handlerId: string; + prompt: string; + defaultParams: Record; + config: { id: string }; + }; + expect(body.handlerId).toBe("router"); + expect(body.prompt).toBe(""); + expect(body.config.id).toBe("router"); + }); + + test("GET template for an unknown pipeline is a 404", async () => { + const res = await fetch(`${cHandle.url}/api/pipelines/ghost/template`); + expect(res.status).toBe(404); + }); + + test("PUT template without an approval code is 401", async () => { + const res = await fetch(`${cHandle.url}/api/pipelines/router/template`, { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "new", defaultParams: {} }), + }); + expect(res.status).toBe(401); + }); + + test("PUT template with an invalid approval code is 403", async () => { + const res = await fetch(`${cHandle.url}/api/pipelines/router/template`, { + method: "PUT", + headers: { "content-type": "application/json", "x-vesper-approval": "deadbeef" }, + body: JSON.stringify({ prompt: "new", defaultParams: {} }), + }); + expect(res.status).toBe(403); + }); + + test("PUT template with a valid single-use code persists prompt + params and audits", async () => { + const code = tokens.mint(); + const res = await fetch(`${cHandle.url}/api/pipelines/router/template`, { + method: "PUT", + headers: { "content-type": "application/json", "x-vesper-approval": code }, + body: JSON.stringify({ prompt: "classify strictly", defaultParams: { tone: "warm" } }), + }); + expect(res.status).toBe(200); + + // The template is now persisted and read back via GET. + const got = (await (await fetch(`${cHandle.url}/api/pipelines/router/template`)).json()) as { + prompt: string; + defaultParams: Record; + }; + expect(got.prompt).toBe("classify strictly"); + expect(got.defaultParams).toEqual({ tone: "warm" }); + + // The code is single-use — a second PUT with the same code is 403. + const replay = await fetch(`${cHandle.url}/api/pipelines/router/template`, { + method: "PUT", + headers: { "content-type": "application/json", "x-vesper-approval": code }, + body: JSON.stringify({ prompt: "x", defaultParams: {} }), + }); + expect(replay.status).toBe(403); + }); + + test("PUT template is rejected cross-origin BEFORE the token is checked (403)", async () => { + const code = tokens.mint(); + const res = await fetch(`${cHandle.url}/api/pipelines/router/template`, { + method: "PUT", + headers: { + "content-type": "application/json", + origin: "http://evil.example.com", + "x-vesper-approval": code, + }, + body: JSON.stringify({ prompt: "x", defaultParams: {} }), + }); + expect(res.status).toBe(403); + // The local-origin guard fired first, so the code was NOT consumed. + expect(tokens.isValid(code)).toBe(true); + }); + + test("POST /api/approval/request mints a code (production mint path) without leaking it", async () => { + const res = await fetch(`${cHandle.url}/api/approval/request`, { method: "POST" }); + expect(res.status).toBe(200); + const body = (await res.json()) as Record; + // Returns ok; the code is surfaced OUT-OF-BAND on the daemon TTY, never in the body. + expect(body).toEqual({ ok: true }); + expect(JSON.stringify(body)).not.toMatch(/[0-9a-f]{6,}/); + }); +}); diff --git a/packages/vesper-ui/src/server/server.ts b/packages/vesper-ui/src/server/server.ts index 69e9bdb..16ee3fe 100644 --- a/packages/vesper-ui/src/server/server.ts +++ b/packages/vesper-ui/src/server/server.ts @@ -1,7 +1,7 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import type { RunOutcome, RunTreeNode, Scheduler, Store } from "@vesper/core"; -import { RUN_COMPLETED, RUN_EVENT, SchedulerError } from "@vesper/core"; +import type { ApprovalTokenStore, RunOutcome, RunTreeNode, Scheduler, Store } from "@vesper/core"; +import { ApprovalError, RUN_COMPLETED, RUN_EVENT, SchedulerError } from "@vesper/core"; import { ModuleRegistry } from "../modules/registry.ts"; import type { UiModule } from "../modules/types.ts"; import type { PresenceInfo, RunEventInfo, RunTreeInfo } from "../world/types.ts"; @@ -26,6 +26,13 @@ export interface UiServerDeps { readonly presencePollMs?: number; /** Default Vesper World theme id, stamped into the page for the client to read. */ readonly defaultTheme?: string; + /** + * Out-of-band approval-token store. Privileged config mutations + * (`PUT /api/pipelines/:id/template`) require a valid single-use code from this + * store IN ADDITION to {@link isLocalRequest}. When omitted, those mutations are + * refused (403) — fail-closed; the chatbot/template surface is then read-only. + */ + readonly approvalTokens?: ApprovalTokenStore; } /** A running UI server. */ @@ -81,6 +88,96 @@ function mapTreeToInfo(node: RunTreeNode): RunTreeInfo { }; } +/** A chat-turn frame published to the `chat:` WS topic. */ +interface ChatTurnFrame { + readonly turnId: string; + readonly runId: string | null; + readonly role: "user" | "assistant"; + readonly text: string; +} + +/** Publish a chat turn to its session's topic so live transcript views update. */ +function publishChatTurn( + server: { publish(topic: string, data: string): unknown }, + sessionId: string, + frame: ChatTurnFrame, +): void { + server.publish(`chat:${sessionId}`, JSON.stringify({ type: "chat:turn", ...frame })); +} + +/** The editable-config view of a `ScheduledTask` returned by the template routes. */ +interface PipelineConfig { + readonly id: string; + readonly handlerId: string; + readonly kind: string; + readonly scheduleExpr: string; + readonly enabled: boolean; + readonly maxRunsPerDay: number | null; + readonly maxConcurrent: number | null; + readonly maxDurationMs: number | null; + readonly requiredCapabilities: readonly string[]; +} + +/** Map a core `ScheduledTask` to the thin {@link PipelineConfig} view (no secrets). */ +function toPipelineConfig(task: { + id: string; + handler_id: string; + kind: string; + schedule_expr: string; + enabled: boolean; + max_runs_per_day: number | null; + max_concurrent: number | null; + max_duration_ms: number | null; + required_capabilities: readonly string[]; +}): PipelineConfig { + return { + id: task.id, + handlerId: task.handler_id, + kind: task.kind, + scheduleExpr: task.schedule_expr, + enabled: task.enabled, + maxRunsPerDay: task.max_runs_per_day, + maxConcurrent: task.max_concurrent, + maxDurationMs: task.max_duration_ms, + requiredCapabilities: [...task.required_capabilities], + }; +} + +/** Parse a request's JSON body, returning a record or null (malformed/non-object). */ +async function readJsonBody(req: Request): Promise | null> { + try { + const parsed: unknown = await req.json(); + return isRecord(parsed) ? parsed : null; + } catch { + return null; + } +} + +/** + * Gate a privileged mutation behind a single-use approval code. Returns null when the + * caller is authorised; otherwise a 403/401 `Response`. The code is read from the + * `x-vesper-approval` header (out-of-band — minted by the daemon, never in the page). + * When no token store is configured the route is fail-closed (403). + */ +function requireApproval(req: Request, tokens: ApprovalTokenStore | undefined): Response | null { + if (tokens === undefined) { + return json({ error: "approval is not configured (mutation refused)" }, 403); + } + const code = req.headers.get("x-vesper-approval"); + if (code === null || code.length === 0) { + return json({ error: "approval code required" }, 401); + } + try { + tokens.verify(code); + return null; + } catch (err) { + if (err instanceof ApprovalError) { + return json({ error: `approval ${err.reason}` }, 403); + } + return json({ error: "approval failed" }, 403); + } +} + /** Extract the host (no port) from a `Host` header value or an `Origin` URL. */ function hostOf(value: string | null): string | null { if (value === null || value.length === 0) return null; @@ -242,6 +339,144 @@ export async function startUiServer(deps: UiServerDeps): Promise } } + // ── Chatbot home ───────────────────────────────────────────────────── + // POST /api/chat — a chat message is a manual run of the `router` pipeline + // through the EXISTING run path (no new execution). Persists the user turn, + // runs the router, persists the assistant turn carrying its runId, audits the + // mutation, and publishes both turns to the session's chat: WS topic. + if (req.method === "POST" && pathname === "/api/chat") { + const body = await readJsonBody(req); + const message = typeof body?.message === "string" ? body.message : ""; + if (message.trim().length === 0) { + return json({ error: "message is required" }, 400); + } + const requested = typeof body?.sessionId === "string" ? body.sessionId : null; + if (requested !== null && !UUID_RE.test(requested)) { + return json({ error: "invalid sessionId" }, 400); + } + + // Create the session lazily; a brand-new session is titled from the message. + const sessionId = requested ?? store.createSession({ title: message.slice(0, 80) }); + const userTurnId = store.appendTurn({ sessionId, role: "user", text: message }); + publishChatTurn(server, sessionId, { + turnId: userTurnId, + runId: null, + role: "user", + text: message, + }); + + let outcome: RunOutcome; + try { + outcome = await scheduler.run("router", { params: { message, sessionId } }); + } catch (err) { + if (err instanceof SchedulerError && err.reason === "unknown_task") { + return json({ error: "router pipeline is not registered" }, 500); + } + return json({ error: err instanceof Error ? err.message : String(err) }, 500); + } + + const assistantText = outcome.summary ?? "(no response)"; + const turnId = store.appendTurn({ + sessionId, + role: "assistant", + text: assistantText, + runId: outcome.runId, + }); + publishChatTurn(server, sessionId, { + turnId, + runId: outcome.runId, + role: "assistant", + text: assistantText, + }); + store.appendEvent({ + source: "chat", + kind: "message", + payload: { sessionId, turnId, runId: outcome.runId }, + }); + return json({ sessionId, turnId, runId: outcome.runId }); + } + + // GET /api/chat/sessions — the session list (newest-first) for the home. + if (req.method === "GET" && pathname === "/api/chat/sessions") { + return json(store.listSessions()); + } + + // GET /api/chat/sessions/:id/turns?afterTs= — replay/backfill a transcript. + const turnsMatch = pathname.match(/^\/api\/chat\/sessions\/([^/]+)\/turns$/); + if (req.method === "GET" && turnsMatch) { + const sessionId = decodeURIComponent(turnsMatch[1] ?? ""); + if (!UUID_RE.test(sessionId)) return json({ error: "invalid sessionId" }, 400); + const afterRaw = url.searchParams.get("afterTs"); + const afterTs = afterRaw === null ? undefined : Number(afterRaw); + const turns = store.listTurns({ + sessionId, + ...(afterTs !== undefined && Number.isFinite(afterTs) ? { afterTs } : {}), + limit: 500, + }); + return json(turns); + } + + // ── Editable pipeline templates ────────────────────────────────────── + // GET /api/pipelines — registered tasks + their editable ScheduledTask config. + if (req.method === "GET" && pathname === "/api/pipelines") { + return json(scheduler.list().map(toPipelineConfig)); + } + + // GET /api/pipelines/:id/template — the editable prompt + default params + config. + const templateMatch = pathname.match(/^\/api\/pipelines\/([^/]+)\/template$/); + if (req.method === "GET" && templateMatch) { + const id = decodeURIComponent(templateMatch[1] ?? ""); + const task = scheduler.list().find((t) => t.id === id); + if (task === undefined) return json({ error: `unknown pipeline "${id}"` }, 404); + const template = store.getTemplate(task.handler_id); + return json({ + handlerId: task.handler_id, + prompt: template?.prompt ?? "", + defaultParams: template?.defaultParams ?? {}, + config: toPipelineConfig(task), + }); + } + + // PUT /api/pipelines/:id/template — the PRIVILEGED config mutation. Behind + // isLocalRequest (above) AND a single-use out-of-band approval code. A rejected + // edit is a row upsert, never a destructive file op (Hard rule 4). + if (req.method === "PUT" && templateMatch) { + const id = decodeURIComponent(templateMatch[1] ?? ""); + const tokenError = requireApproval(req, deps.approvalTokens); + if (tokenError !== null) return tokenError; + + const task = scheduler.list().find((t) => t.id === id); + if (task === undefined) return json({ error: `unknown pipeline "${id}"` }, 404); + + const body = await readJsonBody(req); + if (body === null) return json({ error: "invalid JSON body" }, 400); + const prompt = typeof body.prompt === "string" ? body.prompt : ""; + const defaultParams = isRecord(body.defaultParams) ? body.defaultParams : {}; + store.upsertTemplate({ handlerId: task.handler_id, prompt, defaultParams }); + store.appendEvent({ + source: "templates", + kind: "updated", + payload: { pipelineId: id, handlerId: task.handler_id }, + }); + return json({ ok: true, handlerId: task.handler_id }); + } + + // POST /api/approval/request — mint a single-use approval code and surface it + // OUT-OF-BAND on the daemon's own stdout (the operator's `vesper daemon` terminal). + // The HTTP response NEVER carries the code, so a malicious local app can trigger a + // mint but cannot READ it — only the operator at the foreground terminal sees it. + // The operator pastes it into the privileged-mutation form (`x-vesper-approval`). + if (req.method === "POST" && pathname === "/api/approval/request") { + if (deps.approvalTokens === undefined) { + return json({ error: "approval is not configured" }, 403); + } + const code = deps.approvalTokens.mint(); + process.stdout.write( + `\n Vesper approval code: ${code} (single-use, expires shortly)\n\n`, + ); + return json({ ok: true }); + } + return new Response("not found", { status: 404 }); }, websocket: { @@ -249,17 +484,23 @@ export async function startUiServer(deps: UiServerDeps): Promise ws.subscribe("world"); }, message(ws, raw) { - // Control protocol: {type:'subscribe'|'unsubscribe', runId}. A client follows - // one run's live trace by subscribing to its agent: topic. The runId is - // guarded by UUID shape (rejects crafted/wildcard topics); malformed frames are - // ignored silently — the connection is already local-origin (upgrade is guarded). + // Control protocol, two topic families on ONE socket: + // {type:'subscribe'|'unsubscribe', runId} -> a run's live trace (agent:) + // {type:'subscribe'|'unsubscribe', sessionId} -> a chat transcript (chat:) + // Each id is guarded by UUID shape (rejects crafted/wildcard topics); malformed + // frames are ignored silently — the connection is already local-origin (upgrade + // is guarded). try { const msg: unknown = JSON.parse(typeof raw === "string" ? raw : raw.toString()); if (!isRecord(msg)) return; - const { type, runId } = msg; - if (typeof runId !== "string" || !UUID_RE.test(runId)) return; - if (type === "subscribe") ws.subscribe(`agent:${runId}`); - else if (type === "unsubscribe") ws.unsubscribe(`agent:${runId}`); + const { type, runId, sessionId } = msg; + if (typeof runId === "string" && UUID_RE.test(runId)) { + if (type === "subscribe") ws.subscribe(`agent:${runId}`); + else if (type === "unsubscribe") ws.unsubscribe(`agent:${runId}`); + } else if (typeof sessionId === "string" && UUID_RE.test(sessionId)) { + if (type === "subscribe") ws.subscribe(`chat:${sessionId}`); + else if (type === "unsubscribe") ws.unsubscribe(`chat:${sessionId}`); + } } catch { // Non-JSON / unexpected payload — ignore. } From 39eaf6577f7bd3d1d16c03a2e191168cf4c41405 Mon Sep 17 00:00:00 2001 From: ogarciarevett Date: Mon, 1 Jun 2026 21:19:46 +0200 Subject: [PATCH 2/6] feat(desktop): native Tauri shell over Bun daemon sidecar (DEV-112 slices 1-2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add packages/vesper-desktop — a thin Tauri 2 native window over the existing Bun-hosted Vesper World UI. The host runtime stays Bun; the Rust core holds no business logic. This is the consumer surface (double-click app); the vesper CLI remains the developer surface. Slice 1 — scaffold the Tauri app: window config + entrypoint, generated icons, Rust build verified. Slice 2 — sidecar auto-start: - vesper-ui: startUiServer gains an optional clientAssets dep + a process-wide setEmbeddedClientAssets() fallback + an exported buildClientAssets(). The on-disk read + Bun.build stays as the fallback, so `vesper daemon run` from source is unchanged. - vesper-cli/compiled-entry.ts: embeds the prebuilt client (index.html + app.js) via an import attribute so a `bun build --compile` daemon serves the UI without client source files or a runtime bundler. - scripts/build-daemon.ts (root build:daemon): builds the client assets, then compiles the daemon to src-tauri/binaries/vesper-daemon-. - Rust core (tauri-plugin-shell): spawns the sidecar, health-waits on 127.0.0.1:4317, opens the window onto it, and stops the sidecar on exit. attach-if-already-running is free via the daemon's single-instance guard. Verified: the compiled daemon serves the full UI headlessly (GET / 200, /app.js, /api/world); cargo build clean; vesper-ui + vesper-cli suites green; biome clean on the changed files. Adds the Rust/cargo toolchain to the repo, scoped to the desktop package; the Hard-rule-14 contract amendment lands with a later slice. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 8 + bun.lock | 34 + package.json | 4 +- packages/vesper-cli/src/compiled-entry.ts | 23 + packages/vesper-cli/src/text-imports.d.ts | 8 + packages/vesper-desktop/README.md | 48 + packages/vesper-desktop/package.json | 15 + packages/vesper-desktop/src-tauri/Cargo.lock | 4787 +++++++++++++++++ packages/vesper-desktop/src-tauri/Cargo.toml | 28 + packages/vesper-desktop/src-tauri/build.rs | 3 + .../src-tauri/capabilities/default.json | 7 + .../src-tauri/icons/128x128.png | Bin 0 -> 300 bytes .../src-tauri/icons/128x128@2x.png | Bin 0 -> 666 bytes .../vesper-desktop/src-tauri/icons/32x32.png | Bin 0 -> 106 bytes .../vesper-desktop/src-tauri/icons/64x64.png | Bin 0 -> 161 bytes .../src-tauri/icons/Square107x107Logo.png | Bin 0 -> 240 bytes .../src-tauri/icons/Square142x142Logo.png | Bin 0 -> 329 bytes .../src-tauri/icons/Square150x150Logo.png | Bin 0 -> 348 bytes .../src-tauri/icons/Square284x284Logo.png | Bin 0 -> 763 bytes .../src-tauri/icons/Square30x30Logo.png | Bin 0 -> 105 bytes .../src-tauri/icons/Square310x310Logo.png | Bin 0 -> 853 bytes .../src-tauri/icons/Square44x44Logo.png | Bin 0 -> 122 bytes .../src-tauri/icons/Square71x71Logo.png | Bin 0 -> 173 bytes .../src-tauri/icons/Square89x89Logo.png | Bin 0 -> 200 bytes .../src-tauri/icons/StoreLogo.png | Bin 0 -> 131 bytes .../vesper-desktop/src-tauri/icons/icon.icns | Bin 0 -> 12673 bytes .../vesper-desktop/src-tauri/icons/icon.ico | Bin 0 -> 1983 bytes .../vesper-desktop/src-tauri/icons/icon.png | Bin 0 -> 1817 bytes packages/vesper-desktop/src-tauri/src/main.rs | 87 + .../vesper-desktop/src-tauri/tauri.conf.json | 27 + packages/vesper-desktop/src/index.html | 46 + packages/vesper-ui/src/index.ts | 9 +- packages/vesper-ui/src/server/server.test.ts | 32 + packages/vesper-ui/src/server/server.ts | 58 +- scripts/build-daemon.ts | 44 + 35 files changed, 5262 insertions(+), 6 deletions(-) create mode 100644 packages/vesper-cli/src/compiled-entry.ts create mode 100644 packages/vesper-cli/src/text-imports.d.ts create mode 100644 packages/vesper-desktop/README.md create mode 100644 packages/vesper-desktop/package.json create mode 100644 packages/vesper-desktop/src-tauri/Cargo.lock create mode 100644 packages/vesper-desktop/src-tauri/Cargo.toml create mode 100644 packages/vesper-desktop/src-tauri/build.rs create mode 100644 packages/vesper-desktop/src-tauri/capabilities/default.json create mode 100644 packages/vesper-desktop/src-tauri/icons/128x128.png create mode 100644 packages/vesper-desktop/src-tauri/icons/128x128@2x.png create mode 100644 packages/vesper-desktop/src-tauri/icons/32x32.png create mode 100644 packages/vesper-desktop/src-tauri/icons/64x64.png create mode 100644 packages/vesper-desktop/src-tauri/icons/Square107x107Logo.png create mode 100644 packages/vesper-desktop/src-tauri/icons/Square142x142Logo.png create mode 100644 packages/vesper-desktop/src-tauri/icons/Square150x150Logo.png create mode 100644 packages/vesper-desktop/src-tauri/icons/Square284x284Logo.png create mode 100644 packages/vesper-desktop/src-tauri/icons/Square30x30Logo.png create mode 100644 packages/vesper-desktop/src-tauri/icons/Square310x310Logo.png create mode 100644 packages/vesper-desktop/src-tauri/icons/Square44x44Logo.png create mode 100644 packages/vesper-desktop/src-tauri/icons/Square71x71Logo.png create mode 100644 packages/vesper-desktop/src-tauri/icons/Square89x89Logo.png create mode 100644 packages/vesper-desktop/src-tauri/icons/StoreLogo.png create mode 100644 packages/vesper-desktop/src-tauri/icons/icon.icns create mode 100644 packages/vesper-desktop/src-tauri/icons/icon.ico create mode 100644 packages/vesper-desktop/src-tauri/icons/icon.png create mode 100644 packages/vesper-desktop/src-tauri/src/main.rs create mode 100644 packages/vesper-desktop/src-tauri/tauri.conf.json create mode 100644 packages/vesper-desktop/src/index.html create mode 100644 scripts/build-daemon.ts diff --git a/.gitignore b/.gitignore index 32b5585..d589979 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,14 @@ node_modules/ dist/ *.tsbuildinfo +# Rust / Tauri build artifacts (packages/vesper-desktop — the native shell, DEV-112). +# Cargo.lock IS committed (this is an application binary — reproducible builds). +target/ +/packages/vesper-desktop/src-tauri/gen/ +# generated daemon-embed assets + the compiled sidecar binary (DEV-112 Slice 2) +/packages/vesper-cli/src/generated/ +/packages/vesper-desktop/src-tauri/binaries/ + # Vesper local runtime directory .vesper/ diff --git a/bun.lock b/bun.lock index ff5a773..e7a1efb 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "devDependencies": { "@biomejs/biome": "^2.4.15", "@types/bun": "^1.3.14", + "@vesper/ui": "workspace:*", }, }, "packages/pipelines": { @@ -32,6 +33,13 @@ "name": "@vesper/core", "version": "0.1.0", }, + "packages/vesper-desktop": { + "name": "@vesper/desktop", + "version": "0.1.0", + "devDependencies": { + "@tauri-apps/cli": "^2.11.2", + }, + }, "packages/vesper-ui": { "name": "@vesper/ui", "version": "0.1.0", @@ -59,6 +67,30 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.15", "", { "os": "win32", "cpu": "x64" }, "sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ=="], + "@tauri-apps/cli": ["@tauri-apps/cli@2.11.2", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.11.2", "@tauri-apps/cli-darwin-x64": "2.11.2", "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2", "@tauri-apps/cli-linux-arm64-gnu": "2.11.2", "@tauri-apps/cli-linux-arm64-musl": "2.11.2", "@tauri-apps/cli-linux-riscv64-gnu": "2.11.2", "@tauri-apps/cli-linux-x64-gnu": "2.11.2", "@tauri-apps/cli-linux-x64-musl": "2.11.2", "@tauri-apps/cli-win32-arm64-msvc": "2.11.2", "@tauri-apps/cli-win32-ia32-msvc": "2.11.2", "@tauri-apps/cli-win32-x64-msvc": "2.11.2" }, "bin": { "tauri": "tauri.js" } }, "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw=="], + + "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.11.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w=="], + + "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.11.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg=="], + + "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.11.2", "", { "os": "linux", "cpu": "arm" }, "sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA=="], + + "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.11.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw=="], + + "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.11.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw=="], + + "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.11.2", "", { "os": "linux", "cpu": "none" }, "sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ=="], + + "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.11.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw=="], + + "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.11.2", "", { "os": "linux", "cpu": "x64" }, "sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw=="], + + "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.11.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA=="], + + "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.11.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA=="], + + "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.11.2", "", { "os": "win32", "cpu": "x64" }, "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], @@ -67,6 +99,8 @@ "@vesper/core": ["@vesper/core@workspace:packages/vesper-core"], + "@vesper/desktop": ["@vesper/desktop@workspace:packages/vesper-desktop"], + "@vesper/pipelines": ["@vesper/pipelines@workspace:packages/pipelines"], "@vesper/ui": ["@vesper/ui@workspace:packages/vesper-ui"], diff --git a/package.json b/package.json index e717e3a..b9dfbf1 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "vesper": "bun packages/vesper-cli/src/index.ts", "sync:ai": "bun scripts/sync-ai-docs.ts", "docs:cli": "bun scripts/gen-cli-docs.ts", + "build:daemon": "bun scripts/build-daemon.ts", "prepare": "git config core.hooksPath .githooks || true" }, "engines": { @@ -21,6 +22,7 @@ }, "devDependencies": { "@biomejs/biome": "^2.4.15", - "@types/bun": "^1.3.14" + "@types/bun": "^1.3.14", + "@vesper/ui": "workspace:*" } } diff --git a/packages/vesper-cli/src/compiled-entry.ts b/packages/vesper-cli/src/compiled-entry.ts new file mode 100644 index 0000000..6cd1865 --- /dev/null +++ b/packages/vesper-cli/src/compiled-entry.ts @@ -0,0 +1,23 @@ +#!/usr/bin/env bun +// Compiled daemon entrypoint for the desktop sidecar (DEV-112 Slice 2). +// +// `bun build --compile` bundles this into a single self-contained `vesper-daemon` +// binary that the Tauri shell spawns. Unlike `vesper daemon run` from source, the +// compiled binary has no `client/` directory and no runtime bundler (its FS is the +// virtual `/$bunfs`), so the UI server cannot build its assets on the fly. We instead +// embed the prebuilt client assets at COMPILE time via `with { type: "text" }` (inlined +// as string constants) and install them before the daemon starts. +// +// The `.txt` files are generated by `scripts/build-daemon.ts` immediately before the +// compile step; they do not exist in a normal source checkout (see text-imports.d.ts). +import { setEmbeddedClientAssets } from "@vesper/ui"; +import { registry } from "./commands/index.ts"; +import { dispatch } from "./dispatch.ts"; +import appJs from "./generated/app-js.txt" with { type: "text" }; +import indexHtml from "./generated/index-html.txt" with { type: "text" }; + +setEmbeddedClientAssets({ indexHtml, appJs }); + +// Default to `daemon run`; still forward any explicit argv the shell passes through. +const args = process.argv.slice(2); +process.exit(await dispatch(registry, args.length > 0 ? args : ["daemon", "run"])); diff --git a/packages/vesper-cli/src/text-imports.d.ts b/packages/vesper-cli/src/text-imports.d.ts new file mode 100644 index 0000000..8e1b401 --- /dev/null +++ b/packages/vesper-cli/src/text-imports.d.ts @@ -0,0 +1,8 @@ +// Ambient declaration for `import x from "....txt" with { type: "text" }`. +// The desktop build (scripts/build-daemon.ts) generates the referenced `.txt` files +// just before `bun build --compile`; this keeps the compiled-entry type-clean in a +// plain source checkout where those generated files are absent. +declare module "*.txt" { + const content: string; + export default content; +} diff --git a/packages/vesper-desktop/README.md b/packages/vesper-desktop/README.md new file mode 100644 index 0000000..d94f0dc --- /dev/null +++ b/packages/vesper-desktop/README.md @@ -0,0 +1,48 @@ +# @vesper/desktop + +Native desktop shell for Vesper — a deliberately thin [Tauri 2](https://tauri.app) window +whose WebView loads the Bun daemon's "Vesper World" UI. The host runtime stays Bun; this +package adds no business logic. See `specs/tauri-migration.md` and Linear DEV-112. + +## Why this exists + +For the elder-first Desktop target, "open a browser to `localhost:4317`" is a UX wall. +This shell makes Vesper a double-click native app while reusing the exact same web UI the +daemon already serves (`@vesper/ui`). No Rust in the host — only in this shell. + +## Slice 1 (current) + +A native window pointed at `http://127.0.0.1:4317`. The daemon is started manually for now; +auto-starting it as a bundled sidecar is Slice 2. + +### Run it + +```sh +# 1. Toolchain (one-time): Rust + the Tauri CLI. +# rustup is the supported installer; the Tauri CLI is a devDependency here. + +# 2. Start the Bun daemon (hosts Vesper World on 127.0.0.1:4317): +bun run vesper daemon start # from the repo root + +# 3. Launch the native shell (from this package): +bun run dev # = tauri dev +``` + +A native window opens showing Vesper World — click a creature, inspect, Run; live updates +arrive over the same WebSocket the browser uses. No browser involved. + +### Build a bundle + +```sh +bun run build # = tauri build -> .app / .dmg (macOS) +``` + +## Layout + +- `src-tauri/` — the Rust core (window config + entrypoint). Thin by design. +- `src/index.html` — Slice 1 fallback page (becomes the Slice 2 boot splash). + +## Not yet (later slices) + +Sidecar auto-start + health-wait + attach-to-running-daemon (Slice 2); tray / menu / +notifications (Slice 3); signed installers + auto-updater + CI (Slice 4). diff --git a/packages/vesper-desktop/package.json b/packages/vesper-desktop/package.json new file mode 100644 index 0000000..7d90729 --- /dev/null +++ b/packages/vesper-desktop/package.json @@ -0,0 +1,15 @@ +{ + "name": "@vesper/desktop", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Native desktop shell for Vesper — a thin Tauri 2 window over the Bun-hosted Vesper World. DEV-112 Slice 1.", + "scripts": { + "dev": "tauri dev", + "build": "tauri build", + "tauri": "tauri" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.11.2" + } +} diff --git a/packages/vesper-desktop/src-tauri/Cargo.lock b/packages/vesper-desktop/src-tauri/Cargo.lock new file mode 100644 index 0000000..0793af5 --- /dev/null +++ b/packages/vesper-desktop/src-tauri/Cargo.lock @@ -0,0 +1,4787 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8119e4516436f5708bbc474a9d395bf12f1b5395e93a92a56e647ac3388c8610" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.11.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash 0.2.0", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.11.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.11.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a2e3dff89cd322c66647942668faee0a2b1f88ea6cbb4d374b4a8d7e92528c" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.11.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.11.1", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437404997acf375d85f1177afa7e11bb971f274ed6a7b83a2a3e339015f4cc28" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa1f9055fc23919a54e4e125052bed16ed04aef0487086e758fe01a67b451c7" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a0319528a025a38c4078e7dae2c446f4e63620ddb0659a643ede1cb38f90e9" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.117", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae6cb4e3896c21d2f6da5b31251d2faea0153bba56ed0e970f918115dbee4924" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e126abc9e84e35cdfd01596140a73a1850cdb0df0a23acf0185776c30b469a6e" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48222d7116c8807eaa6fe2f372e023fae125084e61e6eca6d70b7961cdf129ef" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092379df9a707631978e6c56b1bc2401d387f01e2d4a3c123360d167bbb9aa95" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vesper-desktop" +version = "0.1.0" +dependencies = [ + "tauri", + "tauri-build", + "tauri-plugin-shell", +] + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.14.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/packages/vesper-desktop/src-tauri/Cargo.toml b/packages/vesper-desktop/src-tauri/Cargo.toml new file mode 100644 index 0000000..7a72093 --- /dev/null +++ b/packages/vesper-desktop/src-tauri/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "vesper-desktop" +version = "0.1.0" +edition = "2021" +description = "Native desktop shell for Vesper (thin Tauri window over the Bun daemon)." +authors = ["ogarciarevett"] +# Pin a recent-enough toolchain; CI provisions Rust for the desktop package only. +rust-version = "1.77" + +# The compiled shell binary. The Bun host stays Bun — see specs/tauri-migration.md. +[[bin]] +name = "vesper-desktop" +path = "src/main.rs" + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +tauri-plugin-shell = "2" + +# Smaller, faster release binaries for distribution. +[profile.release] +codegen-units = 1 +lto = true +opt-level = "s" +panic = "abort" +strip = true diff --git a/packages/vesper-desktop/src-tauri/build.rs b/packages/vesper-desktop/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/packages/vesper-desktop/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/packages/vesper-desktop/src-tauri/capabilities/default.json b/packages/vesper-desktop/src-tauri/capabilities/default.json new file mode 100644 index 0000000..cbdce08 --- /dev/null +++ b/packages/vesper-desktop/src-tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Core permissions for the main window. The window loads the local Vesper World over HTTP and uses no Tauri IPC in Slice 1, so only core defaults are granted.", + "windows": ["main"], + "permissions": ["core:default"] +} diff --git a/packages/vesper-desktop/src-tauri/icons/128x128.png b/packages/vesper-desktop/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..1c1de898ff39c7916a5710ad3419737e2856062e GIT binary patch literal 300 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSpFLe1Ln>~)z2wNr;K0Ffu=>WM z15V%0FE(XKSJILGxmrHGm4)p9g9HO}0s~J2qXEN!a+p0RcEyIn49Po~TN*nWuQ33D Mr>mdKI;Vst00+T6!TFVdQ&MBb@0L2<1od5s; literal 0 HcmV?d00001 diff --git a/packages/vesper-desktop/src-tauri/icons/64x64.png b/packages/vesper-desktop/src-tauri/icons/64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..e8d22399243e8b129baad5dfa66f17ea99389563 GIT binary patch literal 161 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=8J;eVAr-gYUNGckFyLS?;ES1X zM&RGqhW?9Q8T(#!9yaO|R&Z!wU}R$95TJlzAHn>Vsbz@EfqV^qH=)qlO*d59^ZiPd`p3-`1HPBEQ>LOKymsu3Glyf-9pPD=oC4+6Q= gSHOB2kG0o$75^|B7+3$+0D6JJ)78&qol`;+0Kel*B>(^b literal 0 HcmV?d00001 diff --git a/packages/vesper-desktop/src-tauri/icons/Square142x142Logo.png b/packages/vesper-desktop/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..90ebdc716bff5b643d4131a83ec2882c9e6f237c GIT binary patch literal 329 zcmeAS@N?(olHy`uVBq!ia0vp^eIU%i1|*;VHQ-=iU=;9laSW-r_4bk>FM|Wefdldd zA357TtYFM|OG!$H0s z8BL0RUNbWH^pp$P$^Tp}FT>4gAa?9fM1oFF>&62S65Pp+8-W5$$%#5WtUz%IZlJh< k7*IS>hmH#A=9V(%h-=Ju+$}HO28J4gr>mdKI;Vst0Bh5Cq5uE@ literal 0 HcmV?d00001 diff --git a/packages/vesper-desktop/src-tauri/icons/Square284x284Logo.png b/packages/vesper-desktop/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..98edde4434844778ffaa22b6bc2a11feb050bd46 GIT binary patch literal 763 zcmeAS@N?(olHy`uVBq!ia0y~yV3Yx24mO~O=ojt>3=B+%JY5_^DsH{KV#v$jz;WP! ze8ES~wh!ysO*RGm@RE bq?vuUv3`k~wxS1^s2Mz6{an^LB{Ts5$cEbH literal 0 HcmV?d00001 diff --git a/packages/vesper-desktop/src-tauri/icons/Square30x30Logo.png b/packages/vesper-desktop/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..bf6213f402c59f26dbbd60c1335987d476f91c9b GIT binary patch literal 105 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{32A(dCAr-gYo;BoUFyLS?;ES1X zM&RGqhL*WDYa5@fo@X(k&_zeA`)Ja}2oP1z{Zg3mkFRs13{WqFr>mdKI;Vst0D#9I Am;e9( literal 0 HcmV?d00001 diff --git a/packages/vesper-desktop/src-tauri/icons/Square310x310Logo.png b/packages/vesper-desktop/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..93874be138ea532a00501d19f42dae21f5fd6b9b GIT binary patch literal 853 zcmeAS@N?(olHy`uVBq!ia0y~yU^D|^4mP03>plBcGB7a9c)B=-RNQ)d#gLc5fP>*6 z-;RtX#XqkZg?*m*9pab&xmx~!q9S8L4#P1vDABNyQ9>FFS$kcwMxFBtMN7;rFbklhi{ zr1Q_DP literal 0 HcmV?d00001 diff --git a/packages/vesper-desktop/src-tauri/icons/Square89x89Logo.png b/packages/vesper-desktop/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f35bf6a0c793b4d6aee2131998f94891edcac3be GIT binary patch literal 200 zcmeAS@N?(olHy`uVBq!ia0vp^ks!>$1|&ndJJtiKeoq(2kcwMxFBtMN7;rFbklhi{ zr1BTy^8&yE3F?hQAxvX(_L9jDMAu2Sx`~It{1YGTE$2+B+Q2%4A&OYB@=aD#2y@Li+nL7 z62g~m4_%;)UVM@%A%Zfyh)76Cy+|M?X&0B;{+U_xxI50w$sJtSbB5hLbN0;lpY#3S zfBth8*6V5V0XRFo9>-n)uAu8^<)P%H%}D@|TwYdMg`d3mvL@jB(EhrY`0=g0w79zM z!}Pmi8@R0S^z8U2q2&9>?Ltd^?(U3V53=hoSP}$C2eTXOJm-Kly)l=qdKCRb;2ALc zJ1=#P0haQEm8B0$+~>R=rxR~m6*$Ym_8f^bUwiu;-Z(>?y)I4zuz+Lzo{61zIBcrv@=^L2h%5x8slCQG1sVGtiCE#~3vJq3#{YJ{x1LxvC5jFxK3_bJ40q59!~C7-aMqh#nI3an-(c{wsY~-6*}K==C^? zm_k<{P*HT$Vu;5b%!4L*4>s2}x&V+c8QO!5;YQkJ6@={OS(~^AJJR2>{pPW)9rI?WwAnzw z2l0^Pz{3*mfo9uK ztrG%cXw4Z~F+n=;!4Q!B3P=jlZbBh+4ALKsMC|x8mHIjw&K4cIesBshSfneGC5tND zU&^gUc~P=%6u9E7Bf^`ou8#|uAqsG1O4V8o0w<=buc|i2aI*9v(#Il`2w)*R;ec&o zOAKfvTmr-(m8(W+gn|ot0aAb~&swe3AaEL@rM_xo40kgPc=)rLc8mh2Aeu=;kt|Ku z5gm;3Vyq+s0eS!njk*;%RbIsjA~mZtOiO(=5s3~o;51ysiW^y^E2@%EfM%6R1jIt9 kUy;()2fFMDgTQILXwWLc55sV8nm=c9xIrXz@lOPQ0c)bd-~a#s literal 0 HcmV?d00001 diff --git a/packages/vesper-desktop/src-tauri/icons/icon.ico b/packages/vesper-desktop/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1e300464beac08368140353638a9f34890fd1ed7 GIT binary patch literal 1983 zcmZQzU}RupP*7k10tJSLKr#)81q2}c)j-Ofk%2)%0>VEIq&@-p1_ltmG?2ZMiGjhv z0m2Ui@~<$1*dU<5FbgQB%F4jd8Q|y6%O%AH6y){va0voS0u^wu0a?XWRbPPA3Qrfu zkP61P2bdSAL{2;Q@qhWd-V~uhj;+nZeW5&t;ucLK7Abf`U*0 z>_Pd@{}X|nA__fNZ>s%A35EoQ22hzx6$bT8g1czcNa}?E^-N-z%@|V5+A?wW=rQq7s!H<)Z;Q&wvP?rG%LjxlN0}lg30#JaNfkA?SfE0>); + +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_shell::init()) + .manage(Sidecar(Mutex::new(None))) + .setup(|app| { + // 1. Spawn the compiled daemon sidecar (runs `vesper-daemon daemon run`). + let (mut rx, child) = app + .shell() + .sidecar("vesper-daemon")? + .args(["daemon", "run"]) + .spawn()?; + app.state::().0.lock().unwrap().replace(child); + + // Drain the sidecar's output channel so its stdio pipe never fills and blocks. + tauri::async_runtime::spawn(async move { while rx.recv().await.is_some() {} }); + + // 2 + 3. Wait for the UI port, then open the window onto it (on the main thread). + let handle = app.handle().clone(); + std::thread::spawn(move || { + let addr: SocketAddr = UI_ADDR.parse().expect("valid UI socket address"); + let deadline = Instant::now() + HEALTH_TIMEOUT; + while Instant::now() < deadline { + if TcpStream::connect_timeout(&addr, HEALTH_POLL).is_ok() { + break; + } + std::thread::sleep(HEALTH_POLL); + } + let window_handle = handle.clone(); + let _ = handle.run_on_main_thread(move || { + let url = format!("http://{UI_ADDR}"); + let _ = WebviewWindowBuilder::new( + &window_handle, + "main", + WebviewUrl::External(url.parse().expect("valid UI url")), + ) + .title("Vesper") + .inner_size(1180.0, 820.0) + .min_inner_size(880.0, 600.0) + .build(); + }); + }); + + Ok(()) + }) + .build(tauri::generate_context!()) + .expect("error while building the Vesper desktop shell") + .run(|app_handle, event| { + // 4. Stop our sidecar on exit. Never touches an attached (CLI-owned) daemon. + if let tauri::RunEvent::Exit = event { + if let Some(child) = app_handle.state::().0.lock().unwrap().take() { + let _ = child.kill(); + } + } + }); +} diff --git a/packages/vesper-desktop/src-tauri/tauri.conf.json b/packages/vesper-desktop/src-tauri/tauri.conf.json new file mode 100644 index 0000000..9b7e1c0 --- /dev/null +++ b/packages/vesper-desktop/src-tauri/tauri.conf.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Vesper", + "version": "0.1.0", + "identifier": "com.ogarciarevett.vesper.desktop", + "build": { + "frontendDist": "../src" + }, + "app": { + "windows": [], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "externalBin": ["binaries/vesper-daemon"], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/packages/vesper-desktop/src/index.html b/packages/vesper-desktop/src/index.html new file mode 100644 index 0000000..a18b41b --- /dev/null +++ b/packages/vesper-desktop/src/index.html @@ -0,0 +1,46 @@ + + + + + + Vesper + + + + +
+
Vesper
+
+ Waiting for the local runtime. Start the daemon, then reopen: +
vesper daemon start +
+
+ + diff --git a/packages/vesper-ui/src/index.ts b/packages/vesper-ui/src/index.ts index b722974..0e9be2b 100644 --- a/packages/vesper-ui/src/index.ts +++ b/packages/vesper-ui/src/index.ts @@ -8,7 +8,14 @@ export { presenceDetectorFor, presenceSignature, } from "./server/presence.ts"; -export { startUiServer, type UiServerDeps, type UiServerHandle } from "./server/server.ts"; +export { + buildClientAssets, + type ClientAssets, + setEmbeddedClientAssets, + startUiServer, + type UiServerDeps, + type UiServerHandle, +} from "./server/server.ts"; export { buildSnapshot } from "./server/snapshot.ts"; export { buildWorld } from "./world/build.ts"; export { fnv1a, seededUnit } from "./world/hash.ts"; diff --git a/packages/vesper-ui/src/server/server.test.ts b/packages/vesper-ui/src/server/server.test.ts index ac82048..4d6d825 100644 --- a/packages/vesper-ui/src/server/server.test.ts +++ b/packages/vesper-ui/src/server/server.test.ts @@ -103,6 +103,38 @@ describe("UI server", () => { expect(await res.text()).toContain("Vesper"); }); + test("serves injected clientAssets verbatim, with runtime theme stamping", async () => { + // The compiled (`bun build --compile`) daemon has no client/ dir or bundler, so it + // injects prebuilt assets. Provided assets must be served as-is — not the on-disk + // build — while per-process theme stamping still applies to the injected shell. + const scheduler = new Scheduler({ + db, + registry: new HandlerRegistry(), + grants: CAPABILITIES, + complete: fakeComplete, + }); + const injected = await startUiServer({ + scheduler, + store, + seed: "test-seed", + port: 0, + defaultTheme: "glass", + clientAssets: { + indexHtml: "INJECTED-SHELL", + appJs: "/* INJECTED-APP-JS */ globalThis.__vesper_injected = true;", + }, + }); + try { + const html = await fetch(`${injected.url}/`).then((r) => r.text()); + expect(html).toContain("INJECTED-SHELL"); + expect(html).toContain('name="vesper-theme" content="glass"'); + const js = await fetch(`${injected.url}/app.js`).then((r) => r.text()); + expect(js).toContain("INJECTED-APP-JS"); + } finally { + injected.stop(); + } + }); + test("GET /app.js serves the bundled client", async () => { const res = await fetch(`${handle.url}/app.js`); expect(res.status).toBe(200); diff --git a/packages/vesper-ui/src/server/server.ts b/packages/vesper-ui/src/server/server.ts index 16ee3fe..e486917 100644 --- a/packages/vesper-ui/src/server/server.ts +++ b/packages/vesper-ui/src/server/server.ts @@ -10,6 +10,30 @@ import { buildSnapshot } from "./snapshot.ts"; const CLIENT_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "client"); +/** + * Prebuilt browser-client assets — the `index.html` shell and the bundled `app.js`. + * Supplied when the daemon runs as a `bun build --compile` binary, where the client + * source files and the runtime bundler are unavailable (the embedded FS has no + * `client/` dir). See {@link setEmbeddedClientAssets}. + */ +export interface ClientAssets { + readonly indexHtml: string; + readonly appJs: string; +} + +/** + * Process-wide fallback client assets, set once by a compiled entrypoint before the + * daemon starts. `startUiServer` prefers an explicit `deps.clientAssets`, then this, + * then a from-disk build (the dev path) — so the compiled sidecar can embed the UI + * without threading assets through every daemon caller. + */ +let embeddedClientAssets: ClientAssets | null = null; + +/** Install process-wide {@link ClientAssets} (used by the compiled daemon sidecar). */ +export function setEmbeddedClientAssets(assets: ClientAssets): void { + embeddedClientAssets = assets; +} + /** Dependencies for {@link startUiServer}. */ export interface UiServerDeps { readonly scheduler: Scheduler; @@ -33,6 +57,13 @@ export interface UiServerDeps { * refused (403) — fail-closed; the chatbot/template surface is then read-only. */ readonly approvalTokens?: ApprovalTokenStore; + /** + * Prebuilt client assets. When set, the server serves these instead of reading + * `client/index.html` and bundling `client/main.ts` from disk — required for the + * compiled (`bun build --compile`) daemon. Falls back to {@link setEmbeddedClientAssets}, + * then to an on-disk build. + */ + readonly clientAssets?: ClientAssets; } /** A running UI server. */ @@ -221,6 +252,25 @@ async function buildClientBundle(): Promise { return await out.text(); } +/** + * Build the raw client assets from disk — the `index.html` shell and the bundled + * `app.js`, without theme stamping. Used by the dev path and by the desktop build + * step that embeds these into the compiled daemon; theme stamping happens later, + * per-process, in {@link startUiServer}. + */ +export async function buildClientAssets(): Promise { + const indexHtml = await Bun.file(join(CLIENT_DIR, "index.html")).text(); + const appJs = await buildClientBundle(); + return { indexHtml, appJs }; +} + +/** Resolve client assets: explicit dep, then process-embedded, then an on-disk build. */ +async function resolveClientAssets(deps: UiServerDeps): Promise { + if (deps.clientAssets !== undefined) return deps.clientAssets; + if (embeddedClientAssets !== null) return embeddedClientAssets; + return await buildClientAssets(); +} + /** * Start the local Vesper World UI server (HTTP + WebSocket) on `127.0.0.1`. * @@ -242,18 +292,18 @@ export async function startUiServer(deps: UiServerDeps): Promise const port = deps.port ?? 4317; const modules = new ModuleRegistry(deps.modules ?? []); - const baseHtml = await Bun.file(join(CLIENT_DIR, "index.html")).text(); + const assets = await resolveClientAssets(deps); // Stamp the configured default theme into the page (sanitized to [a-z0-9-]) so the // client can read it via . Shell templating only. const themeId = (deps.defaultTheme ?? "").replace(/[^a-z0-9-]/gi, ""); const indexHtml = themeId.length > 0 - ? baseHtml.replace( + ? assets.indexHtml.replace( "", ` \n `, ) - : baseHtml; - const appJs = await buildClientBundle(); + : assets.indexHtml; + const appJs = assets.appJs; // Live presence: the agents running on this machine right now. Detected once at // startup, then re-scanned on an interval; the latest set feeds every /api/world. diff --git a/scripts/build-daemon.ts b/scripts/build-daemon.ts new file mode 100644 index 0000000..b7e8666 --- /dev/null +++ b/scripts/build-daemon.ts @@ -0,0 +1,44 @@ +#!/usr/bin/env bun +// Build the standalone Vesper daemon binary for the Tauri desktop sidecar (DEV-112 Slice 2). +// +// Steps: +// 1. Build the raw client assets (index.html shell + bundled app.js) from @vesper/ui. +// 2. Write them where compiled-entry.ts embeds them at compile time (`with { type: "text" }`). +// 3. Resolve the Rust host target triple (Tauri's externalBin requires a `-` suffix). +// 4. `bun build --compile` the compiled entry -> src-tauri/binaries/vesper-daemon-. +// +// Run from anywhere: `bun run build:daemon` (see root package.json) or `bun scripts/build-daemon.ts`. +import { mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { buildClientAssets } from "@vesper/ui"; + +const repoRoot = join(import.meta.dir, ".."); +const genDir = join(repoRoot, "packages", "vesper-cli", "src", "generated"); +const entry = join(repoRoot, "packages", "vesper-cli", "src", "compiled-entry.ts"); +const binDir = join(repoRoot, "packages", "vesper-desktop", "src-tauri", "binaries"); + +console.log("building client assets…"); +const assets = await buildClientAssets(); +await mkdir(genDir, { recursive: true }); +await writeFile(join(genDir, "index-html.txt"), assets.indexHtml); +await writeFile(join(genDir, "app-js.txt"), assets.appJs); +console.log(` client: ${assets.indexHtml.length}B html, ${assets.appJs.length}B js`); + +// Tauri appends the build target triple to externalBin names; match it to rustc's host. +const rustc = Bun.spawnSync(["rustc", "-Vv"]); +const triple = new TextDecoder().decode(rustc.stdout).match(/host:\s*(\S+)/)?.[1]; +if (rustc.exitCode !== 0 || triple === undefined) { + throw new Error("could not resolve the Rust host target triple — is rustc installed?"); +} + +await mkdir(binDir, { recursive: true }); +const outFile = join(binDir, `vesper-daemon-${triple}`); +console.log(`compiling daemon -> ${outFile}`); +const build = Bun.spawnSync(["bun", "build", "--compile", "--outfile", outFile, entry], { + stdout: "inherit", + stderr: "inherit", +}); +if (build.exitCode !== 0) { + throw new Error("bun build --compile failed"); +} +console.log("done."); From 43c5db440afd909c637f96e1bd6b30ccdd5f5ba9 Mon Sep 17 00:00:00 2001 From: ogarciarevett Date: Wed, 3 Jun 2026 22:51:10 +0200 Subject: [PATCH 3/6] feat(ui): premium dark-glass desktop shell + native macOS window chrome Rebuild the Vesper desktop UI as an OpenClaw-style native companion and rebuild "Vesper World" from scratch. Replaces the floating-pill nav over a pixel-art canvas with a real app shell; dark glass is now the default theme (light/hearth opt-in). Shell - Custom draggable titlebar: Cmd+E command search + live status pills (/api/status). - Grouped sidebar + client-side SectionRouter; chrome-only theme system that replaces the canvas-coupled WorldTheme. - 14 sections: Chat, Runtime, Helper CLIs, Permissions, Sandbox, Settings, Diagnostics, About (live data); Pipelines/Channels/Schedule (thin views); Skills/Memory/Voice (honest stubs naming their owning specs). Vesper World rebuild - Chat scoped to "only Vesper": a transcript + a Vesper-only activity rail that follows the conversation's run tree (existing /api/chat + run-trace APIs, no backend rewrite; subscribe-before-backfill + de-dupe preserved). - Machine-wide agent presence moves off the home into Diagnostics. - Retires the pixel-art renderer (sprite/render/world/themes/brand) via controlled git rm (recoverable). Net -890 lines tracked in vesper-ui. Server - New read-only /api/status, /api/presence, /api/runs; /api/world + snapshot removed. Native - macOS overlay titlebar (TitleBarStyle::Overlay + hidden_title, cfg-gated) so the custom titlebar shows with the traffic lights inset; tray + single-instance shell. Specs: specs/desktop-app-shell.md + specs/vesper-world-rebuild.md (Omar-authorized; Linear issue-capped, recorded in cycle-log.md). biome ci clean; vesper-ui 46 / vesper-cli 104 pass; compiled sidecar verified serving the new shell. No provider SDKs. --- cycle-log.md | 36 + .../vesper-cli/src/commands/daemon-run.ts | 4 + packages/vesper-desktop/src-tauri/Cargo.lock | 433 ++++++++++++ packages/vesper-desktop/src-tauri/Cargo.toml | 3 +- packages/vesper-desktop/src-tauri/src/main.rs | 57 +- .../vesper-ui/src/client/brand/builtins.ts | 193 ----- .../src/client/brand/default-glyph.ts | 43 -- packages/vesper-ui/src/client/brand/index.ts | 6 - .../src/client/brand/registry.test.ts | 39 -- .../vesper-ui/src/client/brand/registry.ts | 35 - packages/vesper-ui/src/client/brand/types.ts | 19 - packages/vesper-ui/src/client/chat.ts | 231 ------ packages/vesper-ui/src/client/index.html | 662 ++++++------------ packages/vesper-ui/src/client/main.ts | 645 ++--------------- packages/vesper-ui/src/client/render.ts | 414 ----------- .../vesper-ui/src/client/sections/about.ts | 45 ++ .../src/client/sections/activity-rail.ts | 186 +++++ .../vesper-ui/src/client/sections/channels.ts | 71 ++ .../vesper-ui/src/client/sections/chat.ts | 288 ++++++++ packages/vesper-ui/src/client/sections/cli.ts | 59 ++ .../src/client/sections/diagnostics.ts | 107 +++ .../src/client/sections/index.test.ts | 25 + .../vesper-ui/src/client/sections/index.ts | 39 ++ .../src/client/sections/permissions.ts | 64 ++ .../src/client/sections/pipelines.ts | 109 +++ .../vesper-ui/src/client/sections/runtime.ts | 96 +++ .../vesper-ui/src/client/sections/sandbox.ts | 65 ++ .../vesper-ui/src/client/sections/schedule.ts | 46 ++ .../vesper-ui/src/client/sections/settings.ts | 101 +++ .../vesper-ui/src/client/sections/stub.ts | 39 ++ .../vesper-ui/src/client/sections/stubs.ts | 36 + packages/vesper-ui/src/client/shell/api.ts | 45 ++ .../vesper-ui/src/client/shell/contracts.ts | 61 ++ packages/vesper-ui/src/client/shell/icons.ts | 47 ++ packages/vesper-ui/src/client/shell/router.ts | 106 +++ .../vesper-ui/src/client/shell/section.ts | 104 +++ .../vesper-ui/src/client/shell/sidebar.ts | 72 ++ packages/vesper-ui/src/client/shell/themes.ts | 57 ++ .../vesper-ui/src/client/shell/titlebar.ts | 149 ++++ packages/vesper-ui/src/client/sprite.ts | 162 ----- packages/vesper-ui/src/client/templates.ts | 311 -------- .../src/client/theme/registry.test.ts | 33 - .../vesper-ui/src/client/theme/registry.ts | 34 - packages/vesper-ui/src/client/theme/types.ts | 29 - .../src/client/themes/glass/theme.ts | 389 ---------- .../src/client/themes/hearth/theme.ts | 9 - packages/vesper-ui/src/client/themes/index.ts | 6 - packages/vesper-ui/src/index.ts | 17 +- .../vesper-ui/src/modules/registry.test.ts | 38 +- packages/vesper-ui/src/modules/registry.ts | 21 +- packages/vesper-ui/src/modules/types.ts | 29 +- packages/vesper-ui/src/server/server.test.ts | 33 +- packages/vesper-ui/src/server/server.ts | 64 +- packages/vesper-ui/src/server/snapshot.ts | 30 - packages/vesper-ui/src/world/build.test.ts | 168 ----- packages/vesper-ui/src/world/build.ts | 146 ---- packages/vesper-ui/src/world/hash.ts | 15 - packages/vesper-ui/src/world/types.ts | 91 +-- 58 files changed, 2898 insertions(+), 3564 deletions(-) delete mode 100644 packages/vesper-ui/src/client/brand/builtins.ts delete mode 100644 packages/vesper-ui/src/client/brand/default-glyph.ts delete mode 100644 packages/vesper-ui/src/client/brand/index.ts delete mode 100644 packages/vesper-ui/src/client/brand/registry.test.ts delete mode 100644 packages/vesper-ui/src/client/brand/registry.ts delete mode 100644 packages/vesper-ui/src/client/brand/types.ts delete mode 100644 packages/vesper-ui/src/client/chat.ts delete mode 100644 packages/vesper-ui/src/client/render.ts create mode 100644 packages/vesper-ui/src/client/sections/about.ts create mode 100644 packages/vesper-ui/src/client/sections/activity-rail.ts create mode 100644 packages/vesper-ui/src/client/sections/channels.ts create mode 100644 packages/vesper-ui/src/client/sections/chat.ts create mode 100644 packages/vesper-ui/src/client/sections/cli.ts create mode 100644 packages/vesper-ui/src/client/sections/diagnostics.ts create mode 100644 packages/vesper-ui/src/client/sections/index.test.ts create mode 100644 packages/vesper-ui/src/client/sections/index.ts create mode 100644 packages/vesper-ui/src/client/sections/permissions.ts create mode 100644 packages/vesper-ui/src/client/sections/pipelines.ts create mode 100644 packages/vesper-ui/src/client/sections/runtime.ts create mode 100644 packages/vesper-ui/src/client/sections/sandbox.ts create mode 100644 packages/vesper-ui/src/client/sections/schedule.ts create mode 100644 packages/vesper-ui/src/client/sections/settings.ts create mode 100644 packages/vesper-ui/src/client/sections/stub.ts create mode 100644 packages/vesper-ui/src/client/sections/stubs.ts create mode 100644 packages/vesper-ui/src/client/shell/api.ts create mode 100644 packages/vesper-ui/src/client/shell/contracts.ts create mode 100644 packages/vesper-ui/src/client/shell/icons.ts create mode 100644 packages/vesper-ui/src/client/shell/router.ts create mode 100644 packages/vesper-ui/src/client/shell/section.ts create mode 100644 packages/vesper-ui/src/client/shell/sidebar.ts create mode 100644 packages/vesper-ui/src/client/shell/themes.ts create mode 100644 packages/vesper-ui/src/client/shell/titlebar.ts delete mode 100644 packages/vesper-ui/src/client/sprite.ts delete mode 100644 packages/vesper-ui/src/client/templates.ts delete mode 100644 packages/vesper-ui/src/client/theme/registry.test.ts delete mode 100644 packages/vesper-ui/src/client/theme/registry.ts delete mode 100644 packages/vesper-ui/src/client/theme/types.ts delete mode 100644 packages/vesper-ui/src/client/themes/glass/theme.ts delete mode 100644 packages/vesper-ui/src/client/themes/hearth/theme.ts delete mode 100644 packages/vesper-ui/src/client/themes/index.ts delete mode 100644 packages/vesper-ui/src/server/snapshot.ts delete mode 100644 packages/vesper-ui/src/world/build.test.ts delete mode 100644 packages/vesper-ui/src/world/build.ts delete mode 100644 packages/vesper-ui/src/world/hash.ts diff --git a/cycle-log.md b/cycle-log.md index 0275d51..efea13b 100644 --- a/cycle-log.md +++ b/cycle-log.md @@ -690,3 +690,39 @@ Backend->Client->Review workflow; the review's 2 real HIGH gaps were then fixed (007=rag) shifts to 008/009 for rag/eval (gitignored planning doc, reconciled at their build). - DEFERRED (per spec Out of Scope): the security-hardening §C token formalization; multi-session history UX; capability editing from the templates UI; token-level streaming. + +--- + +## Desktop shell redesign — premium dark-glass native companion + Vesper World rebuild — SHIPPED + +- Specs: `specs/desktop-app-shell.md` + `specs/vesper-world-rebuild.md` (Omar-authorized 2026-06-02; record + surface = specs + this log; Linear issue cap active). Reference look: OpenClaw Windows Companion. +- **Decisions locked (Omar):** premium dark-glass SUPERSEDES the elder-first *visual* framing (Hard rule 14 + amendment pending on a later sync); primary section name = **Pipelines**; presence/echo MOVES to + Diagnostics (not deleted); built shell + rebuilt Chat together as slice 1. +- **What shipped:** the `@vesper/ui` client is now an app shell — custom draggable titlebar (Cmd+E command + search, live status pills off `/api/status`), grouped sidebar, a client-side `SectionRouter`, and a + chrome-only theme system (dark default; light/hearth opt-in) that REPLACES the canvas-coupled `WorldTheme`. + 14 sections: Chat + Runtime/CLIs/Permissions/Sandbox/Settings/Diagnostics/About (live) + Pipelines/ + Channels/Schedule (thin) + Skills/Memory/Voice (honest stubs naming their specs). +- **Vesper World rebuilt:** the pixel-art canvas + machine-wide presence home are RETIRED (controlled + `git rm`, recoverable). Chat = transcript + a Vesper-ONLY activity rail (follows the conversation's run + tree via the existing `/api/chat` + run-trace APIs; subscribe-before-backfill + de-dupe preserved). No + backend rewrite — reused chat/router/sessions/turns verbatim. +- **Server:** new read-only `/api/status`, `/api/presence`, `/api/runs`; `/api/world` + `snapshot.ts` removed; + presence poll kept (feeds `/api/presence` for Diagnostics). +- **Native:** macOS overlay titlebar (`TitleBarStyle::Overlay` + `hidden_title`, cfg-gated to macOS) so the + custom HTML titlebar shows with the traffic lights inset; tray + single-instance from DEV-112 slice 3. +- **Parallel build:** lead built the backbone + Chat + real sections + server routes; 2 sub-agents built the + 6 thin views + the Rust overlay window concurrently (file-disjoint). Net **-890 lines** tracked in vesper-ui. +- **Gotcha (cost a runtime crash Omar caught):** the browser client is bundled by Bun (which does NOT error + on an undefined identifier) and sits OUTSIDE the root tsc program, so a section referenced in the barrel + but never imported (`sandboxSection`) only failed at runtime in the browser — green tests + clean bundle + missed it. FIX + GUARD: `sections/index.test.ts` imports the barrel and asserts ALL_SECTIONS (14, unique + ids, valid shape). Lesson: for the browser client, an import-the-barrel test is the real typecheck. +- Verified: `biome ci` clean (2 cosmetic warnings), vesper-ui 46 / vesper-cli 104 pass, no new tsc errors in + touched files, compiled sidecar serves the new shell end-to-end. No provider SDKs. +- DEFERRED: privileged config writes from Settings (theme is client-side; default-CLI read-only); full + template editing in Pipelines (read-only view); Windows/Linux window chrome (macOS-first per Omar); the + one `!important` (reduced-motion) biome warning; the menu-bar popover app + internal-pipelines auto-skills + feature (next requests). diff --git a/packages/vesper-cli/src/commands/daemon-run.ts b/packages/vesper-cli/src/commands/daemon-run.ts index 7d6f81d..69cbaa8 100644 --- a/packages/vesper-cli/src/commands/daemon-run.ts +++ b/packages/vesper-cli/src/commands/daemon-run.ts @@ -89,6 +89,10 @@ export const daemonRunCommand: Command = { store: uiStore, seed: machineFingerprint(), port: uiPort(), + version: "0.1.0", + socketPath: socketPath(), + defaultCli: config.cli.default ?? null, + detectClis: async () => installed.map((name) => ({ name, status: "installed", ok: true })), detectPresences: presenceDetectorFor(presenceMatchers), approvalTokens, ...(config.presence?.pollMs !== undefined ? { presencePollMs: config.presence.pollMs } : {}), diff --git a/packages/vesper-desktop/src-tauri/Cargo.lock b/packages/vesper-desktop/src-tauri/Cargo.lock index 0793af5..e41b582 100644 --- a/packages/vesper-desktop/src-tauri/Cargo.lock +++ b/packages/vesper-desktop/src-tauri/Cargo.lock @@ -47,6 +47,137 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "atk" version = "0.18.2" @@ -142,6 +273,19 @@ dependencies = [ "objc2", ] +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "brotli" version = "8.0.3" @@ -331,6 +475,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "cookie" version = "0.18.1" @@ -711,6 +864,33 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -738,6 +918,27 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -865,6 +1066,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.32" @@ -1230,6 +1444,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1716,6 +1936,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.2" @@ -2097,6 +2323,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "os_pipe" version = "1.2.3" @@ -2132,6 +2368,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2226,6 +2468,17 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.33" @@ -2271,6 +2524,20 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -2518,6 +2785,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -3226,6 +3506,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.18", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + [[package]] name = "tauri-runtime" version = "2.11.2" @@ -3326,6 +3621,19 @@ dependencies = [ "toml 1.1.2+spec-1.1.0", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "tendril" version = "0.5.0" @@ -3631,9 +3939,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "tracing-core" version = "0.1.36" @@ -3683,6 +4003,17 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + [[package]] name = "unic-char-property" version = "0.9.0" @@ -3810,6 +4141,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-shell", + "tauri-plugin-single-instance", ] [[package]] @@ -4726,6 +5058,67 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.3", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow 1.0.3", + "zvariant", +] + [[package]] name = "zerofrom" version = "0.1.8" @@ -4785,3 +5178,43 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.3", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "winnow 1.0.3", +] diff --git a/packages/vesper-desktop/src-tauri/Cargo.toml b/packages/vesper-desktop/src-tauri/Cargo.toml index 7a72093..246f070 100644 --- a/packages/vesper-desktop/src-tauri/Cargo.toml +++ b/packages/vesper-desktop/src-tauri/Cargo.toml @@ -16,8 +16,9 @@ path = "src/main.rs" tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2", features = [] } +tauri = { version = "2", features = ["tray-icon"] } tauri-plugin-shell = "2" +tauri-plugin-single-instance = "2" # Smaller, faster release binaries for distribution. [profile.release] diff --git a/packages/vesper-desktop/src-tauri/src/main.rs b/packages/vesper-desktop/src-tauri/src/main.rs index 9849221..2e33675 100644 --- a/packages/vesper-desktop/src-tauri/src/main.rs +++ b/packages/vesper-desktop/src-tauri/src/main.rs @@ -1,9 +1,11 @@ -// Vesper desktop shell — DEV-112 Slice 2. +// Vesper desktop shell — DEV-112 slices 2-3. // -// A thin native window over the Bun daemon. The Rust core does four things and holds no -// business logic: (1) spawns the compiled `vesper-daemon` sidecar (which serves Vesper -// World on 127.0.0.1:4317), (2) waits for that port to accept connections, (3) opens the -// window onto it, and (4) stops the sidecar on exit. +// A thin native window over the Bun daemon. The Rust core holds no business logic; it: +// 1. spawns the compiled `vesper-daemon` sidecar (serves Vesper World on 127.0.0.1:4317), +// 2. waits for that port to accept connections, +// 3. opens the window onto it, +// 4. stops the sidecar on exit, +// plus native chrome (slice 3): a system tray (Show/Quit) and single-instance focus. // // Attach-if-already-running is free: if a daemon is already up (e.g. `vesper daemon // start`), the sidecar's own single-instance guard makes it exit immediately, the window @@ -15,6 +17,8 @@ use std::net::{SocketAddr, TcpStream}; use std::sync::Mutex; use std::time::{Duration, Instant}; +use tauri::menu::{Menu, MenuItem}; +use tauri::tray::TrayIconBuilder; use tauri::{Manager, WebviewUrl, WebviewWindowBuilder}; use tauri_plugin_shell::process::CommandChild; use tauri_plugin_shell::ShellExt; @@ -30,11 +34,39 @@ const HEALTH_POLL: Duration = Duration::from_millis(250); /// already-running daemon (the spawned child exited via the single-instance guard). struct Sidecar(Mutex>); +/// Show and focus the main window if it exists yet (it is created after the health-wait). +fn focus_main(app: &tauri::AppHandle) { + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + } +} + fn main() { tauri::Builder::default() + // single-instance MUST be the first plugin: a second launch just focuses us. + .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| { + focus_main(app); + })) .plugin(tauri_plugin_shell::init()) .manage(Sidecar(Mutex::new(None))) .setup(|app| { + // Native chrome (slice 3): a tray icon with Show/Quit. Built on the app's + // bundled icon; Tauri retains the registered tray for the app's lifetime. + let show = MenuItem::with_id(app, "show", "Show Vesper", true, None::<&str>)?; + let quit = MenuItem::with_id(app, "quit", "Quit Vesper", true, None::<&str>)?; + let menu = Menu::with_items(app, &[&show, &quit])?; + TrayIconBuilder::with_id("vesper-tray") + .icon(app.default_window_icon().expect("bundled app icon").clone()) + .tooltip("Vesper") + .menu(&menu) + .on_menu_event(|app, event| match event.id.as_ref() { + "show" => focus_main(app), + "quit" => app.exit(0), + _ => {} + }) + .build(app)?; + // 1. Spawn the compiled daemon sidecar (runs `vesper-daemon daemon run`). let (mut rx, child) = app .shell() @@ -60,15 +92,24 @@ fn main() { let window_handle = handle.clone(); let _ = handle.run_on_main_thread(move || { let url = format!("http://{UI_ADDR}"); - let _ = WebviewWindowBuilder::new( + let builder = WebviewWindowBuilder::new( &window_handle, "main", WebviewUrl::External(url.parse().expect("valid UI url")), ) .title("Vesper") .inner_size(1180.0, 820.0) - .min_inner_size(880.0, 600.0) - .build(); + .min_inner_size(880.0, 600.0); + + // macOS-only: frameless/overlay titlebar so the web UI's custom HTML + // titlebar shows with the native traffic-light buttons inset over it + // (the "native application" look). Windows/Linux are unaffected. + #[cfg(target_os = "macos")] + let builder = builder + .title_bar_style(tauri::TitleBarStyle::Overlay) + .hidden_title(true); + + let _ = builder.build(); }); }); diff --git a/packages/vesper-ui/src/client/brand/builtins.ts b/packages/vesper-ui/src/client/brand/builtins.ts deleted file mode 100644 index ad29a72..0000000 --- a/packages/vesper-ui/src/client/brand/builtins.ts +++ /dev/null @@ -1,193 +0,0 @@ -/// -import { registerMark } from "./registry.ts"; -import type { BrandMark } from "./types.ts"; - -// --- per-brand procedural draws (centered at cx,cy within radius r) ---------- - -function sunburst( - ctx: CanvasRenderingContext2D, - cx: number, - cy: number, - r: number, - c: string, -): void { - ctx.strokeStyle = c; - ctx.lineWidth = Math.max(1.5, r * 0.22); - for (let i = 0; i < 7; i++) { - const a = (i / 7) * Math.PI * 2 - Math.PI / 2; - ctx.beginPath(); - ctx.moveTo(cx + Math.cos(a) * r * 0.28, cy + Math.sin(a) * r * 0.28); - ctx.lineTo(cx + Math.cos(a) * r, cy + Math.sin(a) * r); - ctx.stroke(); - } -} - -function knot(ctx: CanvasRenderingContext2D, cx: number, cy: number, r: number, c: string): void { - ctx.strokeStyle = c; - ctx.lineWidth = Math.max(1.5, r * 0.2); - for (let i = 0; i < 6; i++) { - const a = (i / 6) * Math.PI * 2; - ctx.beginPath(); - ctx.arc(cx + Math.cos(a) * r * 0.42, cy + Math.sin(a) * r * 0.42, r * 0.42, 0, Math.PI * 2); - ctx.stroke(); - } -} - -function sparkle( - ctx: CanvasRenderingContext2D, - cx: number, - cy: number, - r: number, - c: string, -): void { - ctx.fillStyle = c; - const w = r * 0.32; - const pts: [number, number][] = [ - [0, -r], - [w, -w], - [r, 0], - [w, w], - [0, r], - [-w, w], - [-r, 0], - [-w, -w], - ]; - ctx.beginPath(); - for (let i = 0; i < pts.length; i++) { - const p = pts[i]; - if (p === undefined) continue; - if (i === 0) ctx.moveTo(cx + p[0], cy + p[1]); - else ctx.lineTo(cx + p[0], cy + p[1]); - } - ctx.closePath(); - ctx.fill(); -} - -function terminal( - ctx: CanvasRenderingContext2D, - cx: number, - cy: number, - r: number, - c: string, -): void { - ctx.strokeStyle = c; - ctx.lineWidth = Math.max(1.5, r * 0.2); - ctx.lineCap = "round"; - ctx.lineJoin = "round"; - ctx.beginPath(); - ctx.moveTo(cx - r * 0.5, cy - r * 0.4); - ctx.lineTo(cx - r * 0.05, cy); - ctx.lineTo(cx - r * 0.5, cy + r * 0.4); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(cx + r * 0.1, cy + r * 0.45); - ctx.lineTo(cx + r * 0.6, cy + r * 0.45); - ctx.stroke(); -} - -function claw(ctx: CanvasRenderingContext2D, cx: number, cy: number, r: number, c: string): void { - ctx.strokeStyle = c; - ctx.lineWidth = Math.max(1.5, r * 0.22); - ctx.lineCap = "round"; - ctx.beginPath(); - ctx.arc(cx, cy - r * 0.1, r * 0.7, Math.PI * 0.15, Math.PI * 0.95); - ctx.stroke(); - ctx.beginPath(); - ctx.arc(cx, cy + r * 0.35, r * 0.6, -Math.PI * 0.9, -Math.PI * 0.1); - ctx.stroke(); -} - -function wingedStaff( - ctx: CanvasRenderingContext2D, - cx: number, - cy: number, - r: number, - c: string, -): void { - ctx.strokeStyle = c; - ctx.lineWidth = Math.max(1.5, r * 0.18); - ctx.lineCap = "round"; - // staff - ctx.beginPath(); - ctx.moveTo(cx, cy - r * 0.7); - ctx.lineTo(cx, cy + r * 0.7); - ctx.stroke(); - // two short wings near the top - for (const dir of [-1, 1]) { - ctx.beginPath(); - ctx.moveTo(cx, cy - r * 0.45); - ctx.quadraticCurveTo(cx + dir * r * 0.7, cy - r * 0.7, cx + dir * r * 0.9, cy - r * 0.3); - ctx.stroke(); - } -} - -function mechClaw( - ctx: CanvasRenderingContext2D, - cx: number, - cy: number, - r: number, - c: string, -): void { - ctx.strokeStyle = c; - ctx.lineWidth = Math.max(1.5, r * 0.18); - ctx.lineJoin = "miter"; - // three angular talon segments - for (let i = -1; i <= 1; i++) { - const ox = i * r * 0.4; - ctx.beginPath(); - ctx.moveTo(cx + ox, cy - r * 0.6); - ctx.lineTo(cx + ox + r * 0.18, cy); - ctx.lineTo(cx + ox, cy + r * 0.6); - ctx.stroke(); - } -} - -// --- registration ------------------------------------------------------------ - -const MARKS: readonly BrandMark[] = [ - { - id: "claude", - label: "Claude", - color: "#d97757", - draw: (c, x, y, r) => sunburst(c, x, y, r, "#d97757"), - }, - // Codex presents the OpenAI knot; id stays "codex" to match the presence matcher. - { - id: "codex", - label: "Codex", - color: "#1b1b1b", - draw: (c, x, y, r) => knot(c, x, y, r, "#1b1b1b"), - }, - { - id: "gemini", - label: "Gemini", - color: "#7c8cf0", - draw: (c, x, y, r) => sparkle(c, x, y, r, "#7c8cf0"), - }, - { - id: "opencode", - label: "opencode", - color: "#f3b03a", - draw: (c, x, y, r) => terminal(c, x, y, r, "#f3b03a"), - }, - { - id: "zeroclaw", - label: "ZeroClaw", - color: "#d2691e", - draw: (c, x, y, r) => claw(c, x, y, r, "#d2691e"), - }, - { - id: "hermes", - label: "Hermes", - color: "#d4a017", - draw: (c, x, y, r) => wingedStaff(c, x, y, r, "#d4a017"), - }, - { - id: "ironclaw", - label: "IronClaw", - color: "#9fb3c8", - draw: (c, x, y, r) => mechClaw(c, x, y, r, "#9fb3c8"), - }, -]; - -for (const mark of MARKS) registerMark(mark); diff --git a/packages/vesper-ui/src/client/brand/default-glyph.ts b/packages/vesper-ui/src/client/brand/default-glyph.ts deleted file mode 100644 index 6af1273..0000000 --- a/packages/vesper-ui/src/client/brand/default-glyph.ts +++ /dev/null @@ -1,43 +0,0 @@ -/// -import type { BrandMark } from "./types.ts"; - -function drawStar(ctx: CanvasRenderingContext2D, cx: number, cy: number, s: number): void { - ctx.beginPath(); - ctx.moveTo(cx, cy - s); - ctx.lineTo(cx + s * 0.28, cy - s * 0.28); - ctx.lineTo(cx + s, cy); - ctx.lineTo(cx + s * 0.28, cy + s * 0.28); - ctx.lineTo(cx, cy + s); - ctx.lineTo(cx - s * 0.28, cy + s * 0.28); - ctx.lineTo(cx - s, cy); - ctx.lineTo(cx - s * 0.28, cy - s * 0.28); - ctx.closePath(); - ctx.fill(); -} - -/** - * The fallback mark for any agent with no registered brand (Vesper's own - * pipelines, or an unknown agent). An evening-star "V": a chevron with a small - * four-point star above. resolveMark() returns this whenever nothing else matches, - * so a brand mark ALWAYS exists. - */ -export const VESPER_DEFAULT: BrandMark = { - id: "vesper", - label: "Vesper", - color: "#38f0ff", - draw(ctx, cx, cy, r) { - ctx.save(); - ctx.strokeStyle = "#38f0ff"; - ctx.fillStyle = "#38f0ff"; - ctx.lineWidth = Math.max(1.5, r * 0.2); - ctx.lineCap = "round"; - ctx.lineJoin = "round"; - ctx.beginPath(); - ctx.moveTo(cx - r * 0.55, cy - r * 0.3); - ctx.lineTo(cx, cy + r * 0.62); - ctx.lineTo(cx + r * 0.55, cy - r * 0.3); - ctx.stroke(); - drawStar(ctx, cx, cy - r * 0.62, r * 0.28); - ctx.restore(); - }, -}; diff --git a/packages/vesper-ui/src/client/brand/index.ts b/packages/vesper-ui/src/client/brand/index.ts deleted file mode 100644 index 9e049d0..0000000 --- a/packages/vesper-ui/src/client/brand/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Theme-agnostic brand/logo layer — the "every agent shows its real logo" seam. -import "./builtins.ts"; // side-effect: registers the built-in marks - -export { VESPER_DEFAULT } from "./default-glyph.ts"; -export { listMarks, registerMark, resolveMark } from "./registry.ts"; -export type { BrandMark } from "./types.ts"; diff --git a/packages/vesper-ui/src/client/brand/registry.test.ts b/packages/vesper-ui/src/client/brand/registry.test.ts deleted file mode 100644 index 2fbc2e7..0000000 --- a/packages/vesper-ui/src/client/brand/registry.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import "./builtins.ts"; // self-registers the built-in marks -import { listMarks, resolveMark } from "./registry.ts"; - -describe("brand registry resolveMark", () => { - test("resolves a live presence id (presence:) to its brand", () => { - expect(resolveMark("presence:claude-cli").id).toBe("claude"); - expect(resolveMark("presence:claude-app").id).toBe("claude"); - expect(resolveMark("presence:codex-cli").id).toBe("codex"); - expect(resolveMark("presence:gemini-cli").id).toBe("gemini"); - expect(resolveMark("presence:opencode-cli").id).toBe("opencode"); - }); - - test("distinguishes zeroclaw from ironclaw (no substring collision)", () => { - expect(resolveMark("presence:zeroclaw-cli").id).toBe("zeroclaw"); - expect(resolveMark("ironclaw").id).toBe("ironclaw"); - expect(resolveMark("hermes").id).toBe("hermes"); - }); - - test("resolves a bare brand id exactly", () => { - expect(resolveMark("claude").id).toBe("claude"); - expect(resolveMark("codex").id).toBe("codex"); - }); - - test("NEVER returns null — an unknown agent falls back to the Vesper default mark", () => { - expect(resolveMark("skill-train").id).toBe("vesper"); - expect(resolveMark("presence:totally-unknown").id).toBe("vesper"); - expect(resolveMark("").id).toBe("vesper"); - }); - - test("every mark exposes an id, a label, a color, and a draw fn", () => { - for (const mark of listMarks()) { - expect(typeof mark.id).toBe("string"); - expect(mark.label.length).toBeGreaterThan(0); - expect(mark.color).toMatch(/^#[0-9a-f]{6}$/i); - expect(typeof mark.draw).toBe("function"); - } - }); -}); diff --git a/packages/vesper-ui/src/client/brand/registry.ts b/packages/vesper-ui/src/client/brand/registry.ts deleted file mode 100644 index e5f1b2a..0000000 --- a/packages/vesper-ui/src/client/brand/registry.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { VESPER_DEFAULT } from "./default-glyph.ts"; -import type { BrandMark } from "./types.ts"; - -const REGISTRY = new Map(); - -/** Register a brand mark (built-ins self-register on import of ./builtins.ts). */ -export function registerMark(mark: BrandMark): void { - REGISTRY.set(mark.id, mark); -} - -/** All registered marks (e.g. for a theme picker). The Vesper fallback is implicit. */ -export function listMarks(): readonly BrandMark[] { - return [...REGISTRY.values()]; -} - -/** - * Resolve an agent id or brand token to a mark. NEVER returns null — an unknown - * agent falls back to {@link VESPER_DEFAULT}, so every node always has a logo. - * Resolution order: strip a `presence:` prefix, then exact id -> id-prefix -> - * id-substring -> the Vesper default. - */ -export function resolveMark(idOrBrand: string): BrandMark { - const token = idOrBrand.startsWith("presence:") ? idOrBrand.slice("presence:".length) : idOrBrand; - - const exact = REGISTRY.get(token); - if (exact !== undefined) return exact; - - for (const mark of REGISTRY.values()) { - if (token.startsWith(mark.id)) return mark; - } - for (const mark of REGISTRY.values()) { - if (mark.id.length >= 4 && token.includes(mark.id)) return mark; - } - return VESPER_DEFAULT; -} diff --git a/packages/vesper-ui/src/client/brand/types.ts b/packages/vesper-ui/src/client/brand/types.ts deleted file mode 100644 index 4772092..0000000 --- a/packages/vesper-ui/src/client/brand/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -/// - -/** - * A brand mark for an agent — drawn procedurally on the Canvas (no image assets, - * no network; local-first). The brand layer is THEME-AGNOSTIC: every WorldTheme - * resolves and draws marks through it, so "every agent shows its real logo" is a - * structural guarantee, not a per-theme convention. A theme chooses HOW to frame - * a mark (cottage lantern vs neon holo-ring) but never WHETHER it appears. - */ -export interface BrandMark { - /** Stable logo id, also the resolution key (e.g. "claude", "zeroclaw"). */ - readonly id: string; - /** Human label (e.g. "Claude", "ZeroClaw"). */ - readonly label: string; - /** Brand accent color (#rrggbb). */ - readonly color: string; - /** Draw the mark centered at (cx, cy) within radius r, stroked/filled in its color. */ - readonly draw: (ctx: CanvasRenderingContext2D, cx: number, cy: number, r: number) => void; -} diff --git a/packages/vesper-ui/src/client/chat.ts b/packages/vesper-ui/src/client/chat.ts deleted file mode 100644 index 7056521..0000000 --- a/packages/vesper-ui/src/client/chat.ts +++ /dev/null @@ -1,231 +0,0 @@ -/// -import type { ChatSessionRow, ChatTurnRow } from "./chat-types.ts"; - -/** - * Dependencies the chat home borrows from {@link import("./main.ts")} — it REUSES - * the existing live socket + activity panel rather than opening a second transport. - */ -export interface ChatDeps { - /** Send a control frame on the existing `/api/live` socket (no-op when closed). */ - readonly wsSend: (payload: Record) => void; - /** Open the live activity panel for a run (the demoted canvas tree). */ - readonly openActivity: (runId: string) => void; - /** Surface a transient message via the shared toast. */ - readonly toast: (message: string) => void; -} - -/** A rendered transcript turn, keyed by its persisted turn id (for de-dupe). */ -interface RenderedTurn { - readonly id: string; - readonly role: ChatTurnRow["role"]; -} - -function el(id: string): T { - const node = document.getElementById(id); - if (node === null) throw new Error(`missing #${id}`); - return node as T; -} - -/** - * The chat home: the transcript column + input. A message is a manual run of the - * `router` pipeline through the EXISTING run path (`POST /api/chat`); the assistant - * turn carries the `runId` whose live tree the activity panel renders. The transcript - * streams over the SAME `/api/live` socket (a `chat:` topic) and backfills - * via `GET /api/chat/sessions/:id/turns` on load + reconnect. - */ -export class ChatHome { - readonly #deps: ChatDeps; - readonly #thread = el("chat-thread"); - readonly #empty = el("chat-empty"); - readonly #form = el("chat-form"); - readonly #text = el("chat-text"); - readonly #send = el("chat-send"); - - /** The active session id (null until the first message creates one). */ - #sessionId: string | null = null; - /** Highest turn ts we have rendered — the `afterTs` cursor for backfill. */ - #lastTs = 0; - /** Turn ids already in the DOM (de-dupe live frames against backfilled twins). */ - readonly #seen = new Map(); - /** True while a `POST /api/chat` is in flight (disables Send, shows a placeholder). */ - #sending = false; - - constructor(deps: ChatDeps) { - this.#deps = deps; - this.#form.addEventListener("submit", (e) => { - e.preventDefault(); - void this.#submit(); - }); - // Enter sends; Shift+Enter inserts a newline (keyboard-friendly composer). - this.#text.addEventListener("keydown", (e) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - void this.#submit(); - } - }); - this.#text.addEventListener("input", () => this.#autosize()); - } - - /** Grow the composer with its content, capped by CSS max-height. */ - #autosize(): void { - this.#text.style.height = "auto"; - this.#text.style.height = `${this.#text.scrollHeight}px`; - } - - /** Re-subscribe + re-backfill after a (re)connect, so no turn is missed. */ - onSocketOpen(): void { - if (this.#sessionId !== null) { - this.#deps.wsSend({ type: "subscribe", sessionId: this.#sessionId }); - void this.#backfill(); - } - } - - /** A `chat:turn` frame arrived on the live socket — append it if it's ours. */ - onLiveTurn(frame: { turnId?: unknown; runId?: unknown; role?: unknown; text?: unknown }): void { - if (typeof frame.turnId !== "string" || typeof frame.text !== "string") return; - const role = frame.role === "user" ? "user" : "assistant"; - const runId = typeof frame.runId === "string" ? frame.runId : null; - this.#renderTurn({ id: frame.turnId, role, text: frame.text, runId }); - } - - /** Submit the composer: POST /api/chat (the existing run path), then await frames. */ - async #submit(): Promise { - const message = this.#text.value.trim(); - if (message.length === 0 || this.#sending) return; - this.#sending = true; - this.#send.disabled = true; - this.#text.value = ""; - this.#autosize(); - // Optimistic user bubble (replaced by the persisted twin when its frame lands). - const optimisticId = `pending:${Date.now()}`; - this.#renderTurn({ id: optimisticId, role: "user", text: message, runId: null }); - const thinking = this.#renderPending(); - - try { - const res = await fetch("/api/chat", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify( - this.#sessionId === null ? { message } : { sessionId: this.#sessionId, message }, - ), - }); - const body = (await res.json()) as { - sessionId?: string; - turnId?: string; - runId?: string | null; - error?: string; - }; - thinking.remove(); - if (!res.ok || typeof body.sessionId !== "string") { - this.#deps.toast(body.error ?? "could not send"); - return; - } - // First message established a session — subscribe + adopt it. Frames for both - // turns are published server-side; the backfill is the durable safety net. - if (this.#sessionId === null) { - this.#sessionId = body.sessionId; - this.#deps.wsSend({ type: "subscribe", sessionId: body.sessionId }); - } - await this.#backfill(); - } catch { - thinking.remove(); - this.#deps.toast("could not send"); - } finally { - this.#sending = false; - this.#send.disabled = false; - this.#text.focus(); - } - } - - /** Fetch any turns after our cursor and render them (idempotent via #seen). */ - async #backfill(): Promise { - if (this.#sessionId === null) return; - try { - const url = `/api/chat/sessions/${encodeURIComponent(this.#sessionId)}/turns?afterTs=${this.#lastTs}`; - const res = await fetch(url); - if (!res.ok) return; - const turns = (await res.json()) as ChatTurnRow[]; - for (const t of turns) { - this.#renderTurn(t); - if (t.ts > this.#lastTs) this.#lastTs = t.ts; - } - } catch { - // transient; the next frame or reconnect recovers. - } - } - - /** Restore the most recent session's transcript on first load (if any exists). */ - async restoreLatest(): Promise { - try { - const res = await fetch("/api/chat/sessions"); - if (!res.ok) return; - const sessions = (await res.json()) as ChatSessionRow[]; - const latest = sessions[0]; // server returns newest-first - if (latest === undefined) return; - this.#sessionId = latest.id; - this.#deps.wsSend({ type: "subscribe", sessionId: latest.id }); - await this.#backfill(); - } catch { - // no prior sessions / transient — the empty state stays. - } - } - - /** Append a "thinking…" placeholder; returns it so the caller can remove it. */ - #renderPending(): HTMLElement { - const row = document.createElement("div"); - row.className = "bubble assistant pending"; - row.textContent = "thinking…"; - this.#thread.append(row); - this.#scrollToEnd(); - return row; - } - - /** Render (or, for the optimistic user turn, leave) one transcript bubble. */ - #renderTurn(turn: { - id: string; - role: ChatTurnRow["role"]; - text: string; - runId: string | null; - }): void { - if (this.#seen.has(turn.id)) return; // de-dupe replayed/twin frames. - // Drop the optimistic user bubble once the persisted user turn arrives. - if (turn.role === "user") this.#dropOptimistic(); - this.#empty.style.display = "none"; - - const row = document.createElement("div"); - row.className = `bubble ${turn.role}`; - row.dataset.turnId = turn.id; - const textNode = document.createElement("div"); - textNode.textContent = turn.text; - row.append(textNode); - - // An assistant turn carries the run that produced it — offer to watch it live. - if (turn.role === "assistant" && turn.runId !== null) { - const runId = turn.runId; - const watch = document.createElement("button"); - watch.type = "button"; - watch.className = "watch"; - watch.textContent = "Watch it work"; - watch.addEventListener("click", () => this.#deps.openActivity(runId)); - row.append(watch); - } - - this.#thread.append(row); - this.#seen.set(turn.id, { id: turn.id, role: turn.role }); - this.#scrollToEnd(); - } - - /** Remove the lone optimistic user bubble (id begins with `pending:`). */ - #dropOptimistic(): void { - const pending = this.#thread.querySelector('[data-turn-id^="pending:"]'); - if (pending !== null) { - const id = pending.dataset.turnId; - if (id !== undefined) this.#seen.delete(id); - pending.remove(); - } - } - - #scrollToEnd(): void { - this.#thread.scrollTop = this.#thread.scrollHeight; - } -} diff --git a/packages/vesper-ui/src/client/index.html b/packages/vesper-ui/src/client/index.html index 0c3ba69..14a741d 100644 --- a/packages/vesper-ui/src/client/index.html +++ b/packages/vesper-ui/src/client/index.html @@ -4,493 +4,225 @@ Vesper + - - - -