feat: worker attach workspace console
This commit is contained in:
@@ -1,281 +0,0 @@
|
||||
<script lang="ts">
|
||||
import WorkspaceSidebar from '$lib/workspace-sidebar/WorkspaceSidebar.svelte';
|
||||
import type {
|
||||
CompanionMessageResponse,
|
||||
CompanionState,
|
||||
CompanionStatusResponse,
|
||||
CompanionTranscriptItem,
|
||||
CompanionTranscriptProjection,
|
||||
Diagnostic,
|
||||
WorkspaceResponse
|
||||
} from '$lib/workspace-sidebar/types';
|
||||
|
||||
let workspace = $state<WorkspaceResponse | null>(null);
|
||||
let workspaceError = $state<string | null>(null);
|
||||
let status = $state<CompanionStatusResponse | null>(null);
|
||||
let transcript = $state<CompanionTranscriptProjection | null>(null);
|
||||
let draft = $state('');
|
||||
let operationState = $state<CompanionState>('ready');
|
||||
let error = $state<string | null>(null);
|
||||
let timeoutNotice = $state<string | null>(null);
|
||||
let requestId = 0;
|
||||
|
||||
const currentPath = '/console';
|
||||
const messages = $derived(transcript?.items ?? []);
|
||||
const diagnostics = $derived(mergeDiagnostics(status?.diagnostics ?? [], transcript?.diagnostics ?? []));
|
||||
const sending = $derived(operationState === 'busy');
|
||||
const canSend = $derived(draft.trim().length > 0 && !sending);
|
||||
|
||||
async function getJson<T>(path: string): Promise<T> {
|
||||
const response = await fetch(path);
|
||||
if (!response.ok) {
|
||||
throw new Error(`GET ${path} failed: ${response.status}`);
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
async function postJson<T>(path: string, body: unknown, timeoutMs = 45_000): Promise<T> {
|
||||
const controller = new AbortController();
|
||||
const timeout = window.setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const response = await fetch(path, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal
|
||||
});
|
||||
if (!response.ok) {
|
||||
let detail = '';
|
||||
try {
|
||||
detail = await response.text();
|
||||
} catch {
|
||||
detail = '';
|
||||
}
|
||||
throw new Error(`POST ${path} failed: ${response.status}${detail ? ` ${detail}` : ''}`);
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
} catch (requestError) {
|
||||
if (requestError instanceof DOMException && requestError.name === 'AbortError') {
|
||||
operationState = 'timeout';
|
||||
timeoutNotice = 'Workspace server request timed out before a Companion response arrived.';
|
||||
}
|
||||
throw requestError;
|
||||
} finally {
|
||||
window.clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWorkspace() {
|
||||
workspaceError = null;
|
||||
try {
|
||||
workspace = await getJson<WorkspaceResponse>('/api/workspace');
|
||||
} catch (loadError) {
|
||||
workspaceError = loadError instanceof Error ? loadError.message : String(loadError);
|
||||
workspace = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCompanion() {
|
||||
error = null;
|
||||
timeoutNotice = null;
|
||||
try {
|
||||
const [nextStatus, nextTranscript] = await Promise.all([
|
||||
getJson<CompanionStatusResponse>('/api/companion/status'),
|
||||
getJson<CompanionTranscriptProjection>('/api/companion/transcript?limit=200')
|
||||
]);
|
||||
status = nextStatus;
|
||||
transcript = nextTranscript;
|
||||
operationState = nextStatus.state === 'error' ? 'error' : 'ready';
|
||||
} catch (loadError) {
|
||||
error = loadError instanceof Error ? loadError.message : String(loadError);
|
||||
operationState = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
const content = draft.trim();
|
||||
if (!content || sending) {
|
||||
return;
|
||||
}
|
||||
const currentRequest = ++requestId;
|
||||
error = null;
|
||||
timeoutNotice = null;
|
||||
operationState = 'busy';
|
||||
try {
|
||||
const response = await postJson<CompanionMessageResponse>('/api/companion/messages', { content });
|
||||
if (currentRequest !== requestId) {
|
||||
return;
|
||||
}
|
||||
operationState = response.state;
|
||||
transcript = response.transcript;
|
||||
if (response.worker || status) {
|
||||
status = {
|
||||
state: response.state === 'accepted' ? 'ready' : response.state,
|
||||
worker: response.worker ?? status?.worker ?? null,
|
||||
transport: status?.transport ?? {
|
||||
kind: 'providerless_backend_internal',
|
||||
completion: 'synchronous_request_response',
|
||||
limitation: 'Companion transport metadata was not available during this response.'
|
||||
},
|
||||
diagnostics: response.diagnostics
|
||||
};
|
||||
}
|
||||
if (response.state === 'accepted') {
|
||||
draft = '';
|
||||
operationState = 'ready';
|
||||
} else if (response.state === 'busy') {
|
||||
error = 'Companion is busy with another message.';
|
||||
} else if (response.state === 'rejected') {
|
||||
error = diagnosticsToText(response.diagnostics) || 'Companion rejected the message.';
|
||||
} else if (response.state === 'error') {
|
||||
error = diagnosticsToText(response.diagnostics) || 'Companion returned an error.';
|
||||
}
|
||||
} catch (sendError) {
|
||||
if (currentRequest !== requestId) {
|
||||
return;
|
||||
}
|
||||
if (operationState !== 'timeout') {
|
||||
operationState = 'error';
|
||||
}
|
||||
error = sendError instanceof Error ? sendError.message : String(sendError);
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelMessage() {
|
||||
++requestId;
|
||||
operationState = 'cancelled';
|
||||
try {
|
||||
const response = await postJson<CompanionMessageResponse>('/api/companion/cancel', { reason: 'browser_cancel' }, 10_000);
|
||||
transcript = response.transcript;
|
||||
status = status
|
||||
? { ...status, state: response.state, diagnostics: response.diagnostics }
|
||||
: status;
|
||||
} catch (cancelError) {
|
||||
error = cancelError instanceof Error ? cancelError.message : String(cancelError);
|
||||
operationState = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
function mergeDiagnostics(...groups: Diagnostic[][]): Diagnostic[] {
|
||||
return groups.flat();
|
||||
}
|
||||
|
||||
function diagnosticsToText(items: Diagnostic[]): string {
|
||||
return items.map((item) => `${item.severity}: ${item.message}`).join('\n');
|
||||
}
|
||||
|
||||
function itemClass(item: CompanionTranscriptItem): string {
|
||||
if (item.role === 'assistant') {
|
||||
return 'assistant';
|
||||
}
|
||||
if (item.role === 'user') {
|
||||
return 'user';
|
||||
}
|
||||
return 'system';
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
void loadWorkspace();
|
||||
void loadCompanion();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Companion Console · Yoi Workspace</title>
|
||||
<meta name="description" content="Workspace Companion Web Console MVP" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="workspace-layout">
|
||||
<WorkspaceSidebar {workspace} {workspaceError} {currentPath} />
|
||||
|
||||
<main class="shell console-shell">
|
||||
<section class="console-header card">
|
||||
<div>
|
||||
<p class="eyebrow">Backend-internal Companion</p>
|
||||
<h2>Companion Console</h2>
|
||||
<p class="section-note">
|
||||
Browser traffic stays behind Workspace API projections. No Worker socket, session path,
|
||||
runtime credential, or local session file is exposed to the frontend.
|
||||
</p>
|
||||
</div>
|
||||
<div class="console-status" data-state={operationState}>
|
||||
<span>{operationState}</span>
|
||||
{#if status?.worker}
|
||||
<small>{status.worker.label}</small>
|
||||
{:else}
|
||||
<small>worker pending</small>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#if status?.transport}
|
||||
<section class="card console-transport" aria-label="Companion transport">
|
||||
<div>
|
||||
<dt>Transport</dt>
|
||||
<dd>{status.transport.kind}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Completion</dt>
|
||||
<dd>{status.transport.completion}</dd>
|
||||
</div>
|
||||
<p>{status.transport.limitation}</p>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if error || timeoutNotice || diagnostics.length > 0}
|
||||
<section class="card console-diagnostics" aria-label="Companion diagnostics">
|
||||
{#if timeoutNotice}
|
||||
<p class="diagnostic warning">{timeoutNotice}</p>
|
||||
{/if}
|
||||
{#if error}
|
||||
<p class="diagnostic error">{error}</p>
|
||||
{/if}
|
||||
{#each diagnostics as diagnostic}
|
||||
<p class={`diagnostic ${diagnostic.severity}`}>{diagnostic.code}: {diagnostic.message}</p>
|
||||
{/each}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<section class="card transcript-card" aria-label="Companion transcript">
|
||||
<div class="runtime-heading">
|
||||
<h3>Transcript</h3>
|
||||
<span>{transcript?.total_items ?? 0} items</span>
|
||||
</div>
|
||||
{#if messages.length === 0}
|
||||
<p class="empty-state">No Companion messages yet. Send a message to exercise the backend boundary.</p>
|
||||
{:else}
|
||||
<ol class="transcript-list">
|
||||
{#each messages as message (message.sequence)}
|
||||
<li class={`transcript-item ${itemClass(message)}`}>
|
||||
<div class="message-meta">
|
||||
<strong>{message.role}</strong>
|
||||
<span>{message.status}</span>
|
||||
<time datetime={message.created_at}>{message.created_at}</time>
|
||||
</div>
|
||||
<p>{message.content}</p>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<form class="card composer-card" onsubmit={sendMessage}>
|
||||
<label for="companion-message">Message Companion</label>
|
||||
<textarea
|
||||
id="companion-message"
|
||||
bind:value={draft}
|
||||
rows="4"
|
||||
maxlength="8000"
|
||||
placeholder="Ask or note something for the backend Companion boundary…"
|
||||
disabled={sending}
|
||||
></textarea>
|
||||
<div class="composer-actions">
|
||||
<span>{draft.trim().length}/8000</span>
|
||||
<button type="button" class="secondary" onclick={loadCompanion} disabled={sending}>Refresh</button>
|
||||
<button type="button" class="secondary" onclick={cancelMessage} disabled={!sending}>Cancel</button>
|
||||
<button type="submit" disabled={!canSend}>Send</button>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
+419
@@ -0,0 +1,419 @@
|
||||
<script lang="ts">
|
||||
import WorkspaceSidebar from '$lib/workspace-sidebar/WorkspaceSidebar.svelte';
|
||||
import {
|
||||
projectConsole,
|
||||
workerConsolePath,
|
||||
type ConsoleLine
|
||||
} from '$lib/workspace-console/model';
|
||||
import type {
|
||||
ClientWorkerEventWsFrame,
|
||||
Diagnostic,
|
||||
Worker,
|
||||
WorkerInputResult,
|
||||
WorkerTranscriptProjection,
|
||||
WorkspaceResponse
|
||||
} from '$lib/workspace-sidebar/types';
|
||||
|
||||
type Props = {
|
||||
data: {
|
||||
runtimeId: string;
|
||||
workerId: string;
|
||||
};
|
||||
};
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
const runtimeId = $derived(data.runtimeId);
|
||||
const workerId = $derived(data.workerId);
|
||||
const currentPath = $derived(workerConsolePath(runtimeId, workerId));
|
||||
|
||||
let workspace = $state<WorkspaceResponse | null>(null);
|
||||
let workspaceError = $state<string | null>(null);
|
||||
let worker = $state<Worker | null>(null);
|
||||
let workerError = $state<string | null>(null);
|
||||
let transcript = $state<WorkerTranscriptProjection | null>(null);
|
||||
let transcriptError = $state<string | null>(null);
|
||||
let draft = $state('');
|
||||
let sending = $state(false);
|
||||
let sendError = $state<string | null>(null);
|
||||
let streamState = $state<'connecting' | 'open' | 'unsupported' | 'closed' | 'error'>('connecting');
|
||||
let streamDiagnostics = $state<Diagnostic[]>([]);
|
||||
let observedEvents = $state<Array<{ cursor: string; event: ClientWorkerEventWsFrame & { kind: 'event' } }>>([]);
|
||||
let reloadToken = 0;
|
||||
|
||||
const projection = $derived(
|
||||
projectConsole(
|
||||
transcript?.items ?? [],
|
||||
observedEvents.map((item) => ({ cursor: item.cursor, event: item.event.envelope.payload }))
|
||||
)
|
||||
);
|
||||
const lines = $derived(projection.lines);
|
||||
const diagnostics = $derived(
|
||||
mergeDiagnostics(worker?.diagnostics ?? [], transcript?.diagnostics ?? [], streamDiagnostics)
|
||||
);
|
||||
const canSend = $derived(Boolean(worker?.capabilities.can_accept_input) && draft.trim().length > 0 && !sending);
|
||||
const transcriptOnly = $derived(
|
||||
worker && !worker.capabilities.can_stream_events
|
||||
? 'Streaming observation is not available for this Worker. Console is using bounded transcript plus manual refresh.'
|
||||
: null
|
||||
);
|
||||
|
||||
async function getJson<T>(path: string): Promise<T> {
|
||||
const response = await fetch(path);
|
||||
if (!response.ok) {
|
||||
throw new Error(`GET ${path} failed: ${response.status}`);
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
async function postJson<T>(path: string, body: unknown, timeoutMs = 30_000): Promise<T> {
|
||||
const controller = new AbortController();
|
||||
const timeout = window.setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const response = await fetch(path, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal
|
||||
});
|
||||
if (!response.ok) {
|
||||
let detail = '';
|
||||
try {
|
||||
detail = await response.text();
|
||||
} catch {
|
||||
detail = '';
|
||||
}
|
||||
throw new Error(`POST ${path} failed: ${response.status}${detail ? ` ${detail}` : ''}`);
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
} finally {
|
||||
window.clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWorkspace() {
|
||||
workspaceError = null;
|
||||
try {
|
||||
workspace = await getJson<WorkspaceResponse>('/api/workspace');
|
||||
} catch (error) {
|
||||
workspaceError = error instanceof Error ? error.message : String(error);
|
||||
workspace = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWorker() {
|
||||
workerError = null;
|
||||
try {
|
||||
worker = await getJson<Worker>(
|
||||
`/api/runtimes/${encodeURIComponent(runtimeId)}/workers/${encodeURIComponent(workerId)}`
|
||||
);
|
||||
} catch (error) {
|
||||
workerError = error instanceof Error ? error.message : String(error);
|
||||
worker = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTranscript() {
|
||||
transcriptError = null;
|
||||
try {
|
||||
transcript = await getJson<WorkerTranscriptProjection>(
|
||||
`/api/runtimes/${encodeURIComponent(runtimeId)}/workers/${encodeURIComponent(workerId)}/transcript?limit=200`
|
||||
);
|
||||
} catch (error) {
|
||||
transcriptError = error instanceof Error ? error.message : String(error);
|
||||
transcript = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshConsole() {
|
||||
reloadToken += 1;
|
||||
await Promise.all([loadWorker(), loadTranscript()]);
|
||||
}
|
||||
|
||||
async function sendMessage(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
const content = draft.trim();
|
||||
if (!content || sending || !worker?.capabilities.can_accept_input) {
|
||||
return;
|
||||
}
|
||||
|
||||
sending = true;
|
||||
sendError = null;
|
||||
try {
|
||||
const result = await postJson<WorkerInputResult>(
|
||||
`/api/runtimes/${encodeURIComponent(runtimeId)}/workers/${encodeURIComponent(workerId)}/input`,
|
||||
{ kind: 'user', content }
|
||||
);
|
||||
if (result.state === 'accepted') {
|
||||
draft = '';
|
||||
} else {
|
||||
sendError = diagnosticsToText(result.diagnostics) || `Input was ${result.state}.`;
|
||||
}
|
||||
await loadTranscript();
|
||||
} catch (error) {
|
||||
sendError = error instanceof Error ? error.message : String(error);
|
||||
} finally {
|
||||
sending = false;
|
||||
}
|
||||
}
|
||||
|
||||
function connectObservation(target: Worker | null, token: number) {
|
||||
if (!target) {
|
||||
streamState = 'closed';
|
||||
return;
|
||||
}
|
||||
if (!target.capabilities.can_stream_events) {
|
||||
streamState = 'unsupported';
|
||||
streamDiagnostics = [
|
||||
{
|
||||
code: 'worker_streaming_unsupported',
|
||||
severity: 'info',
|
||||
message: 'This Worker does not expose backend-proxied observation streaming; transcript refresh remains available.'
|
||||
}
|
||||
];
|
||||
return;
|
||||
}
|
||||
|
||||
streamState = 'connecting';
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const ws = new WebSocket(
|
||||
`${protocol}//${window.location.host}/api/runtimes/${encodeURIComponent(runtimeId)}/workers/${encodeURIComponent(
|
||||
workerId
|
||||
)}/events/ws`
|
||||
);
|
||||
|
||||
ws.onopen = () => {
|
||||
if (token === reloadToken) {
|
||||
streamState = 'open';
|
||||
}
|
||||
};
|
||||
ws.onmessage = (message) => {
|
||||
if (token !== reloadToken) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const frame = JSON.parse(String(message.data)) as ClientWorkerEventWsFrame;
|
||||
if (frame.kind === 'event') {
|
||||
observedEvents = [
|
||||
...observedEvents,
|
||||
{
|
||||
cursor: frame.envelope.cursor,
|
||||
event: frame
|
||||
}
|
||||
].slice(-500);
|
||||
} else {
|
||||
streamDiagnostics = [
|
||||
...streamDiagnostics,
|
||||
{
|
||||
code: frame.diagnostic.code,
|
||||
severity: 'warning',
|
||||
message: frame.diagnostic.message
|
||||
}
|
||||
];
|
||||
}
|
||||
} catch (error) {
|
||||
streamDiagnostics = [
|
||||
...streamDiagnostics,
|
||||
{
|
||||
code: 'worker_observation_frame_invalid',
|
||||
severity: 'warning',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
];
|
||||
}
|
||||
};
|
||||
ws.onerror = () => {
|
||||
if (token === reloadToken) {
|
||||
streamState = 'error';
|
||||
streamDiagnostics = [
|
||||
...streamDiagnostics,
|
||||
{
|
||||
code: 'worker_observation_ws_error',
|
||||
severity: 'warning',
|
||||
message: 'Backend observation WebSocket failed; transcript refresh remains available.'
|
||||
}
|
||||
];
|
||||
}
|
||||
};
|
||||
ws.onclose = () => {
|
||||
if (token === reloadToken && streamState !== 'error') {
|
||||
streamState = 'closed';
|
||||
}
|
||||
};
|
||||
|
||||
return () => ws.close();
|
||||
}
|
||||
|
||||
function mergeDiagnostics(...groups: Diagnostic[][]): Diagnostic[] {
|
||||
return groups.flat();
|
||||
}
|
||||
|
||||
function diagnosticsToText(items: Diagnostic[]): string {
|
||||
return items.map((item) => `${item.severity}: ${item.message}`).join('\n');
|
||||
}
|
||||
|
||||
function lineClass(line: ConsoleLine): string {
|
||||
return line.error ? 'error' : line.kind;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
void loadWorkspace();
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
observedEvents = [];
|
||||
streamDiagnostics = [];
|
||||
void refreshConsole();
|
||||
});
|
||||
|
||||
$effect(() => connectObservation(worker, reloadToken));
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Worker Console · Yoi Workspace</title>
|
||||
<meta name="description" content="Worker attach console through Workspace Backend APIs" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="workspace-layout">
|
||||
<WorkspaceSidebar {workspace} {workspaceError} {currentPath} />
|
||||
|
||||
<main class="shell console-shell worker-console-shell">
|
||||
<section class="console-header card">
|
||||
<div>
|
||||
<p class="eyebrow">Worker attach Console</p>
|
||||
<h2>{worker?.label ?? workerId}</h2>
|
||||
<p class="section-note">
|
||||
Target authority is <code>runtime_id</code> + <code>worker_id</code>. Browser traffic uses Workspace Backend Worker APIs only;
|
||||
Runtime endpoints, credentials, socket paths, and session paths are not exposed.
|
||||
</p>
|
||||
</div>
|
||||
<div class="console-status-pill" class:warn={streamState !== 'open'}>
|
||||
{worker?.state ?? 'unknown'} · {worker?.status ?? 'loading'} · stream {streamState}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="console-grid">
|
||||
<article class="card transcript-card worker-transcript-card">
|
||||
<header class="transcript-toolbar">
|
||||
<div>
|
||||
<h3>Transcript and protocol events</h3>
|
||||
{#if projection.status || projection.usage}
|
||||
<p class="section-note">
|
||||
{#if projection.status}status: {projection.status}{/if}
|
||||
{#if projection.status && projection.usage} · {/if}
|
||||
{#if projection.usage}usage: {projection.usage}{/if}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<button type="button" class="secondary-button" onclick={refreshConsole}>Refresh</button>
|
||||
</header>
|
||||
|
||||
{#if workerError}
|
||||
<p class="error">{workerError}</p>
|
||||
{/if}
|
||||
{#if transcriptError}
|
||||
<p class="error">{transcriptError}</p>
|
||||
{/if}
|
||||
{#if transcriptOnly}
|
||||
<p class="section-note degrade-note">{transcriptOnly}</p>
|
||||
{/if}
|
||||
|
||||
{#if lines.length === 0}
|
||||
<p>No transcript items or observation events are available for this Worker yet.</p>
|
||||
{:else}
|
||||
<ol class="transcript worker-transcript">
|
||||
{#each lines as item}
|
||||
<li class:assistant={lineClass(item) === 'assistant'} class:user={lineClass(item) === 'user'} class:system={lineClass(item) !== 'assistant' && lineClass(item) !== 'user'} class:error-line={item.error}>
|
||||
<div class="message-heading">
|
||||
<span>{item.title}</span>
|
||||
<small>{item.source}{item.streaming ? ' · streaming' : ''}</small>
|
||||
</div>
|
||||
<pre>{item.body || '—'}</pre>
|
||||
{#if item.detail || item.cursor}
|
||||
<details class="message-detail">
|
||||
<summary>metadata</summary>
|
||||
{#if item.detail}<p>{item.detail}</p>{/if}
|
||||
{#if item.cursor}<code>{item.cursor}</code>{/if}
|
||||
</details>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
{/if}
|
||||
</article>
|
||||
|
||||
<aside class="console-side-card card">
|
||||
<h3>Worker detail</h3>
|
||||
{#if worker}
|
||||
<dl>
|
||||
<div>
|
||||
<dt>Runtime</dt>
|
||||
<dd><code>{worker.runtime_id}</code></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Worker</dt>
|
||||
<dd><code>{worker.worker_id}</code></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Host</dt>
|
||||
<dd><code>{worker.host_id}</code></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Role / profile</dt>
|
||||
<dd>{worker.role ?? 'unknown'} / {worker.profile ?? 'unknown'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Workspace</dt>
|
||||
<dd>{worker.workspace.visibility} · {worker.workspace.identity}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Implementation</dt>
|
||||
<dd>{worker.implementation.kind} · {worker.implementation.display_hint}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<details class="metadata-details">
|
||||
<summary>Capabilities</summary>
|
||||
<ul>
|
||||
<li>input: {worker.capabilities.can_accept_input ? 'available' : 'unsupported'}</li>
|
||||
<li>stream: {worker.capabilities.can_stream_events ? 'available' : 'unsupported'}</li>
|
||||
<li>bounded transcript: {worker.capabilities.can_read_bounded_transcript ? 'available' : 'unsupported'}</li>
|
||||
<li>stop: {worker.capabilities.can_stop ? 'available' : 'unsupported'}</li>
|
||||
<li>follow-up spawn: {worker.capabilities.can_spawn_followup ? 'available' : 'unsupported'}</li>
|
||||
</ul>
|
||||
</details>
|
||||
{:else if !workerError}
|
||||
<p>Loading Worker detail…</p>
|
||||
{/if}
|
||||
|
||||
{#if diagnostics.length > 0}
|
||||
<details class="metadata-details" open={streamState === 'error'}>
|
||||
<summary>Diagnostics ({diagnostics.length})</summary>
|
||||
<ul>
|
||||
{#each diagnostics as diagnostic}
|
||||
<li>
|
||||
<strong>{diagnostic.severity}</strong>
|
||||
<code>{diagnostic.code}</code>
|
||||
<span>{diagnostic.message}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</details>
|
||||
{/if}
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<form class="console-composer card" onsubmit={sendMessage}>
|
||||
<label for="worker-console-message">Send user input</label>
|
||||
<textarea
|
||||
id="worker-console-message"
|
||||
bind:value={draft}
|
||||
placeholder={worker?.capabilities.can_accept_input ? 'Message this Worker through the Backend input API…' : 'Input is unsupported for this Worker'}
|
||||
disabled={!worker?.capabilities.can_accept_input || sending}
|
||||
></textarea>
|
||||
<div class="composer-actions">
|
||||
<button type="submit" disabled={!canSend}>{sending ? 'Sending…' : 'Send'}</button>
|
||||
{#if sendError}<p class="error">{sendError}</p>{/if}
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
export function load(
|
||||
{ params }: { params: { runtimeId: string; workerId: string } },
|
||||
) {
|
||||
return {
|
||||
runtimeId: params.runtimeId,
|
||||
workerId: params.workerId,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user