feat: expose worker model and context metadata
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -200,21 +200,42 @@ export type SubmissionDisposition = "started" | "queued";
|
||||
|
||||
export type SessionSnapshot = { pending_submissions: PendingSubmissionsSnapshot, entries: Array<SessionSnapshotEntry>, };
|
||||
|
||||
export type ReasoningConfig = { "kind": "effort", effort: string, } | { "kind": "budget_tokens", budget_tokens: number, };
|
||||
|
||||
export type ContextTokenSource = "measured" | "estimated";
|
||||
|
||||
export type ContextUsage = { tokens: number, source: ContextTokenSource, };
|
||||
|
||||
export type Greeting = { worker_name: string, cwd: string, provider: string, model: string,
|
||||
/**
|
||||
* Effective reasoning control. Absent means unspecified or unavailable.
|
||||
*/
|
||||
reasoning?: ReasoningConfig | null, scope_summary: string, tools: Array<string>,
|
||||
/**
|
||||
* Resolved effective model context window in tokens. Legacy producers use
|
||||
* zero when this is unavailable; clients must treat that as unknown.
|
||||
*/
|
||||
context_window: number,
|
||||
/**
|
||||
* Legacy numeric mirror retained for mixed-version clients.
|
||||
*/
|
||||
context_tokens: number,
|
||||
/**
|
||||
* Typed current-context value. Older snapshots omit it; clients may fall
|
||||
* back to `context_tokens` only when `context_window` is known.
|
||||
*/
|
||||
context_usage?: ContextUsage | null, };
|
||||
|
||||
export type InternalWorkerKind = "sub_worker" | { "service": { kind: string, } };
|
||||
|
||||
export type InternalWorkerRef = { session_id: string, name: string, parent_session_id?: string | null, kind: InternalWorkerKind, };
|
||||
|
||||
export type InternalWorkerSnapshot = { worker: InternalWorkerRef, revision: number, session: SessionSnapshot, status: WorkerStatus, error?: string | null, in_flight?: InFlightSnapshot, internal_workers?: Array<InternalWorkerSnapshot>, };
|
||||
|
||||
export type Greeting = { worker_name: string, cwd: string, provider: string, model: string, scope_summary: string, tools: Array<string>,
|
||||
export type InternalWorkerSnapshot = { worker: InternalWorkerRef, revision: number, session: SessionSnapshot,
|
||||
/**
|
||||
* Model context window in tokens. Always filled by the Worker greeting.
|
||||
* Public execution metadata for this child. Older snapshots and service
|
||||
* workers may omit it; clients must never substitute the parent's values.
|
||||
*/
|
||||
context_window: number,
|
||||
/**
|
||||
* Estimated current session context tokens at connect time.
|
||||
*/
|
||||
context_tokens: number, };
|
||||
greeting?: Greeting | null, status: WorkerStatus, error?: string | null, in_flight?: InFlightSnapshot, internal_workers?: Array<InternalWorkerSnapshot>, };
|
||||
|
||||
export type Alert = { level: AlertLevel, source: AlertSource, message: string,
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { cleanup, render } from "@testing-library/svelte";
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import WorkerContextStatus from "./WorkerContextStatus.svelte";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
test("renders resolved model, reasoning, and measured context accessibly", () => {
|
||||
const view = render(WorkerContextStatus, {
|
||||
metadata: {
|
||||
model: "gpt-6-astra",
|
||||
reasoning: { kind: "effort", effort: "high" },
|
||||
contextWindow: 272_000,
|
||||
contextTokens: 142_000,
|
||||
contextSource: "measured",
|
||||
},
|
||||
});
|
||||
|
||||
const status = view.getByLabelText("Worker model and context");
|
||||
expect(status.textContent).toContain("gpt-6-astra · high");
|
||||
expect(status.textContent).toContain("Context 142.0k / 272.0k (52%)");
|
||||
});
|
||||
|
||||
test("renders honest unavailable state instead of a fabricated zero percent", async () => {
|
||||
const view = render(WorkerContextStatus, { metadata: null });
|
||||
expect(view.getByText("Model unavailable")).not.toBeNull();
|
||||
expect(view.getByText("Context unavailable")).not.toBeNull();
|
||||
expect(view.container.textContent).not.toContain("0%");
|
||||
|
||||
await view.rerender({
|
||||
metadata: {
|
||||
model: "claude",
|
||||
reasoning: { kind: "budget_tokens", budget_tokens: 8_192 },
|
||||
contextWindow: 64_000,
|
||||
contextTokens: 12_000,
|
||||
contextSource: "estimated",
|
||||
},
|
||||
});
|
||||
expect(view.container.textContent).toContain("claude · 8.2k token budget");
|
||||
expect(view.container.textContent).toContain("Context ~12.0k / 64.0k (19%)");
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
<script lang="ts">
|
||||
import type { ConsoleWorkerMetadata } from "./model";
|
||||
import {
|
||||
formatContextSummary,
|
||||
formatModelSummary,
|
||||
} from "./worker-metadata";
|
||||
|
||||
type Props = {
|
||||
metadata: ConsoleWorkerMetadata | null;
|
||||
};
|
||||
|
||||
let { metadata }: Props = $props();
|
||||
const modelSummary = $derived(formatModelSummary(metadata));
|
||||
const contextSummary = $derived(formatContextSummary(metadata));
|
||||
</script>
|
||||
|
||||
<div class="worker-context-status" aria-label="Worker model and context">
|
||||
<span class="worker-model" title={modelSummary}>{modelSummary}</span>
|
||||
<span class="separator" aria-hidden="true">|</span>
|
||||
<span class="worker-context" title={contextSummary}>{contextSummary}</span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.worker-context-status {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-compact);
|
||||
line-height: var(--line-height-compact);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.worker-model,
|
||||
.worker-context {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.worker-model {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.separator {
|
||||
flex: 0 0 auto;
|
||||
color: var(--line-strong);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.worker-context-status {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 0;
|
||||
}
|
||||
|
||||
.separator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.worker-model,
|
||||
.worker-context {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -60,7 +60,7 @@
|
||||
<span aria-hidden="true">・</span>
|
||||
<span>{requests} {requestLabel}</span>
|
||||
<span aria-hidden="true">|</span>
|
||||
<span>↑{formatRunTokens(uploadTokens)}/↓{formatRunTokens(outputTokens)}</span>
|
||||
<span>Run ↑{formatRunTokens(uploadTokens)}/↓{formatRunTokens(outputTokens)}</span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -210,6 +210,77 @@ function semanticOutput(lines: readonly ConsoleLine[]) {
|
||||
}));
|
||||
}
|
||||
|
||||
Deno.test("snapshot and usage keep current context separate from run traffic", () => {
|
||||
const snapshot = snapshotEvent("/repo") as SnapshotEvent;
|
||||
snapshot.data.greeting.model = "gpt-6-astra";
|
||||
snapshot.data.greeting.reasoning = { kind: "effort", effort: "high" };
|
||||
snapshot.data.greeting.context_window = 272_000;
|
||||
snapshot.data.greeting.context_tokens = 1;
|
||||
snapshot.data.greeting.context_usage = { tokens: 142_000, source: "estimated" };
|
||||
const projection = projectConsole([
|
||||
{ eventId: "snapshot", event: snapshot, observedAtMs: 1 },
|
||||
{ eventId: "usage", event: { event: "usage", data: {
|
||||
input_tokens: 150_000,
|
||||
cache_read_input_tokens: 140_000,
|
||||
output_tokens: 2_000,
|
||||
} }, observedAtMs: 2 },
|
||||
]);
|
||||
|
||||
assertEquals(projection.workerMetadata, {
|
||||
model: "gpt-6-astra",
|
||||
reasoning: { kind: "effort", effort: "high" },
|
||||
contextWindow: 272_000,
|
||||
contextTokens: 150_000,
|
||||
contextSource: "measured",
|
||||
});
|
||||
assertEquals(projection.runActivity.uploadTokens, 10_000);
|
||||
assertEquals(projection.runActivity.outputTokens, 2_000);
|
||||
});
|
||||
|
||||
Deno.test("snapshot metadata supports budget reasoning and legacy context fallback", () => {
|
||||
const snapshot = snapshotEvent("/repo") as SnapshotEvent;
|
||||
snapshot.data.greeting.reasoning = { kind: "budget_tokens", budget_tokens: 16_384 };
|
||||
delete snapshot.data.greeting.context_usage;
|
||||
const projection = projectConsole([{ eventId: "snapshot", event: snapshot }]);
|
||||
|
||||
assertEquals(projection.workerMetadata, {
|
||||
model: "model",
|
||||
reasoning: { kind: "budget_tokens", budget_tokens: 16_384 },
|
||||
contextWindow: 100,
|
||||
contextTokens: 20,
|
||||
contextSource: "estimated",
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("unknown and destructive boundaries never fabricate context percentages", () => {
|
||||
const retained = snapshotEvent("") as SnapshotEvent;
|
||||
retained.data.greeting.model = "";
|
||||
retained.data.greeting.context_window = 0;
|
||||
retained.data.greeting.context_tokens = 0;
|
||||
delete retained.data.greeting.context_usage;
|
||||
let projection = projectConsole([{ eventId: "retained", event: retained }]);
|
||||
assertEquals(projection.workerMetadata, {
|
||||
model: null,
|
||||
reasoning: null,
|
||||
contextWindow: null,
|
||||
contextTokens: null,
|
||||
contextSource: null,
|
||||
});
|
||||
|
||||
const live = snapshotEvent("/repo") as SnapshotEvent;
|
||||
live.data.greeting.context_usage = { tokens: 20, source: "measured" };
|
||||
projection = projectConsole([
|
||||
{ eventId: "live", event: live },
|
||||
{ eventId: "rewind", event: { event: "rewind_applied", data: {
|
||||
session: canonicalSession([]),
|
||||
input: [],
|
||||
summary: { truncated_to_entries: 0, discarded_entries: 1, tool_side_effect_warning: false },
|
||||
} } },
|
||||
]);
|
||||
assertEquals(projection.workerMetadata?.contextTokens, null);
|
||||
assertEquals(projection.workerMetadata?.contextSource, null);
|
||||
});
|
||||
|
||||
Deno.test("large paste segments project compact artifact metadata", () => {
|
||||
const body = "secret pasted body";
|
||||
const text = segmentsToText([{
|
||||
@@ -2977,6 +3048,18 @@ Deno.test("Internal Worker snapshot output continues without entering the parent
|
||||
worker,
|
||||
revision: 4,
|
||||
session: canonicalSession([]),
|
||||
greeting: {
|
||||
worker_name: "research",
|
||||
cwd: "/child",
|
||||
provider: "anthropic",
|
||||
model: "child-model",
|
||||
reasoning: { kind: "budget_tokens", budget_tokens: 8_192 },
|
||||
scope_summary: "child scope",
|
||||
tools: [],
|
||||
context_window: 64_000,
|
||||
context_tokens: 12_000,
|
||||
context_usage: { tokens: 12_000, source: "measured" },
|
||||
},
|
||||
status: "running",
|
||||
in_flight: {
|
||||
blocks: [{ kind: "text", text: "**chi", finished: false }],
|
||||
@@ -2991,6 +3074,14 @@ Deno.test("Internal Worker snapshot output continues without entering the parent
|
||||
projection.internalWorkers[0].console.lines.map((line) => line.body),
|
||||
["**chi"],
|
||||
);
|
||||
assertEquals(projection.workerMetadata?.model, "model");
|
||||
assertEquals(projection.internalWorkers[0].console.workerMetadata, {
|
||||
model: "child-model",
|
||||
reasoning: { kind: "budget_tokens", budget_tokens: 8_192 },
|
||||
contextWindow: 64_000,
|
||||
contextTokens: 12_000,
|
||||
contextSource: "measured",
|
||||
});
|
||||
|
||||
projection = projector.append([
|
||||
{
|
||||
|
||||
@@ -4,11 +4,13 @@ import type {
|
||||
CommandSnapshot,
|
||||
CommandStreamSlice,
|
||||
Event as ProtocolEvent,
|
||||
Greeting,
|
||||
InFlightBlock,
|
||||
InFlightCompaction,
|
||||
InFlightToolCallState,
|
||||
InternalWorkerRef,
|
||||
InternalWorkerSnapshot,
|
||||
ReasoningConfig,
|
||||
Segment,
|
||||
SessionSnapshotEntry,
|
||||
WorkerState,
|
||||
@@ -164,6 +166,14 @@ export function resolveConsoleWorkerView(
|
||||
return views.find((view) => view.sessionId === selectedSessionId) ?? views[0];
|
||||
}
|
||||
|
||||
export type ConsoleWorkerMetadata = {
|
||||
model: string | null;
|
||||
reasoning: ReasoningConfig | null;
|
||||
contextWindow: number | null;
|
||||
contextTokens: number | null;
|
||||
contextSource: "measured" | "estimated" | null;
|
||||
};
|
||||
|
||||
export type ConsoleProjection = {
|
||||
lines: ConsoleLine[];
|
||||
tasks: ConsoleTask[];
|
||||
@@ -174,6 +184,7 @@ export type ConsoleProjection = {
|
||||
usage: string | null;
|
||||
runActivity: RunActivityStats;
|
||||
cwd: string | null;
|
||||
workerMetadata: ConsoleWorkerMetadata | null;
|
||||
lastEventId: string | null;
|
||||
internalWorkers: InternalWorkerProjection[];
|
||||
/** Terminal child-session fences, reset only by an authoritative snapshot. */
|
||||
@@ -204,6 +215,57 @@ function workerStatusFromState(snapshot: WorkerStateSnapshot): WorkerStatus {
|
||||
return "running";
|
||||
}
|
||||
|
||||
function safeTokenCount(value: unknown, allowZero = true): number | null {
|
||||
return typeof value === "number" && Number.isSafeInteger(value) &&
|
||||
(allowZero ? value >= 0 : value > 0)
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
function metadataFromGreeting(greeting: Greeting): ConsoleWorkerMetadata {
|
||||
const contextWindow = safeTokenCount(greeting.context_window, false);
|
||||
const typedTokens = safeTokenCount(greeting.context_usage?.tokens);
|
||||
const legacyTokens = contextWindow === null
|
||||
? null
|
||||
: safeTokenCount(greeting.context_tokens);
|
||||
const model = greeting.model.trim();
|
||||
return {
|
||||
model: model.length > 0 ? model : null,
|
||||
reasoning: greeting.reasoning ?? null,
|
||||
contextWindow,
|
||||
contextTokens: typedTokens ?? legacyTokens,
|
||||
contextSource: typedTokens !== null
|
||||
? greeting.context_usage?.source ?? null
|
||||
: legacyTokens !== null
|
||||
? "estimated"
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
function metadataWithUsage(
|
||||
metadata: ConsoleWorkerMetadata | null,
|
||||
inputTokens: number | null | undefined,
|
||||
): ConsoleWorkerMetadata {
|
||||
const tokens = safeTokenCount(inputTokens);
|
||||
return {
|
||||
model: metadata?.model ?? null,
|
||||
reasoning: metadata?.reasoning ?? null,
|
||||
contextWindow: metadata?.contextWindow ?? null,
|
||||
contextTokens: tokens,
|
||||
contextSource: tokens === null ? null : "measured",
|
||||
};
|
||||
}
|
||||
|
||||
function metadataWithoutContext(
|
||||
metadata: ConsoleWorkerMetadata | null,
|
||||
): ConsoleWorkerMetadata | null {
|
||||
return metadata && {
|
||||
...metadata,
|
||||
contextTokens: null,
|
||||
contextSource: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function emptyConsoleProjection(): ConsoleProjection {
|
||||
return {
|
||||
lines: [],
|
||||
@@ -215,6 +277,7 @@ export function emptyConsoleProjection(): ConsoleProjection {
|
||||
usage: null,
|
||||
runActivity: emptyRunActivityStats(),
|
||||
cwd: null,
|
||||
workerMetadata: null,
|
||||
lastEventId: null,
|
||||
internalWorkers: [],
|
||||
removedInternalWorkers: {},
|
||||
@@ -779,6 +842,9 @@ function projectInternalWorkerSnapshot(
|
||||
cwd,
|
||||
);
|
||||
console.status = snapshot.status;
|
||||
console.workerMetadata = snapshot.greeting
|
||||
? metadataFromGreeting(snapshot.greeting)
|
||||
: null;
|
||||
appendSnapshotInFlightLines(
|
||||
console,
|
||||
snapshot.in_flight?.blocks ?? [],
|
||||
@@ -951,6 +1017,7 @@ export function applyProtocolEvent(
|
||||
envelope.observedAtMs ?? 0,
|
||||
),
|
||||
cwd: projection.cwd,
|
||||
workerMetadata: projection.workerMetadata,
|
||||
lastEventId: envelope.eventId,
|
||||
internalWorkers: [...projection.internalWorkers],
|
||||
removedInternalWorkers: { ...projection.removedInternalWorkers },
|
||||
@@ -1055,6 +1122,10 @@ export function applyProtocolEvent(
|
||||
break;
|
||||
case "usage":
|
||||
next.usage = usageText(event.data);
|
||||
next.workerMetadata = metadataWithUsage(
|
||||
next.workerMetadata,
|
||||
event.data.input_tokens,
|
||||
);
|
||||
break;
|
||||
case "error":
|
||||
if (hasDurableRunFailure(next, event.data.message)) break;
|
||||
@@ -1072,6 +1143,7 @@ export function applyProtocolEvent(
|
||||
break;
|
||||
case "snapshot": {
|
||||
next.cwd = event.data.greeting.cwd;
|
||||
next.workerMetadata = metadataFromGreeting(event.data.greeting);
|
||||
const snapshot = snapshotProjectionFromSession(
|
||||
envelope.eventId,
|
||||
event.data.session,
|
||||
@@ -1171,6 +1243,7 @@ export function applyProtocolEvent(
|
||||
next.lines = [...segment.lines, ...retainedErrors, ...retainedCompaction];
|
||||
next.tasks = segment.tasks;
|
||||
next.taskNextId = segment.taskNextId;
|
||||
next.workerMetadata = metadataWithoutContext(next.workerMetadata);
|
||||
break;
|
||||
}
|
||||
case "invoke_start":
|
||||
@@ -1196,21 +1269,25 @@ export function applyProtocolEvent(
|
||||
case "memory_worker":
|
||||
case "completions":
|
||||
case "rewind_targets":
|
||||
case "rewind_applied":
|
||||
case "workers_listed":
|
||||
case "worker_restored":
|
||||
case "peer_registered":
|
||||
// These are protocol/status/control events. TUI Console does not append
|
||||
// them to the conversation surface; browser Console should not either.
|
||||
break;
|
||||
case "rewind_applied":
|
||||
next.workerMetadata = metadataWithoutContext(next.workerMetadata);
|
||||
break;
|
||||
case "compaction_progress":
|
||||
return applyInFlightCompaction(next, event.data.compaction ?? null);
|
||||
case "compact_start":
|
||||
case "compact_done":
|
||||
case "compact_failed":
|
||||
// Durable lifecycle events are retained only as historical protocol
|
||||
// compatibility. Runtime progress is the sole Console block authority.
|
||||
break;
|
||||
case "compact_done":
|
||||
next.workerMetadata = metadataWithoutContext(next.workerMetadata);
|
||||
break;
|
||||
case "shutdown":
|
||||
next.status = "shutdown";
|
||||
break;
|
||||
@@ -2182,6 +2259,7 @@ function snapshotProjectionFromSession(
|
||||
usage: null,
|
||||
runActivity: emptyRunActivityStats(),
|
||||
cwd,
|
||||
workerMetadata: null,
|
||||
lastEventId: eventId,
|
||||
internalWorkers: [],
|
||||
removedInternalWorkers: {},
|
||||
|
||||
@@ -8,6 +8,11 @@ import {
|
||||
resolveCompactionStatusPresentation,
|
||||
visibleCompactionProgress,
|
||||
} from "./run-status.ts";
|
||||
import {
|
||||
formatContextSummary,
|
||||
formatModelSummary,
|
||||
formatReasoning,
|
||||
} from "./worker-metadata.ts";
|
||||
|
||||
function assertEquals(actual: unknown, expected: unknown): void {
|
||||
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
||||
@@ -17,6 +22,57 @@ function assertEquals(actual: unknown, expected: unknown): void {
|
||||
}
|
||||
}
|
||||
|
||||
Deno.test("worker metadata formats effort, budget, estimate, and unknown safely", () => {
|
||||
assertEquals(
|
||||
formatModelSummary({
|
||||
model: "gpt-6-astra",
|
||||
reasoning: { kind: "effort", effort: "high" },
|
||||
contextWindow: 272_000,
|
||||
contextTokens: 142_000,
|
||||
contextSource: "measured",
|
||||
}),
|
||||
"gpt-6-astra · high",
|
||||
);
|
||||
assertEquals(
|
||||
formatReasoning({ kind: "budget_tokens", budget_tokens: 16_384 }),
|
||||
"16.4k token budget",
|
||||
);
|
||||
assertEquals(
|
||||
formatContextSummary({
|
||||
model: "model",
|
||||
reasoning: null,
|
||||
contextWindow: 272_000,
|
||||
contextTokens: 142_000,
|
||||
contextSource: "estimated",
|
||||
}),
|
||||
"Context ~142.0k / 272.0k (52%)",
|
||||
);
|
||||
assertEquals(formatModelSummary(null), "Model unavailable");
|
||||
assertEquals(
|
||||
formatReasoning({ kind: "effort", effort: "" }),
|
||||
"reasoning unavailable",
|
||||
);
|
||||
assertEquals(
|
||||
formatReasoning({ kind: "budget_tokens", budget_tokens: Number.NaN }),
|
||||
"reasoning unavailable",
|
||||
);
|
||||
assertEquals(
|
||||
formatReasoning({ kind: "budget_tokens", budget_tokens: -1 }),
|
||||
"reasoning unavailable",
|
||||
);
|
||||
assertEquals(formatContextSummary(null), "Context unavailable");
|
||||
assertEquals(
|
||||
formatContextSummary({
|
||||
model: null,
|
||||
reasoning: null,
|
||||
contextWindow: 0,
|
||||
contextTokens: 0,
|
||||
contextSource: null,
|
||||
}),
|
||||
"Context unavailable",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("run activity follows TUI request and net-token accounting", () => {
|
||||
let stats = applyRunActivityEvent(
|
||||
emptyRunActivityStats(),
|
||||
|
||||
@@ -720,7 +720,8 @@ Deno.test("Worker Console renders Edit diffs without preformatted template gaps"
|
||||
Deno.test("Console spacing and text metrics use existing design tokens", async () => {
|
||||
const files = [
|
||||
"ComposerInput.svelte", "ConsoleLineItem.svelte", "RichMarkdown.svelte",
|
||||
"ConsoleTasks.svelte", "WorkerRunStatus.svelte", "ConsoleDisplayState.svelte",
|
||||
"ConsoleTasks.svelte", "WorkerRunStatus.svelte", "WorkerContextStatus.svelte",
|
||||
"ConsoleDisplayState.svelte",
|
||||
"ConsoleTurnNavigation.svelte",
|
||||
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
|
||||
];
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { ReasoningConfig } from "$lib/generated/protocol";
|
||||
import type { ConsoleWorkerMetadata } from "./model";
|
||||
import { formatRunTokens } from "./run-status";
|
||||
|
||||
export function formatReasoning(reasoning: ReasoningConfig | null): string {
|
||||
if (!reasoning) return "reasoning unavailable";
|
||||
if (reasoning.kind === "effort") {
|
||||
const effort = reasoning.effort.trim();
|
||||
return effort.length > 0 ? effort : "reasoning unavailable";
|
||||
}
|
||||
if (
|
||||
!Number.isSafeInteger(reasoning.budget_tokens) ||
|
||||
reasoning.budget_tokens <= 0
|
||||
) {
|
||||
return "reasoning unavailable";
|
||||
}
|
||||
return `${formatRunTokens(reasoning.budget_tokens)} token budget`;
|
||||
}
|
||||
|
||||
export function formatModelSummary(
|
||||
metadata: ConsoleWorkerMetadata | null,
|
||||
): string {
|
||||
if (!metadata?.model) return "Model unavailable";
|
||||
return `${metadata.model} · ${formatReasoning(metadata.reasoning)}`;
|
||||
}
|
||||
|
||||
export function formatContextSummary(
|
||||
metadata: ConsoleWorkerMetadata | null,
|
||||
): string {
|
||||
const used = metadata?.contextTokens;
|
||||
const limit = metadata?.contextWindow;
|
||||
if (
|
||||
used === null || used === undefined || limit === null ||
|
||||
limit === undefined || !Number.isSafeInteger(used) ||
|
||||
!Number.isSafeInteger(limit) || used < 0 || limit <= 0
|
||||
) return "Context unavailable";
|
||||
const percent = Math.round((used / limit) * 100);
|
||||
if (!Number.isFinite(percent)) return "Context unavailable";
|
||||
const estimate = metadata?.contextSource === "estimated" ? "~" : "";
|
||||
return `Context ${estimate}${formatRunTokens(used)} / ${
|
||||
formatRunTokens(limit)
|
||||
} (${percent}%)`;
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
} from "$lib/workspace/console/composer-command";
|
||||
import { FileCompletions } from "$lib/workspace/console/file-completions";
|
||||
import WorkerRunStatus from "$lib/workspace/console/WorkerRunStatus.svelte";
|
||||
import WorkerContextStatus from "$lib/workspace/console/WorkerContextStatus.svelte";
|
||||
import { resolveWorkerControlShortcut } from "$lib/workspace/console/worker-control-shortcuts";
|
||||
import {
|
||||
consoleWorkerViews,
|
||||
@@ -1651,6 +1652,7 @@
|
||||
|
||||
<div class="console-shell worker-console-shell">
|
||||
<section class="console-header card" aria-label="Worker controls">
|
||||
<WorkerContextStatus metadata={selectedConsoleProjection.workerMetadata} />
|
||||
<div class="console-header-actions">
|
||||
<div
|
||||
class="console-view-modes"
|
||||
@@ -2119,8 +2121,9 @@
|
||||
|
||||
.console-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
@@ -2133,6 +2136,7 @@
|
||||
|
||||
.console-header-actions {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
@@ -2670,7 +2674,12 @@
|
||||
}
|
||||
|
||||
.console-header {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.console-header-actions {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user