Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/ui/src/lib/stores/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
25 changes: 25 additions & 0 deletions packages/ui/src/lib/stores/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,31 @@ export async function loadGraphFromUrl(url: string): Promise<void> {
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.
*/
Expand Down
8 changes: 6 additions & 2 deletions packages/ui/src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { viewMode } from '$lib/stores/graph';
import { loadGraphFromUrl } from '$lib/stores/loader';
import { loadGraphForCurrentProject } from '$lib/stores/loader';
import TreeView from '$lib/components/TreeView/TreeView.svelte';
import GraphView from '$lib/components/GraphView/GraphView.svelte';
import ContextEditor from '$lib/components/Editor/ContextEditor.svelte';
Expand All @@ -11,10 +11,14 @@
let loaded = false;
let error = '';
let showChat = true;
let graphSource: 'real' | 'demo' | null = null;

onMount(async () => {
try {
await loadGraphFromUrl('/demo-graph.json');
// Prefer the graph for the project the server is running in. Falls
// back to the bundled demo fixture only if no .klonode/graph.json
// exists for the server's cwd.
graphSource = await loadGraphForCurrentProject();
loaded = true;
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to load graph';
Expand Down
15 changes: 14 additions & 1 deletion packages/ui/src/routes/api/chat/stream/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
80 changes: 80 additions & 0 deletions packages/ui/src/routes/api/graph/current/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* GET /api/graph/current
*
* Returns the routing graph for the actual project the Workstation backend
* is running in. Reads `.klonode/graph.json` from the server's cwd (or from
* a path supplied via the `repoPath` query string). If no graph exists, the
* endpoint responds 404 so the client can fall back to the bundled demo
* fixture.
*
* Without this endpoint the Workstation always loaded `/demo-graph.json`
* at boot, so every user saw a fake `demo-project` tree with `app / lib /
* tests` folders no matter which real repo they had Klonode running in.
*/

import { json } from '@sveltejs/kit';
import { existsSync, readFileSync } from 'fs';
import { join, dirname } from 'path';
import type { RequestHandler } from './$types';

/**
* Walk up from `start` looking for `.klonode/graph.json`. Returns the
* directory that contains it, or `null` if we hit the filesystem root.
*
* This is needed because the dev server's cwd is usually `packages/ui/`
* (where launch.json's `cwd` points), not the repo root. A fresh user
* running `pnpm dev` inside any package should still see their project's
* real graph, not the bundled demo fixture.
*/
function findKlonodeRoot(start: string): string | null {
let dir = start;
const { root } = { root: dir.match(/^[A-Z]:\\|^\//)?.[0] ?? '/' };
let guard = 0;
while (dir && dir !== root && guard++ < 50) {
if (existsSync(join(dir, '.klonode', 'graph.json'))) return dir;
const parent = dirname(dir);
if (parent === dir) break;
dir = parent;
}
return null;
}

export const GET: RequestHandler = async ({ url }) => {
const explicit = url.searchParams.get('repoPath');
const startDir = explicit || process.cwd();
const repoPath = explicit && existsSync(join(explicit, '.klonode', 'graph.json'))
? explicit
: findKlonodeRoot(startDir);

if (!repoPath) {
return json(
{
error: 'no .klonode/graph.json found walking up from server cwd',
searchedFrom: startDir,
hint: 'run `klonode init` in your project root, or pass ?repoPath=<abs> to target a specific project',
},
{ status: 404 },
);
}

const graphPath = join(repoPath, '.klonode', 'graph.json');

try {
const raw = readFileSync(graphPath, 'utf-8');
const graph = JSON.parse(raw);
// Make sure repoPath is set so the ChatPanel's spawn path finds the
// right cwd even if the stored graph was generated on another machine.
if (!graph.repoPath || graph.repoPath === '' || graph.repoPath === '/path/to/your/project') {
graph.repoPath = repoPath;
}
return json(graph);
} catch (err) {
return json(
{
error: 'failed to parse .klonode/graph.json',
detail: err instanceof Error ? err.message : String(err),
},
{ status: 500 },
);
}
};
2 changes: 1 addition & 1 deletion packages/ui/static/demo-graph.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "demo-project",
"repoPath": "/path/to/your/project",
"repoPath": "",
"rootNodeId": "root",
"nodes": {
"root": {
Expand Down
Loading