From f27543d8d89f93ebfe2c52378e9565dc7844bfb0 Mon Sep 17 00:00:00 2001 From: Hibryda Date: Fri, 6 Mar 2026 15:09:52 +0100 Subject: [PATCH] feat(v2): add copy/paste, theme hot-swap, tree enhancements, session resume, drag-resize - TerminalPane: Ctrl+Shift+C/V copy/paste via attachCustomKeyEventHandler - TerminalPane: subscribe to onThemeChange() for live theme hot-swap - theme.svelte.ts: callback registry (onThemeChange) notifies listeners on setFlavor() - AgentPane: session resume with follow-up prompt and resume_session_id - AgentPane: tree node click scrolls to corresponding message (scrollIntoView) - AgentTree: subtree cost display below each node, NODE_H 32->40 - TilingGrid: pane drag-resize via splitter overlays with mouse drag (10-90% clamping) --- v2/src/lib/components/Agent/AgentPane.svelte | 101 ++++++++++- v2/src/lib/components/Agent/AgentTree.svelte | 19 +- .../lib/components/Layout/TilingGrid.svelte | 169 +++++++++++++++++- .../components/Terminal/TerminalPane.svelte | 27 ++- v2/src/lib/stores/theme.svelte.ts | 20 +++ 5 files changed, 324 insertions(+), 12 deletions(-) diff --git a/v2/src/lib/components/Agent/AgentPane.svelte b/v2/src/lib/components/Agent/AgentPane.svelte index d48de66..f92fb53 100644 --- a/v2/src/lib/components/Agent/AgentPane.svelte +++ b/v2/src/lib/components/Agent/AgentPane.svelte @@ -68,25 +68,37 @@ } }); - async function startQuery(text: string) { + let followUpPrompt = $state(''); + + async function startQuery(text: string, resume = false) { if (!text.trim()) return; const ready = await isAgentReady(); if (!ready) { - createAgentSession(sessionId, text); + if (!resume) createAgentSession(sessionId, text); const { updateAgentStatus } = await import('../../stores/agents.svelte'); updateAgentStatus(sessionId, 'error', 'Sidecar not ready — agent features unavailable'); return; } - createAgentSession(sessionId, text); + const resumeId = resume ? session?.sdkSessionId : undefined; + + if (!resume) { + createAgentSession(sessionId, text); + } else { + const { updateAgentStatus } = await import('../../stores/agents.svelte'); + updateAgentStatus(sessionId, 'starting'); + } + await queryAgent({ session_id: sessionId, prompt: text, cwd, max_turns: 50, + resume_session_id: resumeId, }); inputPrompt = ''; + followUpPrompt = ''; } function handleSubmit(e: Event) { @@ -123,6 +135,17 @@ autoScroll = scrollHeight - scrollTop - clientHeight < 50; } + function handleTreeNodeClick(nodeId: string) { + if (!scrollContainer || !session) return; + // Find the message whose tool_call has this toolUseId + const msg = session.messages.find( + m => m.type === 'tool_call' && (m.content as ToolCallContent).toolUseId === nodeId + ); + if (!msg) return; + autoScroll = false; + scrollContainer.querySelector('#msg-' + CSS.escape(msg.id))?.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + // Auto-scroll when new messages arrive $effect(() => { if (session?.messages.length) { @@ -172,12 +195,12 @@ {#if showTree && session} - + {/if} {/if}
{#each session.messages as msg (msg.id)} -
+
{#if msg.type === 'init'}
Session started @@ -241,6 +264,22 @@ {/if}
+ {#if session.sdkSessionId} + + {/if} {:else if session.status === 'error'}
Error: {session.error ?? 'Unknown'} @@ -250,6 +289,22 @@ {/if}
+ {#if session.sdkSessionId} + + {/if} {/if}
{/if} @@ -633,4 +688,40 @@ .done-bar { color: var(--ctp-green); } .error-bar { color: var(--ctp-red); } + + .follow-up { + display: flex; + gap: 6px; + padding: 4px 0 0; + } + + .follow-up-input { + flex: 1; + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: 3px; + color: var(--text-primary); + font-size: 12px; + padding: 4px 8px; + font-family: inherit; + } + + .follow-up-input:focus { + outline: none; + border-color: var(--accent); + } + + .follow-up-btn { + background: var(--accent); + color: var(--ctp-crust); + border: none; + border-radius: 3px; + padding: 4px 12px; + font-size: 11px; + font-weight: 600; + cursor: pointer; + } + + .follow-up-btn:hover { opacity: 0.9; } + .follow-up-btn:disabled { opacity: 0.4; cursor: not-allowed; } diff --git a/v2/src/lib/components/Agent/AgentTree.svelte b/v2/src/lib/components/Agent/AgentTree.svelte index 63daf48..80743be 100644 --- a/v2/src/lib/components/Agent/AgentTree.svelte +++ b/v2/src/lib/components/Agent/AgentTree.svelte @@ -1,5 +1,5 @@
{#if panes.length === 0}
@@ -91,12 +204,41 @@ {/if}
+ +{#if panes.length > 1 && gridEl} + {#each { length: colCount - 1 } as _, ci} + +
startColDrag(ci, e)} + >
+ {/each} + {#each { length: rowCount - 1 } as _, ri} + +
startRowDrag(ri, e)} + >
+ {/each} +{/if} + diff --git a/v2/src/lib/components/Terminal/TerminalPane.svelte b/v2/src/lib/components/Terminal/TerminalPane.svelte index 1d24693..6738046 100644 --- a/v2/src/lib/components/Terminal/TerminalPane.svelte +++ b/v2/src/lib/components/Terminal/TerminalPane.svelte @@ -4,7 +4,7 @@ import { CanvasAddon } from '@xterm/addon-canvas'; import { FitAddon } from '@xterm/addon-fit'; import { spawnPty, writePty, resizePty, killPty, onPtyData, onPtyExit } from '../../adapters/pty-bridge'; - import { getXtermTheme } from '../../stores/theme.svelte'; + import { getXtermTheme, onThemeChange } from '../../stores/theme.svelte'; import type { UnlistenFn } from '@tauri-apps/api/event'; import '@xterm/xterm/css/xterm.css'; @@ -24,6 +24,7 @@ let unlistenData: UnlistenFn | null = null; let unlistenExit: UnlistenFn | null = null; let resizeObserver: ResizeObserver | null = null; + let unsubTheme: (() => void) | null = null; onMount(async () => { term = new Terminal({ @@ -59,6 +60,24 @@ onExit?.(); }); + // Copy/paste via Ctrl+Shift+C/V + term.attachCustomKeyEventHandler((e: KeyboardEvent) => { + if (e.ctrlKey && e.shiftKey && e.type === 'keydown') { + if (e.key === 'C') { + const selection = term.getSelection(); + if (selection) navigator.clipboard.writeText(selection); + return false; + } + if (e.key === 'V') { + navigator.clipboard.readText().then(text => { + if (text && ptyId) writePty(ptyId, text); + }); + return false; + } + } + return true; + }); + // Forward keyboard input to PTY term.onData((data) => { if (ptyId) writePty(ptyId, data); @@ -80,10 +99,16 @@ }, 100); }); resizeObserver.observe(terminalEl); + + // Hot-swap theme when flavor changes + unsubTheme = onThemeChange(() => { + term.options.theme = getXtermTheme(); + }); }); onDestroy(async () => { resizeObserver?.disconnect(); + unsubTheme?.(); unlistenData?.(); unlistenExit?.(); if (ptyId) { diff --git a/v2/src/lib/stores/theme.svelte.ts b/v2/src/lib/stores/theme.svelte.ts index 6690ed8..7b1b9c9 100644 --- a/v2/src/lib/stores/theme.svelte.ts +++ b/v2/src/lib/stores/theme.svelte.ts @@ -10,6 +10,17 @@ import { let currentFlavor = $state('mocha'); +/** Registered theme-change listeners */ +const themeChangeCallbacks = new Set<() => void>(); + +/** Register a callback invoked after every flavor change. Returns an unsubscribe function. */ +export function onThemeChange(callback: () => void): () => void { + themeChangeCallbacks.add(callback); + return () => { + themeChangeCallbacks.delete(callback); + }; +} + export function getCurrentFlavor(): CatppuccinFlavor { return currentFlavor; } @@ -22,6 +33,15 @@ export function getXtermTheme(): XtermTheme { export async function setFlavor(flavor: CatppuccinFlavor): Promise { currentFlavor = flavor; applyCssVariables(flavor); + // Notify all listeners (e.g. open xterm.js terminals) + for (const cb of themeChangeCallbacks) { + try { + cb(); + } catch (e) { + console.error('Theme change callback error:', e); + } + } + try { await setSetting('theme', flavor); } catch (e) {