feat: add optimistic locking for bttask and error classification
Version column in tasks table with WHERE id=? AND version=? guard. Conflict detection in TaskBoardTab. error-classifier.ts: 6 error types with actionable messages and retry logic. UsageMeter.svelte.
This commit is contained in:
parent
0fe43de357
commit
3cb65fd5e5
10 changed files with 763 additions and 32 deletions
|
|
@ -16,6 +16,8 @@
|
|||
import type { SessionAnchor } from '../../types/anchors';
|
||||
|
||||
import AgentTree from './AgentTree.svelte';
|
||||
import UsageMeter from './UsageMeter.svelte';
|
||||
import { notify } from '../../stores/notifications.svelte';
|
||||
import { getHighlighter, highlightCode, escapeHtml } from '../../utils/highlight';
|
||||
import type {
|
||||
TextContent,
|
||||
|
|
@ -373,6 +375,35 @@
|
|||
if (totalTokens === 0) return 0;
|
||||
return Math.min(100, Math.round((totalTokens / DEFAULT_CONTEXT_LIMIT) * 100));
|
||||
});
|
||||
|
||||
// Context meter color class based on thresholds
|
||||
let contextColorClass = $derived.by(() => {
|
||||
if (contextPercent >= 90) return 'context-critical';
|
||||
if (contextPercent >= 75) return 'context-high';
|
||||
if (contextPercent >= 50) return 'context-medium';
|
||||
return '';
|
||||
});
|
||||
|
||||
// Session burn rate ($/hr)
|
||||
let burnRatePerHr = $derived.by(() => {
|
||||
if (!session || session.durationMs <= 0 || session.costUsd <= 0) return 0;
|
||||
return (session.costUsd / session.durationMs) * 3_600_000;
|
||||
});
|
||||
|
||||
// 90% context warning (fire once per session)
|
||||
let contextWarningFired = $state(false);
|
||||
$effect(() => {
|
||||
if (contextPercent >= 90 && !contextWarningFired && session?.status === 'running') {
|
||||
contextWarningFired = true;
|
||||
notify('warning', `Context usage at ${contextPercent}% — approaching model limit`);
|
||||
}
|
||||
});
|
||||
// Reset warning tracker when session changes
|
||||
$effect(() => {
|
||||
if (session?.id) {
|
||||
contextWarningFired = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="agent-pane" data-testid="agent-pane" data-agent-status={session?.status ?? 'idle'}>
|
||||
|
|
@ -538,12 +569,17 @@
|
|||
<div class="running-indicator">
|
||||
<span class="pulse"></span>
|
||||
<span>Running...</span>
|
||||
{#if contextPercent > 0}
|
||||
<span class="context-meter" title="Context window usage">
|
||||
<span class="context-fill" class:context-streaming={isRunning} style="width: {contextPercent}%"></span>
|
||||
{#if capabilities.supportsCost && (session.inputTokens > 0 || session.outputTokens > 0)}
|
||||
<UsageMeter inputTokens={session.inputTokens} outputTokens={session.outputTokens} contextLimit={DEFAULT_CONTEXT_LIMIT} />
|
||||
{:else if contextPercent > 0}
|
||||
<span class="context-meter {contextColorClass}" title="Context window usage">
|
||||
<span class="context-fill context-streaming" style="width: {contextPercent}%"></span>
|
||||
<span class="context-label">{contextPercent}%</span>
|
||||
</span>
|
||||
{/if}
|
||||
{#if burnRatePerHr > 0}
|
||||
<span class="burn-rate" title="Current session burn rate">${burnRatePerHr.toFixed(2)}/hr</span>
|
||||
{/if}
|
||||
{#if !autoScroll}
|
||||
<button class="scroll-btn" onclick={() => { autoScroll = true; scrollContainer?.querySelector('#message-end')?.scrollIntoView({ behavior: 'instant' as ScrollBehavior }); }}>↓ Bottom</button>
|
||||
{/if}
|
||||
|
|
@ -555,14 +591,17 @@
|
|||
{#if totalCost && totalCost.costUsd > session.costUsd}
|
||||
<span class="total-cost">(total: ${totalCost.costUsd.toFixed(4)})</span>
|
||||
{/if}
|
||||
<span class="cost-detail">{session.inputTokens + session.outputTokens} tok</span>
|
||||
<span class="cost-detail">{(session.durationMs / 1000).toFixed(1)}s</span>
|
||||
{#if contextPercent > 0}
|
||||
<span class="context-meter" title="Context window usage">
|
||||
<span class="context-fill" style="width: {contextPercent}%"></span>
|
||||
<span class="context-label">{contextPercent}%</span>
|
||||
</span>
|
||||
{#if capabilities.supportsCost}
|
||||
<span class="cost-detail token-in" title="Input tokens">{session.inputTokens.toLocaleString()} in</span>
|
||||
<span class="cost-detail token-out" title="Output tokens">{session.outputTokens.toLocaleString()} out</span>
|
||||
{:else}
|
||||
<span class="cost-detail">{session.inputTokens + session.outputTokens} tok</span>
|
||||
{/if}
|
||||
<span class="cost-detail">{(session.durationMs / 1000).toFixed(1)}s</span>
|
||||
{#if burnRatePerHr > 0}
|
||||
<span class="burn-rate" title="Session average burn rate">${burnRatePerHr.toFixed(2)}/hr</span>
|
||||
{/if}
|
||||
<UsageMeter inputTokens={session.inputTokens} outputTokens={session.outputTokens} contextLimit={DEFAULT_CONTEXT_LIMIT} />
|
||||
{#if !autoScroll}
|
||||
<button class="scroll-btn" onclick={() => { autoScroll = true; scrollContainer?.querySelector('#message-end')?.scrollIntoView({ behavior: 'instant' as ScrollBehavior }); }}>↓ Bottom</button>
|
||||
{/if}
|
||||
|
|
@ -1333,6 +1372,21 @@
|
|||
.total-cost { color: var(--ctp-overlay1); font-size: 0.6875rem; }
|
||||
.error-bar { color: var(--ctp-red); }
|
||||
|
||||
.burn-rate {
|
||||
font-size: 0.625rem;
|
||||
font-family: var(--term-font-family, monospace);
|
||||
color: var(--ctp-peach);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.token-in { color: var(--ctp-blue); }
|
||||
.token-out { color: var(--ctp-green); }
|
||||
|
||||
/* Context meter threshold colors */
|
||||
.context-medium .context-fill { background: var(--ctp-yellow); }
|
||||
.context-high .context-fill { background: var(--ctp-peach); }
|
||||
.context-critical .context-fill { background: var(--ctp-red); }
|
||||
|
||||
/* === Session controls === */
|
||||
.session-controls {
|
||||
display: flex;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue