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
5 changes: 5 additions & 0 deletions crates/toolpath-desktop/.taurignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Tauri CLI file-watcher ignore list (gitignore syntax).
# `cargo tauri dev` watches the crate root and restarts the Rust binary on
# any change. Vite already hot-reloads the Svelte frontend, so we don't
# want the frontend tree to trigger a Rust restart.
frontend/
16 changes: 16 additions & 0 deletions crates/toolpath-desktop/frontend/public/contour-texture.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
152 changes: 91 additions & 61 deletions crates/toolpath-desktop/frontend/src/app.svelte
Original file line number Diff line number Diff line change
@@ -1,68 +1,98 @@
<script lang="ts">
import { store } from "./lib/store.svelte";
import Home from "./routes/Home.svelte";
import BrowseAgents from "./routes/BrowseAgents.svelte";
import BrowseClaude from "./routes/BrowseClaude.svelte";
import BrowsePi from "./routes/BrowsePi.svelte";
import BrowseGit from "./routes/BrowseGit.svelte";
import BrowseGithub from "./routes/BrowseGithub.svelte";
import Preview from "./routes/Preview.svelte";
import Result from "./routes/Result.svelte";
import { store } from "./lib/store.svelte";
import Home from "./routes/Home.svelte";
import BrowseAgents from "./routes/BrowseAgents.svelte";
import BrowseClaude from "./routes/BrowseClaude.svelte";
import BrowsePi from "./routes/BrowsePi.svelte";
import BrowseGit from "./routes/BrowseGit.svelte";
import BrowseGithub from "./routes/BrowseGithub.svelte";
import Preview from "./routes/Preview.svelte";
import Result from "./routes/Result.svelte";
import type { Route } from "./lib/types";

