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:
parent
1d028c67f7
commit
f27543d8d8
5 changed files with 324 additions and 12 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue