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)
This commit is contained in:
Hibryda 2026-03-06 15:09:52 +01:00
parent 1d028c67f7
commit f27543d8d8
5 changed files with 324 additions and 12 deletions

View file

@ -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 @@
</button>
</div>
{#if showTree && session}
<AgentTree {session} />
<AgentTree {session} onNodeClick={handleTreeNodeClick} />
{/if}
{/if}
<div class="messages" bind:this={scrollContainer} onscroll={handleScroll}>
{#each session.messages as msg (msg.id)}
<div class="message msg-{msg.type}">
<div class="message msg-{msg.type}" id="msg-{msg.id}">
{#if msg.type === 'init'}
<div class="msg-init">
<span class="label">Session started</span>
@ -241,6 +264,22 @@
<button class="scroll-btn" onclick={() => { autoScroll = true; scrollToBottom(); }}>Scroll to bottom</button>
{/if}
</div>
{#if session.sdkSessionId}
<div class="follow-up">
<input
type="text"
class="follow-up-input"
bind:value={followUpPrompt}
placeholder="Follow up..."
onkeydown={(e) => {
if (e.key === 'Enter' && followUpPrompt.trim()) {
startQuery(followUpPrompt, true);
}
}}
/>
<button class="follow-up-btn" onclick={() => startQuery(followUpPrompt, true)} disabled={!followUpPrompt.trim()}>Send</button>
</div>
{/if}
{:else if session.status === 'error'}
<div class="error-bar">
<span>Error: {session.error ?? 'Unknown'}</span>
@ -250,6 +289,22 @@
</button>
{/if}
</div>
{#if session.sdkSessionId}
<div class="follow-up">
<input
type="text"
class="follow-up-input"
bind:value={followUpPrompt}
placeholder="Retry or follow up..."
onkeydown={(e) => {
if (e.key === 'Enter' && followUpPrompt.trim()) {
startQuery(followUpPrompt, true);
}
}}
/>
<button class="follow-up-btn" onclick={() => startQuery(followUpPrompt, true)} disabled={!followUpPrompt.trim()}>Send</button>
</div>
{/if}
{/if}
</div>
{/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; }
</style>

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { buildAgentTree, type AgentTreeNode } from '../../utils/agent-tree';
import { buildAgentTree, subtreeCost, type AgentTreeNode } from '../../utils/agent-tree';
import type { AgentSession } from '../../stores/agents.svelte';
interface Props {
@ -19,7 +19,7 @@
// Layout constants
const NODE_W = 100;
const NODE_H = 32;
const NODE_H = 40;
const H_GAP = 24;
const V_GAP = 12;
@ -124,19 +124,30 @@
<!-- Status dot -->
<circle
cx={layout.x + 10}
cy={layout.y + NODE_H / 2}
cy={layout.y + NODE_H / 2 - 4}
r="3"
fill={statusColor(layout.node.status)}
/>
<!-- Label -->
<text
x={layout.x + 18}
y={layout.y + NODE_H / 2 + 1}
y={layout.y + NODE_H / 2 - 4}
fill="var(--text-primary)"
font-size="10"
font-family="var(--font-mono)"
dominant-baseline="middle"
>{truncateLabel(layout.node.label, 10)}</text>
<!-- Subtree cost -->
{#if subtreeCost(layout.node) > 0}
<text
x={layout.x + 18}
y={layout.y + NODE_H / 2 + 9}
fill="var(--ctp-yellow)"
font-size="8"
font-family="var(--font-mono)"
dominant-baseline="middle"
>${subtreeCost(layout.node).toFixed(4)}</text>
{/if}
</g>
<!-- Recurse children -->