feat(v2): add SSH management, ctx integration, themes, detached mode, auto-updater
SSH session management: - SshSession struct + ssh_sessions SQLite table in session.rs - CRUD Tauri commands (ssh_session_list/save/delete) in lib.rs - SshDialog.svelte (create/edit modal), SshSessionList.svelte (sidebar) - SSH pane routes to TerminalPane with shell=/usr/bin/ssh + args ctx context database integration: - ctx.rs: read-only CtxDb (SQLITE_OPEN_READ_ONLY for ~/.claude-context/context.db) - 5 Tauri commands (ctx_list_projects/get_context/get_shared/get_summaries/search) - ContextPane.svelte with project selector, tabs, search - ctx-bridge.ts adapter Catppuccin theme flavors (Latte/Frappe/Macchiato/Mocha): - themes.ts: all 4 palette definitions + buildXtermTheme/applyCssVariables - theme.svelte.ts: reactive store with SQLite persistence - SettingsDialog flavor dropdown, TerminalPane theme-aware Detached pane mode (pop-out windows): - detach.ts: isDetachedMode/getDetachedConfig from URL params - App.svelte: conditional rendering of single pane without chrome Other additions: - Shiki syntax highlighting (highlight.ts, lazy singleton, 13 languages) - Tauri auto-updater plugin (tauri-plugin-updater + updater.ts) - AgentPane markdown rendering with Shiki code highlighting - New deps: shiki, @tauri-apps/plugin-updater, tauri-plugin-updater
This commit is contained in:
parent
4f2614186d
commit
4db7ccff60
28 changed files with 2992 additions and 51 deletions
68
v2/src/lib/utils/detach.ts
Normal file
68
v2/src/lib/utils/detach.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// Detachable pane support — opens panes in separate OS windows
|
||||
// Uses Tauri's WebviewWindow API
|
||||
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
import type { Pane } from '../stores/layout.svelte';
|
||||
|
||||
let detachCounter = 0;
|
||||
|
||||
export async function detachPane(pane: Pane): Promise<void> {
|
||||
detachCounter++;
|
||||
const label = `detached-${detachCounter}`;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
detached: 'true',
|
||||
type: pane.type,
|
||||
title: pane.title,
|
||||
});
|
||||
|
||||
if (pane.shell) params.set('shell', pane.shell);
|
||||
if (pane.cwd) params.set('cwd', pane.cwd);
|
||||
if (pane.args) params.set('args', JSON.stringify(pane.args));
|
||||
if (pane.type === 'agent') params.set('sessionId', pane.id);
|
||||
|
||||
const webview = new WebviewWindow(label, {
|
||||
url: `index.html?${params.toString()}`,
|
||||
title: `BTerminal — ${pane.title}`,
|
||||
width: 800,
|
||||
height: 600,
|
||||
decorations: true,
|
||||
resizable: true,
|
||||
});
|
||||
|
||||
// Wait for the window to be created
|
||||
await webview.once('tauri://created', () => {
|
||||
// Window created successfully
|
||||
});
|
||||
|
||||
await webview.once('tauri://error', (e) => {
|
||||
console.error('Failed to create detached window:', e);
|
||||
});
|
||||
}
|
||||
|
||||
export function isDetachedMode(): boolean {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get('detached') === 'true';
|
||||
}
|
||||
|
||||
export function getDetachedConfig(): {
|
||||
type: string;
|
||||
title: string;
|
||||
shell?: string;
|
||||
cwd?: string;
|
||||
args?: string[];
|
||||
sessionId?: string;
|
||||
} | null {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('detached') !== 'true') return null;
|
||||
|
||||
const argsStr = params.get('args');
|
||||
return {
|
||||
type: params.get('type') ?? 'terminal',
|
||||
title: params.get('title') ?? 'Detached',
|
||||
shell: params.get('shell') ?? undefined,
|
||||
cwd: params.get('cwd') ?? undefined,
|
||||
args: argsStr ? JSON.parse(argsStr) : undefined,
|
||||
sessionId: params.get('sessionId') ?? undefined,
|
||||
};
|
||||
}
|
||||
51
v2/src/lib/utils/highlight.ts
Normal file
51
v2/src/lib/utils/highlight.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { createHighlighter, type Highlighter } from 'shiki';
|
||||
|
||||
let highlighter: Highlighter | null = null;
|
||||
let initPromise: Promise<Highlighter> | null = null;
|
||||
|
||||
// Use catppuccin-mocha theme (bundled with shiki)
|
||||
const THEME = 'catppuccin-mocha';
|
||||
|
||||
// Common languages to preload
|
||||
const LANGS = [
|
||||
'typescript', 'javascript', 'rust', 'python', 'bash',
|
||||
'json', 'html', 'css', 'svelte', 'sql', 'yaml', 'toml', 'markdown',
|
||||
];
|
||||
|
||||
export async function getHighlighter(): Promise<Highlighter> {
|
||||
if (highlighter) return highlighter;
|
||||
if (initPromise) return initPromise;
|
||||
|
||||
initPromise = createHighlighter({
|
||||
themes: [THEME],
|
||||
langs: LANGS,
|
||||
});
|
||||
|
||||
highlighter = await initPromise;
|
||||
return highlighter;
|
||||
}
|
||||
|
||||
export function highlightCode(code: string, lang: string): string {
|
||||
if (!highlighter) return escapeHtml(code);
|
||||
|
||||
try {
|
||||
const loadedLangs = highlighter.getLoadedLanguages();
|
||||
if (!loadedLangs.includes(lang as any)) {
|
||||
return escapeHtml(code);
|
||||
}
|
||||
|
||||
return highlighter.codeToHtml(code, {
|
||||
lang,
|
||||
theme: THEME,
|
||||
});
|
||||
} catch {
|
||||
return escapeHtml(code);
|
||||
}
|
||||
}
|
||||
|
||||
export function escapeHtml(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
32
v2/src/lib/utils/updater.ts
Normal file
32
v2/src/lib/utils/updater.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Auto-update checker — uses Tauri updater plugin
|
||||
// Requires signing key to be configured in tauri.conf.json before use
|
||||
|
||||
import { check } from '@tauri-apps/plugin-updater';
|
||||
|
||||
export async function checkForUpdates(): Promise<{
|
||||
available: boolean;
|
||||
version?: string;
|
||||
notes?: string;
|
||||
}> {
|
||||
try {
|
||||
const update = await check();
|
||||
if (update) {
|
||||
return {
|
||||
available: true,
|
||||
version: update.version,
|
||||
notes: update.body ?? undefined,
|
||||
};
|
||||
}
|
||||
return { available: false };
|
||||
} catch {
|
||||
// Updater not configured or network error — silently skip
|
||||
return { available: false };
|
||||
}
|
||||
}
|
||||
|
||||
export async function installUpdate(): Promise<void> {
|
||||
const update = await check();
|
||||
if (update) {
|
||||
await update.downloadAndInstall();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue