feat(v2): scaffold Tauri 2.x + Svelte 5 project (Phase 1)

- Tauri 2.10 + Svelte 5.45 + TypeScript + Vite 7
- Catppuccin Mocha theme with CSS variables and semantic aliases
- CSS Grid layout: sidebar (260px) + workspace, responsive breakpoints
  for ultrawide (3440px+) and narrow (<1200px)
- Component structure: Layout/, Terminal/, Agent/, Markdown/, Sidebar/
- Svelte 5 stores with $state runes: sessions, agents, layout
- SDK message adapter (abstracts Agent SDK wire format)
- PTY bridge (Tauri IPC wrapper, stubbed for Phase 2)
- Node.js sidecar entry point (stdio NDJSON, stubbed for Phase 3)
- Rust modules: pty, sidecar, watcher, session (stubbed)
- Vite dev server on port 9700
- Build verified: binary + .deb + .rpm + AppImage all produced
This commit is contained in:
Hibryda 2026-03-05 23:26:27 +01:00
parent 5996615e68
commit 758d626fab
51 changed files with 2287 additions and 0 deletions

27
v2/src/App.svelte Normal file
View file

@ -0,0 +1,27 @@
<script lang="ts">
import SessionList from './lib/components/Sidebar/SessionList.svelte';
import TilingGrid from './lib/components/Layout/TilingGrid.svelte';
</script>
<aside class="sidebar">
<SessionList />
</aside>
<main class="workspace">
<TilingGrid />
</main>
<style>
.sidebar {
background: var(--bg-secondary);
border-right: 1px solid var(--border);
overflow-y: auto;
display: flex;
flex-direction: column;
}
.workspace {
background: var(--bg-primary);
overflow: hidden;
position: relative;
}
</style>

61
v2/src/app.css Normal file
View file

@ -0,0 +1,61 @@
@import './lib/styles/catppuccin.css';
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
height: 100%;
width: 100%;
overflow: hidden;
background: var(--bg-primary);
color: var(--text-primary);
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 13px;
line-height: 1.4;
-webkit-font-smoothing: antialiased;
}
#app {
height: 100%;
width: 100%;
display: grid;
grid-template-columns: var(--sidebar-width) 1fr;
grid-template-rows: 1fr;
}
/* Ultrawide: show right panel */
@media (min-width: 3440px) {
#app {
grid-template-columns: var(--sidebar-width) 1fr var(--right-panel-width);
}
}
/* Narrow: collapse sidebar to icons */
@media (max-width: 1200px) {
#app {
grid-template-columns: 48px 1fr;
}
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--ctp-surface2);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--ctp-overlay0);
}

View file

@ -0,0 +1,30 @@
// PTY Bridge — IPC wrapper for Rust PTY backend
// Phase 2: terminal spawn, resize, input/output streaming
export interface PtyOptions {
shell?: string;
cwd?: string;
env?: Record<string, string>;
cols?: number;
rows?: number;
}
/**
* Spawn a new PTY session via Tauri IPC.
* Phase 2: implement with @tauri-apps/api invoke
*/
export async function spawnPty(_options: PtyOptions): Promise<string> {
throw new Error('Not implemented — Phase 2');
}
export async function writePty(_id: string, _data: string): Promise<void> {
throw new Error('Not implemented — Phase 2');
}
export async function resizePty(_id: string, _cols: number, _rows: number): Promise<void> {
throw new Error('Not implemented — Phase 2');
}
export async function killPty(_id: string): Promise<void> {
throw new Error('Not implemented — Phase 2');
}

View file

