web: show Worker tasks in Console
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
"dev": "deno run -A npm:vite@7.2.7 dev",
|
"dev": "deno run -A npm:vite@7.2.7 dev",
|
||||||
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787",
|
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787",
|
||||||
"check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json",
|
"check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json",
|
||||||
"test": "deno test --allow-read=src --allow-env=VSCODE_TEXTMATE_DEBUG src/lib/workspace/auth/model.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts src/lib/workspace/sidebar/repository-nav.test.ts src/lib/workspace/tickets/ticket-panel.test.ts",
|
"test": "deno test --allow-read=src --allow-env=VSCODE_TEXTMATE_DEBUG src/lib/workspace/auth/model.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/tasks.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts src/lib/workspace/sidebar/repository-nav.test.ts src/lib/workspace/tickets/ticket-panel.test.ts",
|
||||||
"build": "deno run -A npm:vite@7.2.7 build",
|
"build": "deno run -A npm:vite@7.2.7 build",
|
||||||
"preview": "deno run -A npm:vite@7.2.7 preview"
|
"preview": "deno run -A npm:vite@7.2.7 preview"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { taskCounts, type ConsoleTask } from "./tasks.ts";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
tasks: ConsoleTask[];
|
||||||
|
mode: "mini" | "pane";
|
||||||
|
};
|
||||||
|
|
||||||
|
let { tasks, mode }: Props = $props();
|
||||||
|
const counts = $derived(taskCounts(tasks));
|
||||||
|
const activeTasks = $derived(
|
||||||
|
tasks
|
||||||
|
.filter((task) => task.status === "pending" || task.status === "inprogress")
|
||||||
|
.slice(0, 3),
|
||||||
|
);
|
||||||
|
|
||||||
|
function mark(status: ConsoleTask["status"]): string {
|
||||||
|
switch (status) {
|
||||||
|
case "pending":
|
||||||
|
return "[ ]";
|
||||||
|
case "inprogress":
|
||||||
|
return "[~]";
|
||||||
|
case "completed":
|
||||||
|
return "[x]";
|
||||||
|
case "deleted":
|
||||||
|
return "[-]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if mode === "mini" && tasks.length > 0}
|
||||||
|
<section class="task-mini" aria-label="Worker task summary">
|
||||||
|
{#each activeTasks as task (task.taskid)}
|
||||||
|
<div class="task-mini-row">
|
||||||
|
<span class:inprogress={task.status === "inprogress"} class="task-mark">
|
||||||
|
{mark(task.status)}
|
||||||
|
</span>
|
||||||
|
<span class="task-subject">{task.subject.split("\n", 1)[0]}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<div class="task-summary">
|
||||||
|
{counts.total} task(s) — pending: {counts.pending}, inprogress: {counts.inprogress}, completed: {counts.completed}, deleted: {counts.deleted}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{:else if mode === "pane"}
|
||||||
|
<aside class="task-pane" aria-label="Worker tasks">
|
||||||
|
<h3>Tasks ({counts.total})</h3>
|
||||||
|
|
||||||
|
{#if tasks.length === 0}
|
||||||
|
<p class="task-empty">(no tasks)</p>
|
||||||
|
{:else}
|
||||||
|
<ol class="task-list">
|
||||||
|
{#each tasks as task (task.taskid)}
|
||||||
|
{@const subjectLines = task.subject.split("\n")}
|
||||||
|
<li>
|
||||||
|
<div class="task-heading">
|
||||||
|
<span class="task-id">#{task.taskid}</span>
|
||||||
|
<span
|
||||||
|
class:inprogress={task.status === "inprogress"}
|
||||||
|
class:completed={task.status === "completed"}
|
||||||
|
class:deleted={task.status === "deleted"}
|
||||||
|
class="task-mark"
|
||||||
|
>{mark(task.status)}</span>
|
||||||
|
<span>{subjectLines[0] ?? ""}</span>
|
||||||
|
</div>
|
||||||
|
{#each subjectLines.slice(1) as subjectLine}
|
||||||
|
<div class="task-continuation">{subjectLine}</div>
|
||||||
|
{/each}
|
||||||
|
{#if task.description}
|
||||||
|
{#each task.description.split("\n") as descriptionLine}
|
||||||
|
<div class="task-continuation task-description">{descriptionLine}</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ol>
|
||||||
|
{/if}
|
||||||
|
</aside>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.task-mini,
|
||||||
|
.task-pane {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-mini {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.1rem;
|
||||||
|
min-width: 0;
|
||||||
|
margin-bottom: -0.75rem;
|
||||||
|
padding-inline: 0.75rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-mini-row,
|
||||||
|
.task-heading {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-mark,
|
||||||
|
.task-id {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: var(--text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-mark.inprogress {
|
||||||
|
color: var(--warning);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-mark.completed {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-mark.deleted {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-subject,
|
||||||
|
.task-summary {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-summary,
|
||||||
|
.task-id,
|
||||||
|
.task-empty,
|
||||||
|
.task-description {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-pane {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding-inline: 1rem;
|
||||||
|
border-left: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-pane h3 {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-empty {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-heading {
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-continuation {
|
||||||
|
padding-left: 4ch;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-description {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.task-pane {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1126,3 +1126,68 @@ Deno.test("projectConsole relativizes known tool path displays from snapshot cwd
|
|||||||
"Grep should relativize line-start cwd paths and keep outside paths absolute",
|
"Grep should relativize line-start cwd paths and keep outside paths absolute",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test("projectConsole mirrors live TaskCreate and TaskUpdate calls", () => {
|
||||||
|
const projection = projectConsole([
|
||||||
|
{
|
||||||
|
eventId: "task-create",
|
||||||
|
event: {
|
||||||
|
event: "tool_call_done",
|
||||||
|
data: {
|
||||||
|
id: "task-create-1",
|
||||||
|
name: "TaskCreate",
|
||||||
|
arguments: JSON.stringify({
|
||||||
|
subject: "Port Tasks",
|
||||||
|
description: "Render active Worker tasks",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
} satisfies Event,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventId: "task-update",
|
||||||
|
event: {
|
||||||
|
event: "tool_call_done",
|
||||||
|
data: {
|
||||||
|
id: "task-update-1",
|
||||||
|
name: "TaskUpdate",
|
||||||
|
arguments: JSON.stringify({ taskid: 1, status: "inprogress" }),
|
||||||
|
},
|
||||||
|
} satisfies Event,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertEquals(projection.tasks, [{
|
||||||
|
taskid: 1,
|
||||||
|
status: "inprogress",
|
||||||
|
subject: "Port Tasks",
|
||||||
|
description: "Render active Worker tasks",
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("snapshot restores TaskStore state from system history", () => {
|
||||||
|
const taskSnapshot =
|
||||||
|
`[Session TaskStore snapshot]\n\n\`\`\`json\n{\n "tasks": [{"taskid": 3, "status": "pending", "subject": "Restored", "description": "From compaction"}]\n}\n\`\`\``;
|
||||||
|
const event = snapshotEvent("/repo");
|
||||||
|
if (event.event !== "snapshot") throw new Error("snapshot fixture expected");
|
||||||
|
event.data.entries = [{
|
||||||
|
kind: "segment_start",
|
||||||
|
ts: 1,
|
||||||
|
session_id: "00000000-0000-0000-0000-000000000001",
|
||||||
|
system_prompt: null,
|
||||||
|
config: {},
|
||||||
|
history: [{
|
||||||
|
kind: "message",
|
||||||
|
role: "system",
|
||||||
|
content: [{ kind: "text", text: taskSnapshot }],
|
||||||
|
}],
|
||||||
|
}];
|
||||||
|
|
||||||
|
const projection = projectConsole([{ eventId: "task-snapshot", event }]);
|
||||||
|
assertEquals(projection.tasks, [{
|
||||||
|
taskid: 3,
|
||||||
|
status: "pending",
|
||||||
|
subject: "Restored",
|
||||||
|
description: "From compaction",
|
||||||
|
}]);
|
||||||
|
assertEquals(projection.taskNextId, 4);
|
||||||
|
});
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import type {
|
|||||||
Segment,
|
Segment,
|
||||||
} from "$lib/generated/protocol";
|
} from "$lib/generated/protocol";
|
||||||
import { workspaceRoute } from "$lib/workspace/api/http";
|
import { workspaceRoute } from "$lib/workspace/api/http";
|
||||||
|
import {
|
||||||
|
applyTaskSnapshotText,
|
||||||
|
applyTaskToolCall,
|
||||||
|
type ConsoleTask,
|
||||||
|
} from "./tasks.ts";
|
||||||
|
|
||||||
export type ConsoleLineKind =
|
export type ConsoleLineKind =
|
||||||
| "user"
|
| "user"
|
||||||
@@ -60,6 +65,8 @@ export type ConsoleLine = {
|
|||||||
|
|
||||||
export type ConsoleProjection = {
|
export type ConsoleProjection = {
|
||||||
lines: ConsoleLine[];
|
lines: ConsoleLine[];
|
||||||
|
tasks: ConsoleTask[];
|
||||||
|
taskNextId: number;
|
||||||
status: string | null;
|
status: string | null;
|
||||||
usage: string | null;
|
usage: string | null;
|
||||||
cwd: string | null;
|
cwd: string | null;
|
||||||
@@ -138,6 +145,8 @@ export type ConsoleEventInput = {
|
|||||||
export function emptyConsoleProjection(): ConsoleProjection {
|
export function emptyConsoleProjection(): ConsoleProjection {
|
||||||
return {
|
return {
|
||||||
lines: [],
|
lines: [],
|
||||||
|
tasks: [],
|
||||||
|
taskNextId: 1,
|
||||||
status: null,
|
status: null,
|
||||||
usage: null,
|
usage: null,
|
||||||
cwd: null,
|
cwd: null,
|
||||||
@@ -189,6 +198,8 @@ export function applyProtocolEvent(
|
|||||||
): ConsoleProjection {
|
): ConsoleProjection {
|
||||||
const next: ConsoleProjection = {
|
const next: ConsoleProjection = {
|
||||||
lines: [...projection.lines],
|
lines: [...projection.lines],
|
||||||
|
tasks: [...projection.tasks],
|
||||||
|
taskNextId: projection.taskNextId,
|
||||||
status: projection.status,
|
status: projection.status,
|
||||||
usage: projection.usage,
|
usage: projection.usage,
|
||||||
cwd: projection.cwd,
|
cwd: projection.cwd,
|
||||||
@@ -209,6 +220,7 @@ export function applyProtocolEvent(
|
|||||||
break;
|
break;
|
||||||
case "system_item":
|
case "system_item":
|
||||||
next.lines.push(systemItemLine(envelope.eventId, event.data.item));
|
next.lines.push(systemItemLine(envelope.eventId, event.data.item));
|
||||||
|
applyTaskSystemItem(next, event.data.item);
|
||||||
break;
|
break;
|
||||||
case "text_delta":
|
case "text_delta":
|
||||||
appendStreaming(
|
appendStreaming(
|
||||||
@@ -267,6 +279,7 @@ export function applyProtocolEvent(
|
|||||||
argsStream: event.data.arguments,
|
argsStream: event.data.arguments,
|
||||||
state: "running",
|
state: "running",
|
||||||
});
|
});
|
||||||
|
applyTaskTool(next, event.data.name, event.data.arguments);
|
||||||
break;
|
break;
|
||||||
case "tool_result":
|
case "tool_result":
|
||||||
attachToolResult(next, envelope.eventId, event.data.id, {
|
attachToolResult(next, envelope.eventId, event.data.id, {
|
||||||
@@ -291,26 +304,36 @@ export function applyProtocolEvent(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "snapshot":
|
case "snapshot": {
|
||||||
next.status = event.data.status;
|
next.status = event.data.status;
|
||||||
next.cwd = event.data.greeting.cwd;
|
next.cwd = event.data.greeting.cwd;
|
||||||
next.lines = snapshotLinesFromEntries(
|
const snapshot = snapshotProjectionFromEntries(
|
||||||
envelope.eventId,
|
envelope.eventId,
|
||||||
event.data.entries,
|
event.data.entries,
|
||||||
next.cwd,
|
next.cwd,
|
||||||
);
|
);
|
||||||
|
next.lines = snapshot.lines;
|
||||||
|
next.tasks = snapshot.tasks;
|
||||||
|
next.taskNextId = snapshot.taskNextId;
|
||||||
for (const block of event.data.in_flight?.blocks ?? []) {
|
for (const block of event.data.in_flight?.blocks ?? []) {
|
||||||
next.lines.push(inFlightLine(envelope.eventId, block, next.cwd));
|
next.lines.push(inFlightLine(envelope.eventId, block, next.cwd));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "status":
|
case "status":
|
||||||
next.status = event.data.status;
|
next.status = event.data.status;
|
||||||
break;
|
break;
|
||||||
case "segment_rotated":
|
case "segment_rotated": {
|
||||||
next.lines = snapshotLinesFromEntries(envelope.eventId, [
|
const segment = snapshotProjectionFromEntries(
|
||||||
event.data.entry,
|
envelope.eventId,
|
||||||
], next.cwd);
|
[event.data.entry],
|
||||||
|
next.cwd,
|
||||||
|
);
|
||||||
|
next.lines = segment.lines;
|
||||||
|
next.tasks = segment.tasks;
|
||||||
|
next.taskNextId = segment.taskNextId;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "invoke_start":
|
case "invoke_start":
|
||||||
case "turn_start":
|
case "turn_start":
|
||||||
case "turn_end":
|
case "turn_end":
|
||||||
@@ -1111,13 +1134,47 @@ function usageText(
|
|||||||
} · cache ${data.cache_read_input_tokens ?? "unknown"}`;
|
} · cache ${data.cache_read_input_tokens ?? "unknown"}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function snapshotLinesFromEntries(
|
function applyTaskTool(
|
||||||
|
projection: ConsoleProjection,
|
||||||
|
name: string,
|
||||||
|
argumentsJson: string,
|
||||||
|
): void {
|
||||||
|
const state = applyTaskToolCall(
|
||||||
|
{ tasks: projection.tasks, nextTaskId: projection.taskNextId },
|
||||||
|
name,
|
||||||
|
argumentsJson,
|
||||||
|
);
|
||||||
|
projection.tasks = state.tasks;
|
||||||
|
projection.taskNextId = state.nextTaskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTaskSnapshot(projection: ConsoleProjection, text: string): void {
|
||||||
|
const state = applyTaskSnapshotText(
|
||||||
|
{ tasks: projection.tasks, nextTaskId: projection.taskNextId },
|
||||||
|
text,
|
||||||
|
);
|
||||||
|
projection.tasks = state.tasks;
|
||||||
|
projection.taskNextId = state.nextTaskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTaskSystemItem(
|
||||||
|
projection: ConsoleProjection,
|
||||||
|
item: unknown,
|
||||||
|
): void {
|
||||||
|
if (!isRecord(item)) return;
|
||||||
|
const body = item["body"];
|
||||||
|
if (typeof body === "string") applyTaskSnapshot(projection, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapshotProjectionFromEntries(
|
||||||
eventId: string,
|
eventId: string,
|
||||||
entries: unknown[],
|
entries: unknown[],
|
||||||
cwd: string | null,
|
cwd: string | null,
|
||||||
): ConsoleLine[] {
|
): ConsoleProjection {
|
||||||
const projection: ConsoleProjection = {
|
const projection: ConsoleProjection = {
|
||||||
lines: [],
|
lines: [],
|
||||||
|
tasks: [],
|
||||||
|
taskNextId: 1,
|
||||||
status: null,
|
status: null,
|
||||||
usage: null,
|
usage: null,
|
||||||
cwd,
|
cwd,
|
||||||
@@ -1126,7 +1183,7 @@ function snapshotLinesFromEntries(
|
|||||||
entries.forEach((entry, index) =>
|
entries.forEach((entry, index) =>
|
||||||
applyLogEntry(projection, `${eventId}-snapshot-${index}`, entry)
|
applyLogEntry(projection, `${eventId}-snapshot-${index}`, entry)
|
||||||
);
|
);
|
||||||
return projection.lines;
|
return projection;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyLogEntry(
|
function applyLogEntry(
|
||||||
@@ -1153,6 +1210,7 @@ function applyLogEntry(
|
|||||||
break;
|
break;
|
||||||
case "system_item":
|
case "system_item":
|
||||||
projection.lines.push(systemItemLine(eventId, entry["item"]));
|
projection.lines.push(systemItemLine(eventId, entry["item"]));
|
||||||
|
applyTaskSystemItem(projection, entry["item"]);
|
||||||
break;
|
break;
|
||||||
case "assistant_item":
|
case "assistant_item":
|
||||||
case "tool_result":
|
case "tool_result":
|
||||||
@@ -1224,6 +1282,9 @@ function applyLoggedItem(
|
|||||||
case "assistant":
|
case "assistant":
|
||||||
projection.lines.push(line(eventId, "assistant", "assistant", body));
|
projection.lines.push(line(eventId, "assistant", "assistant", body));
|
||||||
break;
|
break;
|
||||||
|
case "system":
|
||||||
|
applyTaskSnapshot(projection, body);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1238,19 +1299,23 @@ function applyLoggedItem(
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "tool_call":
|
case "tool_call": {
|
||||||
|
const name = stringField(item, "name") ?? "Tool";
|
||||||
|
const argumentsJson = stringField(item, "arguments") ?? "";
|
||||||
upsertToolCall(
|
upsertToolCall(
|
||||||
projection,
|
projection,
|
||||||
eventId,
|
eventId,
|
||||||
stringField(item, "call_id") ?? eventId,
|
stringField(item, "call_id") ?? eventId,
|
||||||
{
|
{
|
||||||
name: stringField(item, "name") ?? "Tool",
|
name,
|
||||||
arguments: stringField(item, "arguments") ?? "",
|
arguments: argumentsJson,
|
||||||
argsStream: stringField(item, "arguments") ?? "",
|
argsStream: argumentsJson,
|
||||||
state: "running",
|
state: "running",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
applyTaskTool(projection, name, argumentsJson);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "tool_result":
|
case "tool_result":
|
||||||
attachToolResult(
|
attachToolResult(
|
||||||
projection,
|
projection,
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
declare const Deno: {
|
||||||
|
test(name: string, fn: () => void): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
import {
|
||||||
|
applyTaskSnapshotText,
|
||||||
|
applyTaskToolCall,
|
||||||
|
emptyConsoleTaskState,
|
||||||
|
taskCounts,
|
||||||
|
} from "./tasks.ts";
|
||||||
|
|
||||||
|
function assert(condition: unknown, message: string): asserts condition {
|
||||||
|
if (!condition) throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
Deno.test("TaskCreate and TaskUpdate mirror the active TUI task store", () => {
|
||||||
|
let state = emptyConsoleTaskState();
|
||||||
|
state = applyTaskToolCall(
|
||||||
|
state,
|
||||||
|
"TaskCreate",
|
||||||
|
JSON.stringify({ subject: "first", description: "First detail" }),
|
||||||
|
);
|
||||||
|
state = applyTaskToolCall(
|
||||||
|
state,
|
||||||
|
"TaskCreate",
|
||||||
|
JSON.stringify({ subject: "second", description: "Second detail" }),
|
||||||
|
);
|
||||||
|
state = applyTaskToolCall(
|
||||||
|
state,
|
||||||
|
"TaskUpdate",
|
||||||
|
JSON.stringify({ taskid: 2, status: "inprogress", subject: "working" }),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(state.tasks.length === 2, "both active tasks should remain visible");
|
||||||
|
assert(
|
||||||
|
state.tasks[0].taskid === 1,
|
||||||
|
"TaskCreate should allocate sequential ids",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
state.tasks[1].status === "inprogress",
|
||||||
|
"TaskUpdate should change status",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
state.tasks[1].subject === "working",
|
||||||
|
"TaskUpdate should change subject",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
state.tasks[1].description === "Second detail",
|
||||||
|
"omitted fields should keep their prior values",
|
||||||
|
);
|
||||||
|
const counts = taskCounts(state.tasks);
|
||||||
|
assert(counts.pending === 1, "one task should be pending");
|
||||||
|
assert(counts.inprogress === 1, "one task should be in progress");
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("completed and deleted tasks remain in the TUI-style store", () => {
|
||||||
|
let state = emptyConsoleTaskState();
|
||||||
|
for (const subject of ["complete", "delete"]) {
|
||||||
|
state = applyTaskToolCall(
|
||||||
|
state,
|
||||||
|
"TaskCreate",
|
||||||
|
JSON.stringify({ subject, description: "detail" }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
state = applyTaskToolCall(
|
||||||
|
state,
|
||||||
|
"TaskUpdate",
|
||||||
|
JSON.stringify({ taskid: 1, status: "completed" }),
|
||||||
|
);
|
||||||
|
state = applyTaskToolCall(
|
||||||
|
state,
|
||||||
|
"TaskUpdate",
|
||||||
|
JSON.stringify({ taskid: 2, status: "deleted" }),
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
state.tasks.length === 2,
|
||||||
|
"the full TaskStore should retain inactive tasks",
|
||||||
|
);
|
||||||
|
const counts = taskCounts(state.tasks);
|
||||||
|
assert(counts.completed === 1, "completed tasks should remain counted");
|
||||||
|
assert(counts.deleted === 1, "deleted tasks should remain counted");
|
||||||
|
assert(counts.active === 0, "inactive tasks should not be shown as active");
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("session TaskStore snapshot replaces stale state and advances ids", () => {
|
||||||
|
let state = applyTaskToolCall(
|
||||||
|
emptyConsoleTaskState(),
|
||||||
|
"TaskCreate",
|
||||||
|
JSON.stringify({ subject: "stale", description: "" }),
|
||||||
|
);
|
||||||
|
const snapshot = `[Session TaskStore snapshot]
|
||||||
|
|
||||||
|
TaskStore: 1 active task(s) (pending: 0, inprogress: 1)
|
||||||
|
|
||||||
|
\`\`\`json
|
||||||
|
{
|
||||||
|
"tasks": [
|
||||||
|
{"taskid": 4, "status": "completed", "subject": "old", "description": ""},
|
||||||
|
{"taskid": 7, "status": "inprogress", "subject": "restored", "description": "detail"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
`;
|
||||||
|
state = applyTaskSnapshotText(state, snapshot);
|
||||||
|
assert(
|
||||||
|
state.tasks.length === 2,
|
||||||
|
"snapshot should restore the full TaskStore",
|
||||||
|
);
|
||||||
|
assert(state.tasks[0].taskid === 4, "completed tasks should remain restored");
|
||||||
|
assert(state.tasks[1].taskid === 7, "active tasks should remain restored");
|
||||||
|
assert(state.nextTaskId === 8, "next id should follow the highest task id");
|
||||||
|
state = applyTaskToolCall(
|
||||||
|
state,
|
||||||
|
"TaskCreate",
|
||||||
|
JSON.stringify({ subject: "next", description: "" }),
|
||||||
|
);
|
||||||
|
assert(state.tasks[2].taskid === 8, "new task should use the advanced id");
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("malformed task events are resilient no-ops", () => {
|
||||||
|
const state = emptyConsoleTaskState();
|
||||||
|
assert(
|
||||||
|
applyTaskToolCall(state, "TaskCreate", '{"subject":1}') === state,
|
||||||
|
"invalid TaskCreate should be ignored",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
applyTaskToolCall(state, "TaskUpdate", "not json") === state,
|
||||||
|
"invalid TaskUpdate should be ignored",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
applyTaskSnapshotText(state, "ordinary system message") === state,
|
||||||
|
"unrelated system messages should be ignored",
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
export type ConsoleTaskStatus =
|
||||||
|
| "pending"
|
||||||
|
| "inprogress"
|
||||||
|
| "completed"
|
||||||
|
| "deleted";
|
||||||
|
type SnapshotTask = ConsoleTask;
|
||||||
|
|
||||||
|
export type ConsoleTask = {
|
||||||
|
taskid: number;
|
||||||
|
status: ConsoleTaskStatus;
|
||||||
|
subject: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConsoleTaskState = {
|
||||||
|
tasks: ConsoleTask[];
|
||||||
|
nextTaskId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TaskUpdate = {
|
||||||
|
taskid: number;
|
||||||
|
status?: ConsoleTaskStatus;
|
||||||
|
subject?: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function emptyConsoleTaskState(): ConsoleTaskState {
|
||||||
|
return { tasks: [], nextTaskId: 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyTaskToolCall(
|
||||||
|
state: ConsoleTaskState,
|
||||||
|
name: string,
|
||||||
|
argumentsJson: string,
|
||||||
|
): ConsoleTaskState {
|
||||||
|
const argumentsValue = parseRecord(argumentsJson);
|
||||||
|
if (!argumentsValue) return state;
|
||||||
|
|
||||||
|
if (name === "TaskCreate") {
|
||||||
|
const subject = stringField(argumentsValue, "subject");
|
||||||
|
const description = stringField(argumentsValue, "description");
|
||||||
|
if (subject === undefined || description === undefined) return state;
|
||||||
|
return {
|
||||||
|
tasks: [
|
||||||
|
...state.tasks,
|
||||||
|
{
|
||||||
|
taskid: state.nextTaskId,
|
||||||
|
status: "pending",
|
||||||
|
subject,
|
||||||
|
description,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nextTaskId: state.nextTaskId + 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name !== "TaskUpdate") return state;
|
||||||
|
const update = taskUpdate(argumentsValue);
|
||||||
|
if (!update) return state;
|
||||||
|
const index = state.tasks.findIndex((task) => task.taskid === update.taskid);
|
||||||
|
if (index < 0) return state;
|
||||||
|
|
||||||
|
const current = state.tasks[index];
|
||||||
|
const status = update.status ?? current.status;
|
||||||
|
const tasks = [...state.tasks];
|
||||||
|
tasks[index] = {
|
||||||
|
taskid: current.taskid,
|
||||||
|
status,
|
||||||
|
subject: update.subject ?? current.subject,
|
||||||
|
description: update.description ?? current.description,
|
||||||
|
};
|
||||||
|
return { tasks, nextTaskId: state.nextTaskId };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyTaskSnapshotText(
|
||||||
|
state: ConsoleTaskState,
|
||||||
|
text: string,
|
||||||
|
): ConsoleTaskState {
|
||||||
|
const tasks = parseTaskSnapshotText(text);
|
||||||
|
if (!tasks) return state;
|
||||||
|
return {
|
||||||
|
tasks,
|
||||||
|
nextTaskId: Math.max(1, ...tasks.map((task) => task.taskid + 1)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseTaskSnapshotText(text: string): ConsoleTask[] | null {
|
||||||
|
if (!text.startsWith("[Session TaskStore snapshot]")) return null;
|
||||||
|
const startMarker = "```json\n";
|
||||||
|
const start = text.indexOf(startMarker);
|
||||||
|
if (start < 0) return null;
|
||||||
|
const jsonStart = start + startMarker.length;
|
||||||
|
const end = text.indexOf("\n```", jsonStart);
|
||||||
|
if (end < 0) return null;
|
||||||
|
|
||||||
|
let value: unknown;
|
||||||
|
try {
|
||||||
|
value = JSON.parse(text.slice(jsonStart, end));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!isRecord(value) || !Array.isArray(value.tasks)) return null;
|
||||||
|
|
||||||
|
const tasks: ConsoleTask[] = [];
|
||||||
|
for (const candidate of value.tasks) {
|
||||||
|
const task = taskEntry(candidate);
|
||||||
|
if (!task) return null;
|
||||||
|
tasks.push(task);
|
||||||
|
}
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function taskCounts(tasks: ConsoleTask[]): {
|
||||||
|
pending: number;
|
||||||
|
inprogress: number;
|
||||||
|
completed: number;
|
||||||
|
deleted: number;
|
||||||
|
active: number;
|
||||||
|
total: number;
|
||||||
|
} {
|
||||||
|
let pending = 0;
|
||||||
|
let inprogress = 0;
|
||||||
|
let completed = 0;
|
||||||
|
let deleted = 0;
|
||||||
|
for (const task of tasks) {
|
||||||
|
switch (task.status) {
|
||||||
|
case "pending":
|
||||||
|
pending += 1;
|
||||||
|
break;
|
||||||
|
case "inprogress":
|
||||||
|
inprogress += 1;
|
||||||
|
break;
|
||||||
|
case "completed":
|
||||||
|
completed += 1;
|
||||||
|
break;
|
||||||
|
case "deleted":
|
||||||
|
deleted += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
pending,
|
||||||
|
inprogress,
|
||||||
|
completed,
|
||||||
|
deleted,
|
||||||
|
active: pending + inprogress,
|
||||||
|
total: tasks.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function taskEntry(value: unknown): SnapshotTask | null {
|
||||||
|
if (!isRecord(value)) return null;
|
||||||
|
const taskid = integerField(value, "taskid");
|
||||||
|
const status = statusField(value, "status");
|
||||||
|
const subject = stringField(value, "subject");
|
||||||
|
const description = stringField(value, "description");
|
||||||
|
if (
|
||||||
|
taskid === undefined ||
|
||||||
|
status === undefined ||
|
||||||
|
subject === undefined ||
|
||||||
|
description === undefined
|
||||||
|
) return null;
|
||||||
|
return { taskid, status, subject, description };
|
||||||
|
}
|
||||||
|
|
||||||
|
function taskUpdate(value: Record<string, unknown>): TaskUpdate | null {
|
||||||
|
const taskid = integerField(value, "taskid");
|
||||||
|
if (taskid === undefined) return null;
|
||||||
|
const statusValue = value.status;
|
||||||
|
const status = statusValue === undefined
|
||||||
|
? undefined
|
||||||
|
: statusField(value, "status");
|
||||||
|
if (statusValue !== undefined && status === undefined) return null;
|
||||||
|
const subjectValue = value.subject;
|
||||||
|
const subject = subjectValue === undefined
|
||||||
|
? undefined
|
||||||
|
: stringField(value, "subject");
|
||||||
|
if (subjectValue !== undefined && subject === undefined) return null;
|
||||||
|
const descriptionValue = value.description;
|
||||||
|
const description = descriptionValue === undefined
|
||||||
|
? undefined
|
||||||
|
: stringField(value, "description");
|
||||||
|
if (descriptionValue !== undefined && description === undefined) return null;
|
||||||
|
return { taskid, status, subject, description };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRecord(value: string): Record<string, unknown> | null {
|
||||||
|
try {
|
||||||
|
const parsed: unknown = JSON.parse(value);
|
||||||
|
return isRecord(parsed) ? parsed : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringField(
|
||||||
|
value: Record<string, unknown>,
|
||||||
|
key: string,
|
||||||
|
): string | undefined {
|
||||||
|
return typeof value[key] === "string" ? value[key] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function integerField(
|
||||||
|
value: Record<string, unknown>,
|
||||||
|
key: string,
|
||||||
|
): number | undefined {
|
||||||
|
const field = value[key];
|
||||||
|
return typeof field === "number" && Number.isSafeInteger(field) && field >= 0
|
||||||
|
? field
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusField(
|
||||||
|
value: Record<string, unknown>,
|
||||||
|
key: string,
|
||||||
|
): ConsoleTaskStatus | undefined {
|
||||||
|
const field = value[key];
|
||||||
|
return field === "pending" ||
|
||||||
|
field === "inprogress" ||
|
||||||
|
field === "completed" ||
|
||||||
|
field === "deleted"
|
||||||
|
? field
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
@@ -775,3 +775,41 @@ Deno.test("Workspace Worker list and Console share the multiplexed connection",
|
|||||||
"A reused Console route should subscribe immediately on the live Workspace socket and install the new route Worker",
|
"A reused Console route should subscribe immediately on the live Workspace socket and install the new route Worker",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test("Web Console renders the client-projected Worker task store", async () => {
|
||||||
|
const consolePage = await Deno.readTextFile(
|
||||||
|
new URL(
|
||||||
|
"./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte",
|
||||||
|
import.meta.url,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const tasksComponent = await Deno.readTextFile(
|
||||||
|
new URL("./ConsoleTasks.svelte", import.meta.url),
|
||||||
|
);
|
||||||
|
const tasksModel = await Deno.readTextFile(
|
||||||
|
new URL("./tasks.ts", import.meta.url),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(
|
||||||
|
consolePage.includes("ConsoleTasks") &&
|
||||||
|
consolePage.includes("consoleProjection.tasks") &&
|
||||||
|
consolePage.includes("taskPaneOpen"),
|
||||||
|
"Console should expose the projected task store through its existing client model",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
tasksComponent.includes("[ ]") &&
|
||||||
|
tasksComponent.includes("[~]") &&
|
||||||
|
tasksComponent.includes("[x]") &&
|
||||||
|
tasksComponent.includes("[-]") &&
|
||||||
|
tasksComponent.includes("task(s) — pending:") &&
|
||||||
|
tasksComponent.includes("task.description"),
|
||||||
|
"Tasks UI should mirror the TUI status marks, summary, and descriptions",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
tasksModel.includes('name === "TaskCreate"') &&
|
||||||
|
tasksModel.includes('name !== "TaskUpdate"') &&
|
||||||
|
tasksModel.includes("[Session TaskStore snapshot]") &&
|
||||||
|
!consolePage.includes("fetchTasks"),
|
||||||
|
"Task projection should replay the protocol client-side without adding a task API",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
+53
-12
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { tick, untrack } from "svelte";
|
import { tick, untrack } from "svelte";
|
||||||
import ConsoleLineItem from "$lib/workspace/console/ConsoleLineItem.svelte";
|
import ConsoleLineItem from "$lib/workspace/console/ConsoleLineItem.svelte";
|
||||||
|
import ConsoleTasks from "$lib/workspace/console/ConsoleTasks.svelte";
|
||||||
import ConsoleTimeline from "$lib/workspace/console/ConsoleTimeline.svelte";
|
import ConsoleTimeline from "$lib/workspace/console/ConsoleTimeline.svelte";
|
||||||
import { chatSubmit } from "$lib/workspace/console/chat-submit";
|
import { chatSubmit } from "$lib/workspace/console/chat-submit";
|
||||||
import {
|
import {
|
||||||
@@ -114,6 +115,7 @@
|
|||||||
} | null = null;
|
} | null = null;
|
||||||
let streamDiagnostics = $state<Diagnostic[]>([]);
|
let streamDiagnostics = $state<Diagnostic[]>([]);
|
||||||
let workerDetailsOpen = $state(false);
|
let workerDetailsOpen = $state(false);
|
||||||
|
let taskPaneOpen = $state(false);
|
||||||
let timelineOpen = $state(false);
|
let timelineOpen = $state(false);
|
||||||
let consoleBodyElement: HTMLElement | null = null;
|
let consoleBodyElement: HTMLElement | null = null;
|
||||||
let composerTextareaElement: HTMLTextAreaElement | null = null;
|
let composerTextareaElement: HTMLTextAreaElement | null = null;
|
||||||
@@ -148,6 +150,7 @@
|
|||||||
const consoleTarget = $derived({ workspaceId, runtimeId, workerId });
|
const consoleTarget = $derived({ workspaceId, runtimeId, workerId });
|
||||||
|
|
||||||
const lines = $derived(consoleProjection.lines);
|
const lines = $derived(consoleProjection.lines);
|
||||||
|
const tasks = $derived(consoleProjection.tasks);
|
||||||
const timelineLayout = $derived(
|
const timelineLayout = $derived(
|
||||||
buildTimelineLayout(lines, eventObservedAtVersion, consoleScroll),
|
buildTimelineLayout(lines, eventObservedAtVersion, consoleScroll),
|
||||||
);
|
);
|
||||||
@@ -1086,6 +1089,7 @@
|
|||||||
const targetWorker = data.worker;
|
const targetWorker = data.worker;
|
||||||
const targetWorkerError = data.workerError;
|
const targetWorkerError = data.workerError;
|
||||||
resetObservedEvents();
|
resetObservedEvents();
|
||||||
|
taskPaneOpen = false;
|
||||||
worker = targetWorker;
|
worker = targetWorker;
|
||||||
workerError = targetWorkerError;
|
workerError = targetWorkerError;
|
||||||
liveWorkerState = targetWorker?.state ?? null;
|
liveWorkerState = targetWorker?.state ?? null;
|
||||||
@@ -1155,11 +1159,25 @@
|
|||||||
>
|
>
|
||||||
Rewind
|
Rewind
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="secondary-button"
|
||||||
|
aria-expanded={taskPaneOpen}
|
||||||
|
onclick={() => {
|
||||||
|
taskPaneOpen = !taskPaneOpen;
|
||||||
|
if (taskPaneOpen) workerDetailsOpen = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Tasks{tasks.length > 0 ? ` ${tasks.length}` : ""}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="secondary-button"
|
class="secondary-button"
|
||||||
aria-expanded={workerDetailsOpen}
|
aria-expanded={workerDetailsOpen}
|
||||||
onclick={() => (workerDetailsOpen = !workerDetailsOpen)}
|
onclick={() => {
|
||||||
|
workerDetailsOpen = !workerDetailsOpen;
|
||||||
|
if (workerDetailsOpen) taskPaneOpen = false;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Details
|
Details
|
||||||
</button>
|
</button>
|
||||||
@@ -1192,7 +1210,8 @@
|
|||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<section class:timeline-open={timelineOpen} class="console-body">
|
<div class:with-task-pane={taskPaneOpen} class="console-history">
|
||||||
|
<section class:timeline-open={timelineOpen} class="console-body">
|
||||||
<div class="console-timeline-spacer" aria-hidden="true"></div>
|
<div class="console-timeline-spacer" aria-hidden="true"></div>
|
||||||
<div class="timeline-fold-cell">
|
<div class="timeline-fold-cell">
|
||||||
<button
|
<button
|
||||||
@@ -1227,15 +1246,20 @@
|
|||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ConsoleTimeline
|
<ConsoleTimeline
|
||||||
marks={timelineMarks}
|
marks={timelineMarks}
|
||||||
thumbStyle={timelineThumb}
|
thumbStyle={timelineThumb}
|
||||||
axisStyle={timelineAxisStyle}
|
axisStyle={timelineAxisStyle}
|
||||||
expanded={timelineOpen}
|
expanded={timelineOpen}
|
||||||
onRailPointerDown={handleTimelineRailPointerDown}
|
onRailPointerDown={handleTimelineRailPointerDown}
|
||||||
onMarkClick={jumpToTimelineMark}
|
onMarkClick={jumpToTimelineMark}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{#if taskPaneOpen}
|
||||||
|
<ConsoleTasks {tasks} mode="pane" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if workerDetailsOpen}
|
{#if workerDetailsOpen}
|
||||||
<aside class="console-side-panel" aria-label="Worker detail">
|
<aside class="console-side-panel" aria-label="Worker detail">
|
||||||
@@ -1320,6 +1344,8 @@
|
|||||||
</aside>
|
</aside>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<ConsoleTasks {tasks} mode="mini" />
|
||||||
|
|
||||||
<form class="console-composer card" onsubmit={sendMessage}>
|
<form class="console-composer card" onsubmit={sendMessage}>
|
||||||
<div class="composer-input-shell">
|
<div class="composer-input-shell">
|
||||||
<textarea
|
<textarea
|
||||||
@@ -1406,7 +1432,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.worker-console-shell > .console-body {
|
.worker-console-shell > .console-history {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -1478,6 +1504,17 @@
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.console-history {
|
||||||
|
display: grid;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-history.with-task-pane {
|
||||||
|
grid-template-columns: minmax(0, 2fr) minmax(18rem, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
.console-body {
|
.console-body {
|
||||||
--console-timeline-width: 12rem;
|
--console-timeline-width: 12rem;
|
||||||
--console-timeline-fold-width: 2.25rem;
|
--console-timeline-fold-width: 2.25rem;
|
||||||
@@ -1734,6 +1771,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 960px) {
|
@media (max-width: 960px) {
|
||||||
|
.console-history.with-task-pane {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
.console-header {
|
.console-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user