// `__TAURI_INTERNALS__` is injected by Tauri's webview. In a plain browser
// tab (e.g. the Vite dev server at localhost:1420 opened manually for
// CSS/DevTools work) it's undefined, every IPC call errors, and every
// screen looks broken. Detect that once and surface a clear banner.
const notInTauri =
typeof window !== "undefined" &&
!(window as unknown as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__;
const notInTauri =
typeof window !== "undefined" &&
!(window as unknown as { __TAURI_INTERNALS__?: unknown })
.__TAURI_INTERNALS__;

// Primary nav. §-numbers match the cartographic eyebrow styling.
const TABS: { key: Route; num: string; label: string }[] = [
{ key: "home", num: "1", label: "New upload" },
{ key: "preview", num: "2", label: "Preview" },
{ key: "result", num: "3", label: "Result" },
];

// "Home" is selected whenever we're on home or in any browse sub-flow.
function activeTab(r: Route): Route {
if (r.startsWith("browse-")) return "home";
return r;
}
</script>

{#if notInTauri}
<div class="banner banner--warn">
<strong>Not a Tauri window.</strong>
This page is the Vite dev server; IPC calls won't work. Use the native
window opened by <code>cargo tauri dev</code>.
</div>
{/if}
<div class="backdrop"></div>

<div class="window">
<!-- Primary tabs -->
<nav class="tabs" aria-label="Primary">
{#each TABS as t (t.key)}
{@const isActive = activeTab(store.m.route) === t.key}
{@const disabled =
(t.key === "preview" && !store.m.preview) ||
(t.key === "result" && !store.m.result)}
<button
class={"tabs__item" + (isActive ? " tabs__item--active" : "")}
{disabled}
style={disabled ? "opacity:0.4;cursor:not-allowed" : ""}
onclick={() =>
!disabled &&
store.dispatch({ t: "NavigateTo", screen: t.key })}
>
<span class="tabs__num">{t.num}:</span>
<span>{t.label}</span>
</button>
{/each}
</nav>

<header class="appbar">
<div class="appbar__brand">
<span class="appbar__logo" aria-hidden="true"></span>
<strong>Toolpath</strong>
<span class="appbar__tag">Pathbase companion</span>
</div>
<nav class="appbar__nav">
<button
class="linklike"
onclick={() => store.dispatch({ t: "NavigateTo", screen: "home" })}
>Home</button>
</nav>
</header>
<!-- Main scroll area -->
<main class="main">
{#if notInTauri}
<div class="banner">
<strong>Not a Tauri window.</strong>
This page is the Vite dev server; IPC calls won't work. Use the native
window opened by <code>cargo tauri dev</code>.
</div>
{/if}

<main class={"screen" + (store.m.route === "preview" ? " screen--wide" : "")}>
{#if store.m.error}
<div class="error">
{store.m.error}
<button class="linklike" onclick={() => store.dispatch({ t: "ClearError" })}>dismiss</button>
</div>
{/if}
{#if store.m.error}
<div class="page" style="padding-bottom:0">
<div class="error">
<span>{store.m.error}</span>
<span class="spacer"></span>
<button
class="btn btn--sm"
onclick={() => store.dispatch({ t: "ClearError" })}
>Dismiss</button
>
</div>
</div>
{/if}

{#if store.m.route === "home"}
<Home />
{:else if store.m.route === "browse-agents"}
<BrowseAgents />
{:else if store.m.route === "browse-claude"}
<BrowseClaude />
{:else if store.m.route === "browse-pi"}
<BrowsePi />
{:else if store.m.route === "browse-git"}
<BrowseGit />
{:else if store.m.route === "browse-github"}
<BrowseGithub />
{:else if store.m.route === "preview"}
<Preview />
{:else if store.m.route === "result"}
<Result />
{/if}
</main>
{#if store.m.route === "home"}
<Home />
{:else if store.m.route === "browse-agents"}
<BrowseAgents />
{:else if store.m.route === "browse-claude"}
<BrowseClaude />
{:else if store.m.route === "browse-pi"}
<BrowsePi />
{:else if store.m.route === "browse-git"}
<BrowseGit />
{:else if store.m.route === "browse-github"}
<BrowseGithub />
{:else if store.m.route === "preview"}
<Preview />
{:else if store.m.route === "result"}
<Result />
{/if}
</main>
</div>
33 changes: 33 additions & 0 deletions crates/toolpath-desktop/frontend/src/lib/SourceLogo.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<script lang="ts">
// Brand marks for the four source types. Paths from simple-icons (MIT/CC0)
// where applicable; Claude uses Anthropic's 8-point burst mark; Pi uses a
// styled π glyph since pi.dev doesn't publish a simple-icons entry.
interface Props {
kind: "claude" | "pi" | "git" | "github";
size?: number;
color?: string;
}
let { kind, size = 16, color = "currentColor" }: Props = $props();
</script>

{#if kind === "github"}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width={size} height={size} fill={color} aria-label="GitHub">
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
</svg>
{:else if kind === "git"}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width={size} height={size} fill={color} aria-label="Git">
<path d="M23.546 10.93L13.067.452c-.604-.603-1.582-.603-2.188 0L8.708 2.627l2.76 2.76c.645-.215 1.379-.07 1.889.441.516.515.658 1.258.438 1.9l2.658 2.66c.645-.223 1.387-.078 1.9.435.721.72.721 1.884 0 2.604-.719.719-1.881.719-2.6 0-.539-.541-.674-1.337-.404-1.996L12.86 8.955v6.525c.176.086.342.203.488.348.713.721.713 1.883 0 2.6-.719.721-1.889.721-2.609 0-.719-.719-.719-1.879 0-2.598.182-.18.387-.316.605-.406V8.835c-.217-.091-.424-.222-.6-.401-.545-.545-.676-1.342-.396-2.009L7.636 3.7.45 10.881c-.6.605-.6 1.584 0 2.189l10.48 10.477c.604.604 1.582.604 2.186 0l10.43-10.43c.605-.603.605-1.582 0-2.187"/>
</svg>
{:else if kind === "claude"}
<!-- Anthropic "A" mark (simple-icons: anthropic). Used for Claude Code. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width={size} height={size} fill={color} aria-label="Claude (Anthropic)">
<path d="M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5527h3.7442L10.5363 3.541Zm-.3712 10.2232 2.2909-5.9502 2.2909 5.9502Z"/>
</svg>
{:else if kind === "pi"}
<!-- pi.dev — bold π glyph in a squared frame. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width={size} height={size} aria-label="Pi.dev">
<rect x="1.5" y="1.5" width="21" height="21" rx="3" fill="none" stroke={color} stroke-width="1.8"/>
<path d="M6.5 9.5 h11 M9 9.5 v8 c0 1 -.5 1.5 -1.5 1.5 M15 9.5 v7.5 c0 1.2 .6 2 1.8 2"
fill="none" stroke={color} stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
{/if}
89 changes: 50 additions & 39 deletions crates/toolpath-desktop/frontend/src/routes/BrowseAgents.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,58 @@
import type { AgentStatus } from "../lib/types";

function statusLabel(s: AgentStatus): string {
switch (s) {
case "available": return "Installed";
case "unavailable": return "Not detected";
case "coming-soon": return "Coming soon";
}
if (s === "available") return "Installed";
if (s === "unavailable") return "Not detected";
return "Coming soon";
}
function statusTag(s: AgentStatus): string {
if (s === "available") return "tag tag--ok";
if (s === "unavailable") return "tag tag--muted";
return "tag tag--warn";
}
</script>

<div class="row">
<button class="linklike" onclick={() => store.dispatch({ t: "NavigateTo", screen: "home" })}>← Back</button>
</div>
<h1>Agents</h1>
<p class="subtitle">
{store.m.agents.loading ? "Detecting installed agents… " : "Select an agent to browse its traces."}
{#if store.m.agents.loading}<span class="spinner"></span>{/if}
</p>
<div class="page">
<div class="row" style="margin-bottom:14px">
<button class="btn btn--ghost" onclick={() => store.dispatch({ t: "NavigateTo", screen: "home" })}>← Back</button>
</div>

{#if store.m.agents.list === null}
<!-- first fetch in-flight; nothing to show yet -->
{:else if store.m.agents.list.length === 0}
<div class="notice">No agents known.</div>
{:else}
<div class="cards">
{#each store.m.agents.list as agent (agent.id)}
{@const clickable = agent.status === "available"}
<div
class={"card" + (clickable ? "" : " card--disabled")}
role="button"
tabindex={clickable ? 0 : -1}
onclick={clickable ? () => store.dispatch({ t: "AgentsSelect", agent }) : undefined}
>
<div class="row">
<div class="card__title">{agent.name}</div>
<div class="spacer"></div>
<span class={"badge badge--" + agent.status}>{statusLabel(agent.status)}</span>
</div>
<div class="card__desc">{agent.tagline ?? ""}</div>
{#if agent.reason}
<div class="card__hint">{agent.reason}</div>
{/if}
</div>
{/each}
<div class="page__header">
<div>
<div class="page__eyebrow">§1.A · AGENTS</div>
<h1 class="page__title">AI coding agents</h1>
<p class="page__lede">
{store.m.agents.loading ? "Detecting installed agents…" : "Select an installed agent to browse its sessions."}
{#if store.m.agents.loading}<span class="spinner"></span>{/if}
</p>
</div>
</div>
{/if}

{#if store.m.agents.list === null}
<!-- first fetch in-flight -->
{:else if store.m.agents.list.length === 0}
<div class="notice">No agents known.</div>
{:else}
<div style="border:0.5px solid var(--ink-5); background:var(--paper-bright)">
{#each store.m.agents.list as agent (agent.id)}
{@const clickable = agent.status === "available"}
<button
class={"row-card" + (clickable ? "" : "")}
disabled={!clickable}
style={!clickable ? "opacity:0.55; cursor:not-allowed" : ""}
onclick={() => clickable && store.dispatch({ t: "AgentsSelect", agent })}
>
<span class="row-card__marker">△</span>
<div style="min-width:0">
<div class="row-card__title">{agent.name}</div>
<div class="row-card__sub">{agent.tagline ?? ""}</div>
{#if agent.reason}
<div class="row-card__meta"><span>{agent.reason}</span></div>
{/if}
</div>
<span class={statusTag(agent.status)}>{statusLabel(agent.status)}</span>
</button>
{/each}
</div>
{/if}
</div>
Loading
Loading