@ -0,0 +1,25 @@
// SDK Message Adapter — insulates UI from Claude Agent SDK wire format changes
// This is the ONLY place that knows SDK internals.
// Phase 3: full implementation
export interface AgentMessage {
id: string;
type: 'text' | 'tool_call' | 'tool_result' | 'subagent_spawn' | 'subagent_stop' | 'status' | 'cost' | 'unknown';
parentId?: string;
content: unknown;
timestamp: number;
}
/**
* Adapt a raw SDK message to our internal format.
* When SDK changes wire format, only this function needs updating.
*/
export function adaptSDKMessage(raw: Record<string, unknown>): AgentMessage {
// Phase 3: implement based on actual SDK message types
return {
id: (raw.id as string) ?? crypto.randomUUID(),
type: 'unknown',
content: raw,
timestamp: Date.now(),
};
}

View file

@ -0,0 +1,54 @@
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
title: string;
children: Snippet;
}
let { title, children }: Props = $props();
</script>
<div class="pane-container">
<div class="pane-header">
<span class="pane-title">{title}</span>
</div>
<div class="pane-content">
{@render children()}
</div>
</div>
<style>
.pane-container {
display: flex;
flex-direction: column;
background: var(--bg-secondary);
border-radius: var(--border-radius);
overflow: hidden;
border: 1px solid var(--border);
}
.pane-header {
height: var(--pane-header-height);
display: flex;
align-items: center;
padding: 0 10px;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.pane-title {
font-size: 12px;
font-weight: 500;
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pane-content {
flex: 1;
overflow: auto;
}
</style>

View file

@ -0,0 +1,44 @@
<script lang="ts">
interface Props {
title: string;
status?: 'idle' | 'running' | 'error' | 'done';
}
let { title, status = 'idle' }: Props = $props();
</script>
<div class="pane-header">
<span class="pane-title">{title}</span>
{#if status !== 'idle'}
<span class="status-indicator {status}">{status}</span>
{/if}
</div>
<style>
.pane-header {
height: var(--pane-header-height);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.pane-title {
font-size: 12px;
font-weight: 500;
color: var(--text-secondary);
}
.status-indicator {
font-size: 10px;
padding: 2px 6px;
border-radius: 3px;
}
.status-indicator.running { color: var(--ctp-blue); }
.status-indicator.error { color: var(--ctp-red); }
.status-indicator.done { color: var(--ctp-green); }
</style>

View file

@ -0,0 +1,58 @@
<script lang="ts">
import PaneContainer from './PaneContainer.svelte';
// Phase 2: dynamic pane management, resize, presets
// For now: single empty pane as placeholder
</script>
<div class="tiling-grid">
<PaneContainer title="Welcome">
<div class="welcome">
<h1>BTerminal v2</h1>
<p>Claude Agent Mission Control</p>
<div class="status">
<span class="badge">Phase 1 — Scaffold</span>
</div>
</div>
</PaneContainer>
</div>
<style>
.tiling-grid {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr;
gap: var(--pane-gap);
height: 100%;
padding: var(--pane-gap);
}
.welcome {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 8px;
color: var(--text-muted);
}
.welcome h1 {
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
}
.welcome p {
font-size: 14px;
}
.badge {
background: var(--bg-surface);
color: var(--accent);
padding: 4px 12px;
border-radius: var(--border-radius);
font-size: 12px;
margin-top: 8px;
}
</style>

View file

@ -0,0 +1,39 @@
<script lang="ts">
// Phase 4: session CRUD, groups, types
</script>
<div class="session-list">
<div class="header">
<h2>Sessions</h2>
</div>
<div class="empty-state">
<p>No sessions yet.</p>
<p class="hint">Phase 2 will add terminal sessions.</p>
</div>
</div>
<style>
.session-list {
padding: 12px;
}
.header h2 {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 12px;
}
.empty-state {
color: var(--text-muted);
font-size: 12px;
text-align: center;
padding: 24px 0;
}
.hint {
margin-top: 4px;
font-size: 11px;
color: var(--ctp-overlay0);
}
</style>

View file

@ -0,0 +1,34 @@
// Agent tracking state — Svelte 5 runes
// Phase 3: SDK agent lifecycle, subagent tree
export type AgentStatus = 'idle' | 'running' | 'thinking' | 'waiting' | 'done' | 'error';
export interface AgentState {
id: string;
sessionId: string;
parentId?: string;
status: AgentStatus;
model?: string;
costUsd?: number;
tokensIn?: number;
tokensOut?: number;
}
let agents = $state<AgentState[]>([]);
export function getAgents() {
return agents;
}
export function getAgentTree(rootId: string): AgentState[] {
const result: AgentState[] = [];
const root = agents.find(a => a.id === rootId);
if (!root) return result;
result.push(root);
const children = agents.filter(a => a.parentId === rootId);
for (const child of children) {
result.push(...getAgentTree(child.id));
}
return result;
}

View file

@ -0,0 +1,28 @@
// Layout state management — Svelte 5 runes
// Phase 2: pane positions, resize, presets
export type LayoutPreset = '1-col' | '2-col' | '3-col' | '2x2' | 'master-stack';
export interface PaneState {
id: string;
sessionId: string;
row: number;
col: number;
rowSpan: number;
colSpan: number;
}
let activePreset = $state<LayoutPreset>('1-col');
let panes = $state<PaneState[]>([]);
export function getActivePreset() {
return activePreset;
}
export function setPreset(preset: LayoutPreset) {
activePreset = preset;
}
export function getPanes() {
return panes;
}

View file

@ -0,0 +1,26 @@
// Session state management — Svelte 5 runes
// Phase 4: full session CRUD, persistence
export type SessionType = 'terminal' | 'agent' | 'markdown';
export interface Session {
id: string;
type: SessionType;
title: string;
createdAt: number;
}
// Reactive session list
let sessions = $state<Session[]>([]);
export function getSessions() {
return sessions;
}
export function addSession(session: Session) {
sessions.push(session);
}
export function removeSession(id: string) {
sessions = sessions.filter(s => s.id !== id);
}

View file

@ -0,0 +1,52 @@
/* Catppuccin Mocha — https://catppuccin.com/palette */
:root {
--ctp-rosewater: #f5e0dc;
--ctp-flamingo: #f2cdcd;
--ctp-pink: #f5c2e7;
--ctp-mauve: #cba6f7;
--ctp-red: #f38ba8;
--ctp-maroon: #eba0ac;
--ctp-peach: #fab387;
--ctp-yellow: #f9e2af;
--ctp-green: #a6e3a1;
--ctp-teal: #94e2d5;
--ctp-sky: #89dceb;
--ctp-sapphire: #74c7ec;
--ctp-blue: #89b4fa;
--ctp-lavender: #b4befe;
--ctp-text: #cdd6f4;
--ctp-subtext1: #bac2de;
--ctp-subtext0: #a6adc8;
--ctp-overlay2: #9399b2;
--ctp-overlay1: #7f849c;
--ctp-overlay0: #6c7086;
--ctp-surface2: #585b70;
--ctp-surface1: #45475a;
--ctp-surface0: #313244;
--ctp-base: #1e1e2e;
--ctp-mantle: #181825;
--ctp-crust: #11111b;
/* Semantic aliases */
--bg-primary: var(--ctp-base);
--bg-secondary: var(--ctp-mantle);
--bg-tertiary: var(--ctp-crust);
--bg-surface: var(--ctp-surface0);
--bg-surface-hover: var(--ctp-surface1);
--text-primary: var(--ctp-text);
--text-secondary: var(--ctp-subtext1);
--text-muted: var(--ctp-overlay1);
--border: var(--ctp-surface1);
--accent: var(--ctp-blue);
--accent-hover: var(--ctp-sapphire);
--success: var(--ctp-green);
--warning: var(--ctp-yellow);
--error: var(--ctp-red);
/* Layout */
--sidebar-width: 260px;
--right-panel-width: 380px;
--pane-header-height: 32px;
--pane-gap: 2px;
--border-radius: 4px;
}

9
v2/src/main.ts Normal file
View file

@ -0,0 +1,9 @@
import { mount } from 'svelte'
import './app.css'
import App from './App.svelte'
const app = mount(App, {
target: document.getElementById('app')!,
})
export default app