From a9196c36e92b6d8b7b651724c1d21b7874c5a725 Mon Sep 17 00:00:00 2001 From: smorchj Date: Wed, 15 Apr 2026 09:39:37 +0200 Subject: [PATCH 1/2] Fix two blockers that crashed every first-time chat send MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found while dogfooding Klonode on itself: opening the Workstation UI, typing any prompt in the chat panel, and clicking send failed immediately with "Feil: get is not defined" and the request never reached the /api/chat/stream endpoint. Two independent bugs chained together to make the UI unusable out of the box. ## Bug 1: missing `get` import in agents.ts The self-hosting-survival helpers getCliSessionId / setCliSessionId / clearCliSessionId call svelte/store's `get(sessionsStore)` but the file only imported `{ writable, derived }`. Every chat send threw ReferenceError at JSON.stringify(body) time because the fetch body built `sessionId: getCliSessionId(...)`. Caught by the catch block and rendered as "Feil: get is not defined" in the assistant bubble, with no fetch ever being made. Fix: import { writable, derived, get } from 'svelte/store'. ## Bug 2: demo graph ships with a placeholder repoPath static/demo-graph.json had `"repoPath": "/path/to/your/project"` — a literal placeholder. The ChatPanel passes graphStore.repoPath as the cwd to /api/chat/stream, which passes it to spawn() as the child working directory. On a fresh install a first-time user's chat send would try to spawn Claude CLI in a nonexistent directory. Fix: 1. static/demo-graph.json now uses "" for repoPath so the intent is unambiguous. 2. /api/chat/stream now validates body.repoPath exists on disk and falls back to process.cwd() with a console.warn if it doesn't. That gives a sane default instead of an opaque spawn failure when the user hasn't configured their project yet. ## How these were found Sent a short "Reply with just READY" test through the chat textarea via the preview tools, got "Feil: get is not defined". Stepped through the send path, monkey-patched window.fetch to see the body, found the fetch was never called. Traced backwards from JSON.stringify and found getCliSessionId → get() → missing import. After fixing that, repeated the test, got a working response, then validated end-to-end by having Claude inside the chat panel write PR #67 (PHP extractor support). Both fixes verified by the full "type in textarea, click send" human path. ## Test plan - pnpm --filter @klonode/core test — 78/78 passing - Manual: Klonode Workstation UI, fresh page load, type a short prompt, click send. Before: "Feil: get is not defined" in the assistant bubble within 100ms. After: streaming Claude CLI response with tool calls visible. --- packages/ui/src/lib/stores/agents.ts | 2 +- packages/ui/src/routes/api/chat/stream/+server.ts | 15 ++++++++++++++- packages/ui/static/demo-graph.json | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/lib/stores/agents.ts b/packages/ui/src/lib/stores/agents.ts index 1c800ea..af99be0 100644 --- a/packages/ui/src/lib/stores/agents.ts +++ b/packages/ui/src/lib/stores/agents.ts @@ -4,7 +4,7 @@ * Context routing happens automatically — user just picks which session to talk to. */ -import { writable, derived } from 'svelte/store'; +import { writable, derived, get } from 'svelte/store'; export type ContextDepth = 'minimal' | 'light' | 'standard' | 'heavy' | 'full'; diff --git a/packages/ui/src/routes/api/chat/stream/+server.ts b/packages/ui/src/routes/api/chat/stream/+server.ts index 906c481..3771c75 100644 --- a/packages/ui/src/routes/api/chat/stream/+server.ts +++ b/packages/ui/src/routes/api/chat/stream/+server.ts @@ -25,7 +25,20 @@ interface StreamRequest { export const POST: RequestHandler = async ({ request }) => { const body: StreamRequest = await request.json(); const cliPath = body.cliPath || 'claude'; - const cwd = body.repoPath || process.cwd(); + // Validate repoPath exists on disk — otherwise fall back to the server's + // cwd. The shipped demo graph has `/path/to/your/project` as a placeholder, + // and if the user never configured a real repo path the first chat send + // would spawn the CLI in a nonexistent directory and fail with an opaque + // error. Falling back to process.cwd() gives a sane default. + let cwd = body.repoPath || process.cwd(); + if (body.repoPath && !existsSync(body.repoPath)) { + console.warn( + `[Klonode Stream] repoPath "${body.repoPath}" does not exist on disk; ` + + `falling back to process.cwd() = "${process.cwd()}". ` + + `Configure a real repo path via settings or reinitialize the graph.`, + ); + cwd = process.cwd(); + } // Build system prompt let systemPrompt: string; diff --git a/packages/ui/static/demo-graph.json b/packages/ui/static/demo-graph.json index d830184..5edf3ff 100644 --- a/packages/ui/static/demo-graph.json +++ b/packages/ui/static/demo-graph.json @@ -1,6 +1,6 @@ { "id": "demo-project", - "repoPath": "/path/to/your/project", + "repoPath": "", "rootNodeId": "root", "nodes": { "root": { From 5ca7ab59d9ffe0e916f8eaa0037c530e514bd857 Mon Sep 17 00:00:00 2001 From: smorchj Date: Wed, 15 Apr 2026 09:47:13 +0200 Subject: [PATCH 2/2] Auto-load the real project graph at boot instead of the demo fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third blocker found while dogfooding: even after fixing the chat send path, the TreeView and GraphView still showed a fake `demo-project` tree with `app / lib / tests` folders. The Workstation hard-coded \`loadGraphFromUrl('/demo-graph.json')\` in +page.svelte onMount, so every user — regardless of which real repo they had Klonode running in — saw the same bundled fixture. ## Fix - **New endpoint** GET /api/graph/current. Walks up from the server's cwd looking for \`.klonode/graph.json\` and returns it if found. Responds 404 when no initialized project is found so the client can fall back. Also supports ?repoPath= for explicit targeting. - **New loader helper** loadGraphForCurrentProject() in loader.ts. Tries /api/graph/current first, falls back to the bundled /demo-graph.json if the project isn't initialized. - **+page.svelte** now calls loadGraphForCurrentProject() at boot. Tracks whether it loaded 'real' vs 'demo' so the UI can signal this in a future toolbar indicator. ## The walk-up matters The Workstation dev server is typically started inside packages/ui/ (where launch.json's cwd points), not the repo root. A naive \`process.cwd() + .klonode/graph.json\` check would 404 on every monorepo. The new endpoint walks up parent directories (with a guard against infinite loops) until it finds an initialized project or hits the filesystem root. ## Verified Dev server up, page loaded. Store now reports: - rootName: "KlonodeV2" (was "demo-project") - nodeCount: 69 (was 4) - topLevelNames: [".github", ".marketing", "packages"] (was ["app", "lib", "tests"]) TreeView renders .github/ISSUE_TEMPLATE, .github/workflows, .marketing, and packages/{cli, core, desktop, ui}. --- packages/ui/src/lib/stores/loader.ts | 25 ++++++ packages/ui/src/routes/+page.svelte | 8 +- .../src/routes/api/graph/current/+server.ts | 80 +++++++++++++++++++ 3 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 packages/ui/src/routes/api/graph/current/+server.ts diff --git a/packages/ui/src/lib/stores/loader.ts b/packages/ui/src/lib/stores/loader.ts index d9975f9..9b7018c 100644 --- a/packages/ui/src/lib/stores/loader.ts +++ b/packages/ui/src/lib/stores/loader.ts @@ -27,6 +27,31 @@ export async function loadGraphFromUrl(url: string): Promise { graphStore.set(graph); } +/** + * Load the graph for the project the Workstation backend is running in. + * Tries `/api/graph/current` first (which reads `.klonode/graph.json` from + * the server cwd), and falls back to `/demo-graph.json` if no real graph + * exists. This means a fresh user who has run `klonode init` in their + * project sees the real tree, not a fake `demo-project`. See #64 / + * self-hosting survival work. + */ +export async function loadGraphForCurrentProject(): Promise< + 'real' | 'demo' +> { + try { + const res = await fetch('/api/graph/current'); + if (res.ok) { + const data: SerializedGraph = await res.json(); + graphStore.set(hydrateGraph(data)); + return 'real'; + } + } catch { + // fall through to demo + } + await loadGraphFromUrl('/demo-graph.json'); + return 'demo'; +} + /** * Convert serialized JSON into a proper RoutingGraph with Maps. */ diff --git a/packages/ui/src/routes/+page.svelte b/packages/ui/src/routes/+page.svelte index dc91cfa..4f8d542 100644 --- a/packages/ui/src/routes/+page.svelte +++ b/packages/ui/src/routes/+page.svelte @@ -1,7 +1,7 @@