mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(workboard): wire task-backed board runs
Summary: - remove the leftover Workboard mini-game/prototype surface - wire autonomous Workboard card starts through Gateway task-backed agent runs - reconcile card task/session lifecycle for starts, stops, stale tasks, reassignment, and default-agent sessions - clarify dispatch summary copy and admin-only model override behavior Verification: - autoreview clean: no accepted/actionable findings - targeted Workboard/UI Vitest: 72 tests passed - Workboard extension Vitest: 9 tests passed - UI build, docs list, docs format, diff check, and focused oxlint passed - PR CI checks: 50 ok, 0 attention - Testbox tbx_01kt07mk5sjyj2whjq2sc967hg: pnpm verify check phase passed; broad test phase exposed unrelated latest-main failures/stalls in memory, Codex app-server, provider timeout, command daemon env, Telegram worker OOM, and gateway-client timeout suites
This commit is contained in:
@@ -99,7 +99,10 @@ openclaw workboard dispatch --url http://127.0.0.1:18789 --token "$OPENCLAW_GATE
|
||||
|
||||
`dispatch` first calls the running Gateway RPC method
|
||||
`workboard.cards.dispatch`. That path uses the same subagent runtime as the
|
||||
dashboard dispatch action, so ready cards can become real worker sessions.
|
||||
dashboard dispatch action, so ready cards become task-tracked worker runs with
|
||||
linked session keys. Cards with an assigned agent use agent-scoped subagent
|
||||
session keys; unassigned cards keep an unscoped subagent key so the Gateway's
|
||||
configured default agent is preserved.
|
||||
|
||||
The dispatch loop:
|
||||
|
||||
@@ -110,8 +113,8 @@ The dispatch loop:
|
||||
5. Claims each selected card for the dispatcher or assigned agent.
|
||||
6. Starts a subagent worker run with bounded card context and the card claim
|
||||
token.
|
||||
7. Stores the worker run id, session key, execution status, and worker log on
|
||||
the card.
|
||||
7. Stores the worker run id, session key, task linkage when the Gateway task
|
||||
ledger reports it, execution status, and worker log on the card.
|
||||
|
||||
Selection is intentionally conservative. One dispatch starts at most three
|
||||
workers by default, skips archived or already-claimed cards, and starts only one
|
||||
@@ -146,6 +149,10 @@ JSON output includes the dispatch result. Gateway-backed dispatch can include
|
||||
`started` and `startFailures`; data-only fallback includes
|
||||
`gatewayUnavailable: true`. Claim tokens are redacted from card JSON output.
|
||||
|
||||
In the dashboard, the same dispatch result is shown as a short summary so an
|
||||
operator can see how many cards started, promoted, blocked, reclaimed, or
|
||||
failed without opening card details.
|
||||
|
||||
## Slash Command Parity
|
||||
|
||||
Command-capable channels can use the matching slash command:
|
||||
|
||||
@@ -9,7 +9,8 @@ title: "Workboard plugin"
|
||||
|
||||
The Workboard plugin adds an optional Kanban-style board to the
|
||||
[Control UI](/web/control-ui). Use it to collect agent-sized work cards, assign
|
||||
them to agents, and jump from a card into the linked dashboard session.
|
||||
them to agents, and track the linked background task, run, and dashboard
|
||||
session from one card.
|
||||
|
||||
Workboard is intentionally small. It tracks local operating work for an
|
||||
OpenClaw Gateway; it is not a replacement for GitHub Issues, Linear, Jira, or
|
||||
@@ -47,8 +48,8 @@ Each card stores:
|
||||
- priority: `low`, `normal`, `high`, or `urgent`
|
||||
- labels
|
||||
- optional agent id
|
||||
- optional linked session, run, task, or source URL
|
||||
- optional execution metadata for a Codex or Claude session started from the card
|
||||
- optional linked task, run, session, or source URL
|
||||
- optional execution metadata for a Codex or Claude run started from the card
|
||||
- compact metadata for attempts, comments, links, proof, artifacts, automation,
|
||||
attachments, worker logs, worker protocol state, claims, diagnostics,
|
||||
notifications, templates, archive state, and stale-session detection
|
||||
@@ -65,26 +66,35 @@ proof snippets, related links, comments, archive markers, and stale-session
|
||||
markers are intentionally local metadata; they do not replace session
|
||||
transcripts or GitHub issue history.
|
||||
|
||||
## Card executions
|
||||
## Card executions and tasks
|
||||
|
||||
Unlinked cards can start work from the card. Start uses the Gateway's configured
|
||||
Unlinked cards can start work from the card. Autonomous starts use the
|
||||
Gateway's task-tracked agent run path, then Workboard links the resulting task,
|
||||
run id, and session key back onto the card. Start uses the Gateway's configured
|
||||
default agent and model. Codex and Claude actions are optional explicit model
|
||||
choices:
|
||||
|
||||
- Run Codex or Run Claude creates a dashboard session, sends the card prompt,
|
||||
and marks the card `running`.
|
||||
- Run Codex or Run Claude starts a task-backed agent run, sends the card
|
||||
prompt, and marks the card `running`.
|
||||
- Open Codex or Open Claude creates a linked dashboard session without sending
|
||||
the card prompt or moving the card, so you can work manually while it stays
|
||||
attached to the board.
|
||||
|
||||
Execution metadata stores the selected engine, mode, model ref, session key,
|
||||
run id, and lifecycle status on the card. Codex executions use
|
||||
`openai/gpt-5.5`; Claude executions use `anthropic/claude-sonnet-4-6`.
|
||||
run id, task id when available, and lifecycle status on the card. Codex
|
||||
executions use `openai/gpt-5.5`; Claude executions use
|
||||
`anthropic/claude-sonnet-4-6`.
|
||||
|
||||
Each linked execution also records an attempt summary on the same card record.
|
||||
The attempt summary keeps the engine, mode, model, run id, timestamps, status,
|
||||
and rolling failure count so repeated failures remain visible on the board.
|
||||
|
||||
The dashboard refreshes task status from the Gateway task ledger and matches
|
||||
tasks back to cards by task id, run id, or linked session key. If a task is
|
||||
queued or running, the card lifecycle shows active task state. If the task
|
||||
finishes, fails, times out, or is cancelled, the card lifecycle moves toward
|
||||
review or blocked status using the same lifecycle sync as linked sessions.
|
||||
|
||||
## Agent coordination
|
||||
|
||||
Workboard also exposes optional agent tools for board-aware workflows:
|
||||
@@ -160,13 +170,15 @@ blocked cards that need attention, repeated failures, done cards without proof,
|
||||
and running cards that only have a loose session link.
|
||||
|
||||
Dispatch is intentionally Gateway-local. It does not spawn arbitrary operating
|
||||
system processes; normal OpenClaw subagent sessions still own execution. A
|
||||
dispatch nudge promotes dependency-ready cards, records dispatch metadata on
|
||||
system processes; normal OpenClaw subagent sessions still own execution. The
|
||||
dispatch action promotes dependency-ready cards, records dispatch metadata on
|
||||
ready cards, blocks expired claims or timed-out runs, marks board-configured
|
||||
triage cards as orchestration candidates, then claims a small batch of ready
|
||||
cards and starts worker runs through the Gateway subagent runtime. Workers get
|
||||
bounded card context plus the claim token they need to heartbeat, complete, or
|
||||
block the card through the Workboard tools.
|
||||
cards and starts worker runs through the Gateway subagent runtime. Assigned
|
||||
cards use `agent:<id>:subagent:workboard-*` worker session keys; unassigned
|
||||
cards use unscoped `subagent:workboard-*` keys so the Gateway still resolves the
|
||||
configured default agent. Workers get bounded card context plus the claim token
|
||||
they need to heartbeat, complete, or block the card through the Workboard tools.
|
||||
|
||||
### Dispatch worker selection
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ describe("dispatchAndStartWorkboardCards", () => {
|
||||
);
|
||||
expect(run).toHaveBeenCalledTimes(2);
|
||||
expect(run.mock.calls[0]?.[0]).toMatchObject({
|
||||
sessionKey: `workboard-default-${first.id}`,
|
||||
sessionKey: `agent:codex-main:subagent:workboard-default-${first.id}`,
|
||||
lane: `workboard:default:${first.id}`,
|
||||
deliver: false,
|
||||
});
|
||||
@@ -66,7 +66,7 @@ describe("dispatchAndStartWorkboardCards", () => {
|
||||
expect(run.mock.calls[0]?.[0]?.message).not.toContain("ownerId and token");
|
||||
await expect(store.get(first.id)).resolves.toMatchObject({
|
||||
status: "running",
|
||||
sessionKey: `workboard-default-${first.id}`,
|
||||
sessionKey: `agent:codex-main:subagent:workboard-default-${first.id}`,
|
||||
runId: "run-first",
|
||||
execution: { status: "running", runId: "run-first" },
|
||||
metadata: {
|
||||
@@ -95,6 +95,11 @@ describe("dispatchAndStartWorkboardCards", () => {
|
||||
expect(result.startFailures).toEqual([
|
||||
expect.objectContaining({ cardId: card.id, error: "model unavailable" }),
|
||||
]);
|
||||
expect(run).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionKey: `subagent:workboard-default-${card.id}`,
|
||||
}),
|
||||
);
|
||||
await expect(store.get(card.id)).resolves.toMatchObject({
|
||||
status: "blocked",
|
||||
metadata: {
|
||||
|
||||
@@ -45,12 +45,24 @@ function cardBoardId(card: WorkboardCard): string {
|
||||
return card.metadata?.automation?.boardId ?? "default";
|
||||
}
|
||||
|
||||
function sanitizeSessionSegment(value: string | undefined, fallback: string): string {
|
||||
const sanitized = (value ?? fallback)
|
||||
.trim()
|
||||
.replace(/[^a-zA-Z0-9_-]/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
return (sanitized || fallback).slice(0, 96);
|
||||
}
|
||||
|
||||
function cardIsArchived(card: WorkboardCard): boolean {
|
||||
return Boolean(card.metadata?.archivedAt);
|
||||
}
|
||||
|
||||
function buildSessionKey(card: WorkboardCard): string {
|
||||
return `workboard-${cardBoardId(card)}-${card.id}`.replace(/[^a-zA-Z0-9:_-]/g, "-");
|
||||
const boardId = sanitizeSessionSegment(cardBoardId(card), "default");
|
||||
const cardId = sanitizeSessionSegment(card.id, "card");
|
||||
const suffix = `subagent:workboard-${boardId}-${cardId}`;
|
||||
return card.agentId ? `agent:${sanitizeSessionSegment(card.agentId, "agent")}:${suffix}` : suffix;
|
||||
}
|
||||
|
||||
function buildExecution(params: {
|
||||
|
||||
8
ui/src/i18n/.i18n/ar.meta.json
generated
8
ui/src/i18n/.i18n/ar.meta.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-31T23:22:03.309Z",
|
||||
"generatedAt": "2026-05-31T23:28:05.990Z",
|
||||
"locale": "ar",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "006a6b5650974ffa777e01c0fc1750c03024b2a1a397e4cb84ddbf7c2b9932ca",
|
||||
"totalKeys": 1294,
|
||||
"translatedKeys": 1294,
|
||||
"sourceHash": "21d07eb018c81914c212ea2dff7f7174db30e25cf1d70dac9b269cf982d7cd8b",
|
||||
"totalKeys": 1284,
|
||||
"translatedKeys": 1284,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
8
ui/src/i18n/.i18n/de.meta.json
generated
8
ui/src/i18n/.i18n/de.meta.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-31T23:20:35.878Z",
|
||||
"generatedAt": "2026-05-31T23:28:05.583Z",
|
||||
"locale": "de",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "006a6b5650974ffa777e01c0fc1750c03024b2a1a397e4cb84ddbf7c2b9932ca",
|
||||
"totalKeys": 1294,
|
||||
"translatedKeys": 1294,
|
||||
"sourceHash": "21d07eb018c81914c212ea2dff7f7174db30e25cf1d70dac9b269cf982d7cd8b",
|
||||
"totalKeys": 1284,
|
||||
"translatedKeys": 1284,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
8
ui/src/i18n/.i18n/es.meta.json
generated
8
ui/src/i18n/.i18n/es.meta.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-31T23:21:25.109Z",
|
||||
"generatedAt": "2026-05-31T23:28:05.664Z",
|
||||
"locale": "es",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "006a6b5650974ffa777e01c0fc1750c03024b2a1a397e4cb84ddbf7c2b9932ca",
|
||||
"totalKeys": 1294,
|
||||
"translatedKeys": 1294,
|
||||
"sourceHash": "21d07eb018c81914c212ea2dff7f7174db30e25cf1d70dac9b269cf982d7cd8b",
|
||||
"totalKeys": 1284,
|
||||
"translatedKeys": 1284,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
8
ui/src/i18n/.i18n/fa.meta.json
generated
8
ui/src/i18n/.i18n/fa.meta.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-31T23:23:43.267Z",
|
||||
"generatedAt": "2026-05-31T23:28:06.719Z",
|
||||
"locale": "fa",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "006a6b5650974ffa777e01c0fc1750c03024b2a1a397e4cb84ddbf7c2b9932ca",
|
||||
"totalKeys": 1294,
|
||||
"translatedKeys": 1294,
|
||||
"sourceHash": "21d07eb018c81914c212ea2dff7f7174db30e25cf1d70dac9b269cf982d7cd8b",
|
||||
"totalKeys": 1284,
|
||||
"translatedKeys": 1284,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
8
ui/src/i18n/.i18n/fr.meta.json
generated
8
ui/src/i18n/.i18n/fr.meta.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-31T23:21:41.367Z",
|
||||
"generatedAt": "2026-05-31T23:28:05.907Z",
|
||||
"locale": "fr",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "006a6b5650974ffa777e01c0fc1750c03024b2a1a397e4cb84ddbf7c2b9932ca",
|
||||
"totalKeys": 1294,
|
||||
"translatedKeys": 1294,
|
||||
"sourceHash": "21d07eb018c81914c212ea2dff7f7174db30e25cf1d70dac9b269cf982d7cd8b",
|
||||
"totalKeys": 1284,
|
||||
"translatedKeys": 1284,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
8
ui/src/i18n/.i18n/id.meta.json
generated
8
ui/src/i18n/.i18n/id.meta.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-31T23:22:46.207Z",
|
||||
"generatedAt": "2026-05-31T23:28:06.305Z",
|
||||
"locale": "id",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "006a6b5650974ffa777e01c0fc1750c03024b2a1a397e4cb84ddbf7c2b9932ca",
|
||||
"totalKeys": 1294,
|
||||
"translatedKeys": 1294,
|
||||
"sourceHash": "21d07eb018c81914c212ea2dff7f7174db30e25cf1d70dac9b269cf982d7cd8b",
|
||||
"totalKeys": 1284,
|
||||
"translatedKeys": 1284,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
8
ui/src/i18n/.i18n/it.meta.json
generated
8
ui/src/i18n/.i18n/it.meta.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-31T23:22:12.640Z",
|
||||
"generatedAt": "2026-05-31T23:28:06.068Z",
|
||||
"locale": "it",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "006a6b5650974ffa777e01c0fc1750c03024b2a1a397e4cb84ddbf7c2b9932ca",
|
||||
"totalKeys": 1294,
|
||||
"translatedKeys": 1294,
|
||||
"sourceHash": "21d07eb018c81914c212ea2dff7f7174db30e25cf1d70dac9b269cf982d7cd8b",
|
||||
"totalKeys": 1284,
|
||||
"translatedKeys": 1284,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
8
ui/src/i18n/.i18n/ja-JP.meta.json
generated
8
ui/src/i18n/.i18n/ja-JP.meta.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-31T23:21:27.902Z",
|
||||
"generatedAt": "2026-05-31T23:28:05.743Z",
|
||||
"locale": "ja-JP",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "006a6b5650974ffa777e01c0fc1750c03024b2a1a397e4cb84ddbf7c2b9932ca",
|
||||
"totalKeys": 1294,
|
||||
"translatedKeys": 1294,
|
||||
"sourceHash": "21d07eb018c81914c212ea2dff7f7174db30e25cf1d70dac9b269cf982d7cd8b",
|
||||
"totalKeys": 1284,
|
||||
"translatedKeys": 1284,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
8
ui/src/i18n/.i18n/ko.meta.json
generated
8
ui/src/i18n/.i18n/ko.meta.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-31T23:21:22.340Z",
|
||||
"generatedAt": "2026-05-31T23:28:05.823Z",
|
||||
"locale": "ko",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "006a6b5650974ffa777e01c0fc1750c03024b2a1a397e4cb84ddbf7c2b9932ca",
|
||||
"totalKeys": 1294,
|
||||
"translatedKeys": 1294,
|
||||
"sourceHash": "21d07eb018c81914c212ea2dff7f7174db30e25cf1d70dac9b269cf982d7cd8b",
|
||||
"totalKeys": 1284,
|
||||
"translatedKeys": 1284,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
8
ui/src/i18n/.i18n/nl.meta.json
generated
8
ui/src/i18n/.i18n/nl.meta.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-31T23:23:35.548Z",
|
||||
"generatedAt": "2026-05-31T23:28:06.639Z",
|
||||
"locale": "nl",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "006a6b5650974ffa777e01c0fc1750c03024b2a1a397e4cb84ddbf7c2b9932ca",
|
||||
"totalKeys": 1294,
|
||||
"translatedKeys": 1294,
|
||||
"sourceHash": "21d07eb018c81914c212ea2dff7f7174db30e25cf1d70dac9b269cf982d7cd8b",
|
||||
"totalKeys": 1284,
|
||||
"translatedKeys": 1284,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
8
ui/src/i18n/.i18n/pl.meta.json
generated
8
ui/src/i18n/.i18n/pl.meta.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-31T23:23:00.128Z",
|
||||
"generatedAt": "2026-05-31T23:28:06.382Z",
|
||||
"locale": "pl",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "006a6b5650974ffa777e01c0fc1750c03024b2a1a397e4cb84ddbf7c2b9932ca",
|
||||
"totalKeys": 1294,
|
||||
"translatedKeys": 1294,
|
||||
"sourceHash": "21d07eb018c81914c212ea2dff7f7174db30e25cf1d70dac9b269cf982d7cd8b",
|
||||
"totalKeys": 1284,
|
||||
"translatedKeys": 1284,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
8
ui/src/i18n/.i18n/pt-BR.meta.json
generated
8
ui/src/i18n/.i18n/pt-BR.meta.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-31T23:20:40.310Z",
|
||||
"generatedAt": "2026-05-31T23:28:05.498Z",
|
||||
"locale": "pt-BR",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "006a6b5650974ffa777e01c0fc1750c03024b2a1a397e4cb84ddbf7c2b9932ca",
|
||||
"totalKeys": 1294,
|
||||
"translatedKeys": 1294,
|
||||
"sourceHash": "21d07eb018c81914c212ea2dff7f7174db30e25cf1d70dac9b269cf982d7cd8b",
|
||||
"totalKeys": 1284,
|
||||
"translatedKeys": 1284,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
8
ui/src/i18n/.i18n/th.meta.json
generated
8
ui/src/i18n/.i18n/th.meta.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-31T23:23:00.947Z",
|
||||
"generatedAt": "2026-05-31T23:28:06.469Z",
|
||||
"locale": "th",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "006a6b5650974ffa777e01c0fc1750c03024b2a1a397e4cb84ddbf7c2b9932ca",
|
||||
"totalKeys": 1294,
|
||||
"translatedKeys": 1294,
|
||||
"sourceHash": "21d07eb018c81914c212ea2dff7f7174db30e25cf1d70dac9b269cf982d7cd8b",
|
||||
"totalKeys": 1284,
|
||||
"translatedKeys": 1284,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
8
ui/src/i18n/.i18n/tr.meta.json
generated
8
ui/src/i18n/.i18n/tr.meta.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-31T23:22:16.120Z",
|
||||
"generatedAt": "2026-05-31T23:28:06.146Z",
|
||||
"locale": "tr",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "006a6b5650974ffa777e01c0fc1750c03024b2a1a397e4cb84ddbf7c2b9932ca",
|
||||
"totalKeys": 1294,
|
||||
"translatedKeys": 1294,
|
||||
"sourceHash": "21d07eb018c81914c212ea2dff7f7174db30e25cf1d70dac9b269cf982d7cd8b",
|
||||
"totalKeys": 1284,
|
||||
"translatedKeys": 1284,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
8
ui/src/i18n/.i18n/uk.meta.json
generated
8
ui/src/i18n/.i18n/uk.meta.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-31T23:22:27.285Z",
|
||||
"generatedAt": "2026-05-31T23:28:06.227Z",
|
||||
"locale": "uk",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "006a6b5650974ffa777e01c0fc1750c03024b2a1a397e4cb84ddbf7c2b9932ca",
|
||||
"totalKeys": 1294,
|
||||
"translatedKeys": 1294,
|
||||
"sourceHash": "21d07eb018c81914c212ea2dff7f7174db30e25cf1d70dac9b269cf982d7cd8b",
|
||||
"totalKeys": 1284,
|
||||
"translatedKeys": 1284,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
8
ui/src/i18n/.i18n/vi.meta.json
generated
8
ui/src/i18n/.i18n/vi.meta.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-31T23:23:11.024Z",
|
||||
"generatedAt": "2026-05-31T23:28:06.555Z",
|
||||
"locale": "vi",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "006a6b5650974ffa777e01c0fc1750c03024b2a1a397e4cb84ddbf7c2b9932ca",
|
||||
"totalKeys": 1294,
|
||||
"translatedKeys": 1294,
|
||||
"sourceHash": "21d07eb018c81914c212ea2dff7f7174db30e25cf1d70dac9b269cf982d7cd8b",
|
||||
"totalKeys": 1284,
|
||||
"translatedKeys": 1284,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
8
ui/src/i18n/.i18n/zh-CN.meta.json
generated
8
ui/src/i18n/.i18n/zh-CN.meta.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-31T23:20:35.218Z",
|
||||
"generatedAt": "2026-05-31T23:28:05.323Z",
|
||||
"locale": "zh-CN",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "006a6b5650974ffa777e01c0fc1750c03024b2a1a397e4cb84ddbf7c2b9932ca",
|
||||
"totalKeys": 1294,
|
||||
"translatedKeys": 1294,
|
||||
"sourceHash": "21d07eb018c81914c212ea2dff7f7174db30e25cf1d70dac9b269cf982d7cd8b",
|
||||
"totalKeys": 1284,
|
||||
"translatedKeys": 1284,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
8
ui/src/i18n/.i18n/zh-TW.meta.json
generated
8
ui/src/i18n/.i18n/zh-TW.meta.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-31T23:20:35.599Z",
|
||||
"generatedAt": "2026-05-31T23:28:05.410Z",
|
||||
"locale": "zh-TW",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "006a6b5650974ffa777e01c0fc1750c03024b2a1a397e4cb84ddbf7c2b9932ca",
|
||||
"totalKeys": 1294,
|
||||
"translatedKeys": 1294,
|
||||
"sourceHash": "21d07eb018c81914c212ea2dff7f7174db30e25cf1d70dac9b269cf982d7cd8b",
|
||||
"totalKeys": 1284,
|
||||
"translatedKeys": 1284,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
31
ui/src/i18n/locales/ar.ts
generated
31
ui/src/i18n/locales/ar.ts
generated
@@ -515,6 +515,9 @@ export const ar: TranslationMap = {
|
||||
runDefaultAgent: "تشغيل الوكيل الافتراضي",
|
||||
start: "بدء",
|
||||
dispatch: "منبّه الموزّع",
|
||||
dispatchSummary:
|
||||
"Dispatch complete: started {started}, promoted {promoted}, blocked {blocked}, reclaimed {reclaimed}, orchestrated {orchestrated}, failures {failures}.",
|
||||
dispatchSummaryEmpty: "Dispatch complete: no ready work changed.",
|
||||
live: "مباشر",
|
||||
fieldTitle: "العنوان",
|
||||
fieldNotes: "ملاحظات",
|
||||
@@ -543,6 +546,7 @@ export const ar: TranslationMap = {
|
||||
badgeTenant: "المستأجر {tenant}",
|
||||
badgeSkills: "{count} مهارات",
|
||||
badgeDispatches: "{count} عمليات إرسال",
|
||||
badgeTaskLinked: "task linked",
|
||||
badgeClaimed: "محجوزة بواسطة {owner}",
|
||||
badgeDiagnostics: "{count} تشخيصات",
|
||||
badgeStale: "قديم",
|
||||
@@ -566,6 +570,14 @@ export const ar: TranslationMap = {
|
||||
lifecycleDoneDetail: "تم النقل إلى المراجعة",
|
||||
lifecycleNeedsReview: "تحتاج إلى مراجعة",
|
||||
lifecycleNeedsReviewDetail: "توقف التشغيل أو فشل",
|
||||
taskStatus: {
|
||||
queued: "Task queued",
|
||||
running: "Task running",
|
||||
completed: "Task complete",
|
||||
failed: "Task failed",
|
||||
cancelled: "Task cancelled",
|
||||
timed_out: "Task timed out",
|
||||
},
|
||||
eventsLabel: "أحداث البطاقة",
|
||||
eventCreated: "تم الإنشاء",
|
||||
eventEdited: "تم التعديل",
|
||||
@@ -592,25 +604,6 @@ export const ar: TranslationMap = {
|
||||
eventArchived: "مؤرشف",
|
||||
eventUnarchived: "غير مؤرشف",
|
||||
eventStale: "جلسة قديمة",
|
||||
gameButton: "لعبة مصغرة",
|
||||
gameTitle: "مطاردة البطاقات",
|
||||
gameStart: "صِل إلى مربع الإطلاق.",
|
||||
gameBoundary: "تم الوصول إلى الحد.",
|
||||
gameBlocked: "محظور.",
|
||||
gameContinue: "استمر.",
|
||||
gameWin: "تم اجتياز الإطلاق.",
|
||||
gameMoves: "الحركات {count}",
|
||||
gameWins: "الانتصارات {count}",
|
||||
gameBoard: "لوحة مطاردة البطاقات",
|
||||
gameControls: "عناصر تحكم مطاردة البطاقات",
|
||||
gameAgent: "الوكيل",
|
||||
gameLaunch: "إطلاق",
|
||||
gameBlockedCell: "محظور",
|
||||
gameOpenCell: "مفتوح",
|
||||
gameMoveUp: "التحرك للأعلى",
|
||||
gameMoveLeft: "التحرك لليسار",
|
||||
gameMoveDown: "التحرك للأسفل",
|
||||
gameMoveRight: "التحرك لليمين",
|
||||
},
|
||||
overview: {
|
||||
access: {
|
||||
|
||||
31
ui/src/i18n/locales/de.ts
generated
31
ui/src/i18n/locales/de.ts
generated
@@ -520,6 +520,9 @@ export const de: TranslationMap = {
|
||||
runDefaultAgent: "Standard-Agent ausführen",
|
||||
start: "Starten",
|
||||
dispatch: "Dispatcher anstoßen",
|
||||
dispatchSummary:
|
||||
"Dispatch complete: started {started}, promoted {promoted}, blocked {blocked}, reclaimed {reclaimed}, orchestrated {orchestrated}, failures {failures}.",
|
||||
dispatchSummaryEmpty: "Dispatch complete: no ready work changed.",
|
||||
live: "live",
|
||||
fieldTitle: "Titel",
|
||||
fieldNotes: "Notizen",
|
||||
@@ -548,6 +551,7 @@ export const de: TranslationMap = {
|
||||
badgeTenant: "Mandant {tenant}",
|
||||
badgeSkills: "{count} Fähigkeiten",
|
||||
badgeDispatches: "{count} Auslösungen",
|
||||
badgeTaskLinked: "task linked",
|
||||
badgeClaimed: "beansprucht von {owner}",
|
||||
badgeDiagnostics: "{count} Diagnosen",
|
||||
badgeStale: "veraltet",
|
||||
@@ -571,6 +575,14 @@ export const de: TranslationMap = {
|
||||
lifecycleDoneDetail: "Zur Überprüfung verschoben",
|
||||
lifecycleNeedsReview: "Überprüfung erforderlich",
|
||||
lifecycleNeedsReviewDetail: "Lauf gestoppt oder fehlgeschlagen",
|
||||
taskStatus: {
|
||||
queued: "Task queued",
|
||||
running: "Task running",
|
||||
completed: "Task complete",
|
||||
failed: "Task failed",
|
||||
cancelled: "Task cancelled",
|
||||
timed_out: "Task timed out",
|
||||
},
|
||||
eventsLabel: "Kartenereignisse",
|
||||
eventCreated: "Erstellt",
|
||||
eventEdited: "Bearbeitet",
|
||||
@@ -597,25 +609,6 @@ export const de: TranslationMap = {
|
||||
eventArchived: "Archiviert",
|
||||
eventUnarchived: "Aus Archiv wiederhergestellt",
|
||||
eventStale: "Veraltete Sitzung",
|
||||
gameButton: "Minispiel",
|
||||
gameTitle: "Card Chase",
|
||||
gameStart: "Erreiche das Startfeld.",
|
||||
gameBoundary: "Grenze erreicht.",
|
||||
gameBlocked: "Blockiert.",
|
||||
gameContinue: "Weiter so.",
|
||||
gameWin: "Start abgeschlossen.",
|
||||
gameMoves: "Züge {count}",
|
||||
gameWins: "Siege {count}",
|
||||
gameBoard: "Card Chase-Spielfeld",
|
||||
gameControls: "Card Chase-Steuerung",
|
||||
gameAgent: "Agent",
|
||||
gameLaunch: "Start",
|
||||
gameBlockedCell: "Blockiert",
|
||||
gameOpenCell: "Offen",
|
||||
gameMoveUp: "Nach oben bewegen",
|
||||
gameMoveLeft: "Nach links bewegen",
|
||||
gameMoveDown: "Nach unten bewegen",
|
||||
gameMoveRight: "Nach rechts bewegen",
|
||||
},
|
||||
overview: {
|
||||
access: {
|
||||
|
||||
@@ -513,7 +513,10 @@ export const en: TranslationMap = {
|
||||
openEngine: "Open {engine}",
|
||||
runDefaultAgent: "Run default agent",
|
||||
start: "Start",
|
||||
dispatch: "Nudge dispatcher",
|
||||
dispatch: "Dispatch ready work",
|
||||
dispatchSummary:
|
||||
"Dispatch complete: started {started}, promoted {promoted}, blocked {blocked}, reclaimed {reclaimed}, orchestrated {orchestrated}, failures {failures}.",
|
||||
dispatchSummaryEmpty: "Dispatch complete: no ready work changed.",
|
||||
live: "live",
|
||||
fieldTitle: "Title",
|
||||
fieldNotes: "Notes",
|
||||
@@ -542,6 +545,7 @@ export const en: TranslationMap = {
|
||||
badgeTenant: "tenant {tenant}",
|
||||
badgeSkills: "{count} skills",
|
||||
badgeDispatches: "{count} dispatches",
|
||||
badgeTaskLinked: "task linked",
|
||||
badgeClaimed: "claimed by {owner}",
|
||||
badgeDiagnostics: "{count} diagnostics",
|
||||
badgeStale: "stale",
|
||||
@@ -565,6 +569,14 @@ export const en: TranslationMap = {
|
||||
lifecycleDoneDetail: "Moved to review",
|
||||
lifecycleNeedsReview: "Needs review",
|
||||
lifecycleNeedsReviewDetail: "Run stopped or failed",
|
||||
taskStatus: {
|
||||
queued: "Task queued",
|
||||
running: "Task running",
|
||||
completed: "Task complete",
|
||||
failed: "Task failed",
|
||||
cancelled: "Task cancelled",
|
||||
timed_out: "Task timed out",
|
||||
},
|
||||
eventsLabel: "Card events",
|
||||
eventCreated: "Created",
|
||||
eventEdited: "Edited",
|
||||
@@ -591,25 +603,6 @@ export const en: TranslationMap = {
|
||||
eventArchived: "Archived",
|
||||
eventUnarchived: "Unarchived",
|
||||
eventStale: "Stale session",
|
||||
gameButton: "Mini game",
|
||||
gameTitle: "Card Chase",
|
||||
gameStart: "Reach the launch tile.",
|
||||
gameBoundary: "Boundary reached.",
|
||||
gameBlocked: "Blocked.",
|
||||
gameContinue: "Keep going.",
|
||||
gameWin: "Launch cleared.",
|
||||
gameMoves: "Moves {count}",
|
||||
gameWins: "Wins {count}",
|
||||
gameBoard: "Card Chase board",
|
||||
gameControls: "Card Chase controls",
|
||||
gameAgent: "Agent",
|
||||
gameLaunch: "Launch",
|
||||
gameBlockedCell: "Blocked",
|
||||
gameOpenCell: "Open",
|
||||
gameMoveUp: "Move up",
|
||||
gameMoveLeft: "Move left",
|
||||
gameMoveDown: "Move down",
|
||||
gameMoveRight: "Move right",
|
||||
},
|
||||
overview: {
|
||||
access: {
|
||||
|
||||
31
ui/src/i18n/locales/es.ts
generated
31
ui/src/i18n/locales/es.ts
generated
@@ -517,6 +517,9 @@ export const es: TranslationMap = {
|
||||
runDefaultAgent: "Ejecutar agente predeterminado",
|
||||
start: "Iniciar",
|
||||
dispatch: "Avisar al despachador",
|
||||
dispatchSummary:
|
||||
"Dispatch complete: started {started}, promoted {promoted}, blocked {blocked}, reclaimed {reclaimed}, orchestrated {orchestrated}, failures {failures}.",
|
||||
dispatchSummaryEmpty: "Dispatch complete: no ready work changed.",
|
||||
live: "en vivo",
|
||||
fieldTitle: "Título",
|
||||
fieldNotes: "Notas",
|
||||
@@ -545,6 +548,7 @@ export const es: TranslationMap = {
|
||||
badgeTenant: "inquilino {tenant}",
|
||||
badgeSkills: "{count} habilidades",
|
||||
badgeDispatches: "{count} envíos",
|
||||
badgeTaskLinked: "task linked",
|
||||
badgeClaimed: "reclamada por {owner}",
|
||||
badgeDiagnostics: "{count} diagnósticos",
|
||||
badgeStale: "obsoleta",
|
||||
@@ -568,6 +572,14 @@ export const es: TranslationMap = {
|
||||
lifecycleDoneDetail: "Movido a revisión",
|
||||
lifecycleNeedsReview: "Necesita revisión",
|
||||
lifecycleNeedsReviewDetail: "La ejecución se detuvo o falló",
|
||||
taskStatus: {
|
||||
queued: "Task queued",
|
||||
running: "Task running",
|
||||
completed: "Task complete",
|
||||
failed: "Task failed",
|
||||
cancelled: "Task cancelled",
|
||||
timed_out: "Task timed out",
|
||||
},
|
||||
eventsLabel: "Eventos de la tarjeta",
|
||||
eventCreated: "Creada",
|
||||
eventEdited: "Editada",
|
||||
@@ -594,25 +606,6 @@ export const es: TranslationMap = {
|
||||
eventArchived: "Archivado",
|
||||
eventUnarchived: "Desarchivado",
|
||||
eventStale: "Sesión obsoleta",
|
||||
gameButton: "Minijuego",
|
||||
gameTitle: "Card Chase",
|
||||
gameStart: "Alcanza la casilla de lanzamiento.",
|
||||
gameBoundary: "Límite alcanzado.",
|
||||
gameBlocked: "Bloqueado.",
|
||||
gameContinue: "Sigue adelante.",
|
||||
gameWin: "Lanzamiento completado.",
|
||||
gameMoves: "Movimientos {count}",
|
||||
gameWins: "Victorias {count}",
|
||||
gameBoard: "Tablero de Card Chase",
|
||||
gameControls: "Controles de Card Chase",
|
||||
gameAgent: "Agente",
|
||||
gameLaunch: "Lanzamiento",
|
||||
gameBlockedCell: "Bloqueado",
|
||||
gameOpenCell: "Abierto",
|
||||
gameMoveUp: "Mover arriba",
|
||||
gameMoveLeft: "Mover a la izquierda",
|
||||
gameMoveDown: "Mover abajo",
|
||||
gameMoveRight: "Mover a la derecha",
|
||||
},
|
||||
overview: {
|
||||
access: {
|
||||
|
||||
31
ui/src/i18n/locales/fa.ts
generated
31
ui/src/i18n/locales/fa.ts
generated
@@ -517,6 +517,9 @@ export const fa: TranslationMap = {
|
||||
runDefaultAgent: "اجرای عامل پیشفرض",
|
||||
start: "شروع",
|
||||
dispatch: "تلنگر به توزیعکننده",
|
||||
dispatchSummary:
|
||||
"Dispatch complete: started {started}, promoted {promoted}, blocked {blocked}, reclaimed {reclaimed}, orchestrated {orchestrated}, failures {failures}.",
|
||||
dispatchSummaryEmpty: "Dispatch complete: no ready work changed.",
|
||||
live: "زنده",
|
||||
fieldTitle: "عنوان",
|
||||
fieldNotes: "یادداشتها",
|
||||
@@ -545,6 +548,7 @@ export const fa: TranslationMap = {
|
||||
badgeTenant: "مستاجر {tenant}",
|
||||
badgeSkills: "{count} مهارت",
|
||||
badgeDispatches: "{count} ارسال",
|
||||
badgeTaskLinked: "task linked",
|
||||
badgeClaimed: "ادعا شده توسط {owner}",
|
||||
badgeDiagnostics: "{count} تشخیص",
|
||||
badgeStale: "قدیمی",
|
||||
@@ -568,6 +572,14 @@ export const fa: TranslationMap = {
|
||||
lifecycleDoneDetail: "به بازبینی منتقل شد",
|
||||
lifecycleNeedsReview: "نیازمند بازبینی",
|
||||
lifecycleNeedsReviewDetail: "اجرا متوقف شد یا ناموفق بود",
|
||||
taskStatus: {
|
||||
queued: "Task queued",
|
||||
running: "Task running",
|
||||
completed: "Task complete",
|
||||
failed: "Task failed",
|
||||
cancelled: "Task cancelled",
|
||||
timed_out: "Task timed out",
|
||||
},
|
||||
eventsLabel: "رویدادهای کارت",
|
||||
eventCreated: "ایجاد شد",
|
||||
eventEdited: "ویرایش شد",
|
||||
@@ -594,25 +606,6 @@ export const fa: TranslationMap = {
|
||||
eventArchived: "بایگانی شد",
|
||||
eventUnarchived: "از بایگانی خارج شد",
|
||||
eventStale: "نشست منقضیشده",
|
||||
gameButton: "بازی کوچک",
|
||||
gameTitle: "تعقیب کارت",
|
||||
gameStart: "به کاشی راهاندازی برسید.",
|
||||
gameBoundary: "به مرز رسیدید.",
|
||||
gameBlocked: "مسدود شده است.",
|
||||
gameContinue: "ادامه دهید.",
|
||||
gameWin: "راهاندازی با موفقیت انجام شد.",
|
||||
gameMoves: "حرکتها {count}",
|
||||
gameWins: "بردها {count}",
|
||||
gameBoard: "صفحه Card Chase",
|
||||
gameControls: "کنترلهای Card Chase",
|
||||
gameAgent: "عامل",
|
||||
gameLaunch: "راهاندازی",
|
||||
gameBlockedCell: "مسدود",
|
||||
gameOpenCell: "باز",
|
||||
gameMoveUp: "حرکت به بالا",
|
||||
gameMoveLeft: "حرکت به چپ",
|
||||
gameMoveDown: "حرکت به پایین",
|
||||
gameMoveRight: "حرکت به راست",
|
||||
},
|
||||
overview: {
|
||||
access: {
|
||||
|
||||
31
ui/src/i18n/locales/fr.ts
generated
31
ui/src/i18n/locales/fr.ts
generated
@@ -519,6 +519,9 @@ export const fr: TranslationMap = {
|
||||
runDefaultAgent: "Exécuter l’agent par défaut",
|
||||
start: "Démarrer",
|
||||
dispatch: "Relancer le répartiteur",
|
||||
dispatchSummary:
|
||||
"Dispatch complete: started {started}, promoted {promoted}, blocked {blocked}, reclaimed {reclaimed}, orchestrated {orchestrated}, failures {failures}.",
|
||||
dispatchSummaryEmpty: "Dispatch complete: no ready work changed.",
|
||||
live: "en direct",
|
||||
fieldTitle: "Titre",
|
||||
fieldNotes: "Notes",
|
||||
@@ -547,6 +550,7 @@ export const fr: TranslationMap = {
|
||||
badgeTenant: "locataire {tenant}",
|
||||
badgeSkills: "{count} compétences",
|
||||
badgeDispatches: "{count} envois",
|
||||
badgeTaskLinked: "task linked",
|
||||
badgeClaimed: "revendiqué par {owner}",
|
||||
badgeDiagnostics: "{count} diagnostics",
|
||||
badgeStale: "obsolète",
|
||||
@@ -570,6 +574,14 @@ export const fr: TranslationMap = {
|
||||
lifecycleDoneDetail: "Déplacé vers la révision",
|
||||
lifecycleNeedsReview: "Nécessite une révision",
|
||||
lifecycleNeedsReviewDetail: "Exécution arrêtée ou échouée",
|
||||
taskStatus: {
|
||||
queued: "Task queued",
|
||||
running: "Task running",
|
||||
completed: "Task complete",
|
||||
failed: "Task failed",
|
||||
cancelled: "Task cancelled",
|
||||
timed_out: "Task timed out",
|
||||
},
|
||||
eventsLabel: "Événements de la carte",
|
||||
eventCreated: "Créé",
|
||||
eventEdited: "Modifié",
|
||||
@@ -596,25 +608,6 @@ export const fr: TranslationMap = {
|
||||
eventArchived: "Archivé",
|
||||
eventUnarchived: "Désarchivé",
|
||||
eventStale: "Session inactive",
|
||||
gameButton: "Mini-jeu",
|
||||
gameTitle: "Card Chase",
|
||||
gameStart: "Atteignez la tuile de lancement.",
|
||||
gameBoundary: "Limite atteinte.",
|
||||
gameBlocked: "Bloqué.",
|
||||
gameContinue: "Continuez.",
|
||||
gameWin: "Lancement terminé.",
|
||||
gameMoves: "Coups {count}",
|
||||
gameWins: "Victoires {count}",
|
||||
gameBoard: "Plateau Card Chase",
|
||||
gameControls: "Commandes de Card Chase",
|
||||
gameAgent: "Agent",
|
||||
gameLaunch: "Lancement",
|
||||
gameBlockedCell: "Bloqué",
|
||||
gameOpenCell: "Ouvert",
|
||||
gameMoveUp: "Déplacer vers le haut",
|
||||
gameMoveLeft: "Déplacer vers la gauche",
|
||||
gameMoveDown: "Déplacer vers le bas",
|
||||
gameMoveRight: "Déplacer vers la droite",
|
||||
},
|
||||
overview: {
|
||||
access: {
|
||||
|
||||
31
ui/src/i18n/locales/id.ts
generated
31
ui/src/i18n/locales/id.ts
generated
@@ -516,6 +516,9 @@ export const id: TranslationMap = {
|
||||
runDefaultAgent: "Jalankan agen default",
|
||||
start: "Mulai",
|
||||
dispatch: "Dorong dispatcher",
|
||||
dispatchSummary:
|
||||
"Dispatch complete: started {started}, promoted {promoted}, blocked {blocked}, reclaimed {reclaimed}, orchestrated {orchestrated}, failures {failures}.",
|
||||
dispatchSummaryEmpty: "Dispatch complete: no ready work changed.",
|
||||
live: "langsung",
|
||||
fieldTitle: "Judul",
|
||||
fieldNotes: "Catatan",
|
||||
@@ -544,6 +547,7 @@ export const id: TranslationMap = {
|
||||
badgeTenant: "penyewa {tenant}",
|
||||
badgeSkills: "{count} keahlian",
|
||||
badgeDispatches: "{count} pengiriman",
|
||||
badgeTaskLinked: "task linked",
|
||||
badgeClaimed: "diklaim oleh {owner}",
|
||||
badgeDiagnostics: "{count} diagnostik",
|
||||
badgeStale: "usang",
|
||||
@@ -567,6 +571,14 @@ export const id: TranslationMap = {
|
||||
lifecycleDoneDetail: "Dipindahkan ke peninjauan",
|
||||
lifecycleNeedsReview: "Perlu ditinjau",
|
||||
lifecycleNeedsReviewDetail: "Run dihentikan atau gagal",
|
||||
taskStatus: {
|
||||
queued: "Task queued",
|
||||
running: "Task running",
|
||||
completed: "Task complete",
|
||||
failed: "Task failed",
|
||||
cancelled: "Task cancelled",
|
||||
timed_out: "Task timed out",
|
||||
},
|
||||
eventsLabel: "Peristiwa kartu",
|
||||
eventCreated: "Dibuat",
|
||||
eventEdited: "Diedit",
|
||||
@@ -593,25 +605,6 @@ export const id: TranslationMap = {
|
||||
eventArchived: "Diarsipkan",
|
||||
eventUnarchived: "Dibatalkan pengarsipannya",
|
||||
eventStale: "Sesi kedaluwarsa",
|
||||
gameButton: "Mini game",
|
||||
gameTitle: "Card Chase",
|
||||
gameStart: "Capai petak peluncuran.",
|
||||
gameBoundary: "Mencapai batas.",
|
||||
gameBlocked: "Terhalang.",
|
||||
gameContinue: "Terus lanjut.",
|
||||
gameWin: "Peluncuran selesai.",
|
||||
gameMoves: "Gerakan {count}",
|
||||
gameWins: "Kemenangan {count}",
|
||||
gameBoard: "Papan Card Chase",
|
||||
gameControls: "Kontrol Card Chase",
|
||||
gameAgent: "Agen",
|
||||
gameLaunch: "Luncurkan",
|
||||
gameBlockedCell: "Terhalang",
|
||||
gameOpenCell: "Terbuka",
|
||||
gameMoveUp: "Bergerak ke atas",
|
||||
gameMoveLeft: "Bergerak ke kiri",
|
||||
gameMoveDown: "Bergerak ke bawah",
|
||||
gameMoveRight: "Bergerak ke kanan",
|
||||
},
|
||||
overview: {
|
||||
access: {
|
||||
|
||||
31
ui/src/i18n/locales/it.ts
generated
31
ui/src/i18n/locales/it.ts
generated
@@ -518,6 +518,9 @@ export const it: TranslationMap = {
|
||||
runDefaultAgent: "Esegui agente predefinito",
|
||||
start: "Avvia",
|
||||
dispatch: "Sollecita dispatcher",
|
||||
dispatchSummary:
|
||||
"Dispatch complete: started {started}, promoted {promoted}, blocked {blocked}, reclaimed {reclaimed}, orchestrated {orchestrated}, failures {failures}.",
|
||||
dispatchSummaryEmpty: "Dispatch complete: no ready work changed.",
|
||||
live: "live",
|
||||
fieldTitle: "Titolo",
|
||||
fieldNotes: "Note",
|
||||
@@ -546,6 +549,7 @@ export const it: TranslationMap = {
|
||||
badgeTenant: "ambiente {tenant}",
|
||||
badgeSkills: "{count} competenze",
|
||||
badgeDispatches: "{count} invii",
|
||||
badgeTaskLinked: "task linked",
|
||||
badgeClaimed: "rivendicato da {owner}",
|
||||
badgeDiagnostics: "{count} diagnostiche",
|
||||
badgeStale: "obsoleto",
|
||||
@@ -569,6 +573,14 @@ export const it: TranslationMap = {
|
||||
lifecycleDoneDetail: "Spostata in revisione",
|
||||
lifecycleNeedsReview: "Richiede revisione",
|
||||
lifecycleNeedsReviewDetail: "Esecuzione interrotta o non riuscita",
|
||||
taskStatus: {
|
||||
queued: "Task queued",
|
||||
running: "Task running",
|
||||
completed: "Task complete",
|
||||
failed: "Task failed",
|
||||
cancelled: "Task cancelled",
|
||||
timed_out: "Task timed out",
|
||||
},
|
||||
eventsLabel: "Eventi della scheda",
|
||||
eventCreated: "Creato",
|
||||
eventEdited: "Modificato",
|
||||
@@ -595,25 +607,6 @@ export const it: TranslationMap = {
|
||||
eventArchived: "Archiviato",
|
||||
eventUnarchived: "Non archiviato",
|
||||
eventStale: "Sessione obsoleta",
|
||||
gameButton: "Mini gioco",
|
||||
gameTitle: "Card Chase",
|
||||
gameStart: "Raggiungi la casella di lancio.",
|
||||
gameBoundary: "Limite raggiunto.",
|
||||
gameBlocked: "Bloccato.",
|
||||
gameContinue: "Continua.",
|
||||
gameWin: "Lancio completato.",
|
||||
gameMoves: "Mosse {count}",
|
||||
gameWins: "Vittorie {count}",
|
||||
gameBoard: "Tabellone di Card Chase",
|
||||
gameControls: "Controlli di Card Chase",
|
||||
gameAgent: "Agente",
|
||||
gameLaunch: "Lancio",
|
||||
gameBlockedCell: "Bloccata",
|
||||
gameOpenCell: "Aperta",
|
||||
gameMoveUp: "Sposta su",
|
||||
gameMoveLeft: "Sposta a sinistra",
|
||||
gameMoveDown: "Sposta giù",
|
||||
gameMoveRight: "Sposta a destra",
|
||||
},
|
||||
overview: {
|
||||
access: {
|
||||
|
||||
31
ui/src/i18n/locales/ja-JP.ts
generated
31
ui/src/i18n/locales/ja-JP.ts
generated
@@ -519,6 +519,9 @@ export const ja_JP: TranslationMap = {
|
||||
runDefaultAgent: "デフォルトエージェントを実行",
|
||||
start: "開始",
|
||||
dispatch: "ディスパッチャーを促す",
|
||||
dispatchSummary:
|
||||
"Dispatch complete: started {started}, promoted {promoted}, blocked {blocked}, reclaimed {reclaimed}, orchestrated {orchestrated}, failures {failures}.",
|
||||
dispatchSummaryEmpty: "Dispatch complete: no ready work changed.",
|
||||
live: "ライブ",
|
||||
fieldTitle: "タイトル",
|
||||
fieldNotes: "メモ",
|
||||
@@ -547,6 +550,7 @@ export const ja_JP: TranslationMap = {
|
||||
badgeTenant: "テナント {tenant}",
|
||||
badgeSkills: "{count} 個のスキル",
|
||||
badgeDispatches: "{count} 回のディスパッチ",
|
||||
badgeTaskLinked: "task linked",
|
||||
badgeClaimed: "{owner} が要求済み",
|
||||
badgeDiagnostics: "{count} 件の診断",
|
||||
badgeStale: "古い",
|
||||
@@ -570,6 +574,14 @@ export const ja_JP: TranslationMap = {
|
||||
lifecycleDoneDetail: "レビューに移動しました",
|
||||
lifecycleNeedsReview: "レビューが必要",
|
||||
lifecycleNeedsReviewDetail: "実行が停止または失敗しました",
|
||||
taskStatus: {
|
||||
queued: "Task queued",
|
||||
running: "Task running",
|
||||
completed: "Task complete",
|
||||
failed: "Task failed",
|
||||
cancelled: "Task cancelled",
|
||||
timed_out: "Task timed out",
|
||||
},
|
||||
eventsLabel: "カードイベント",
|
||||
eventCreated: "作成済み",
|
||||
eventEdited: "編集済み",
|
||||
@@ -596,25 +608,6 @@ export const ja_JP: TranslationMap = {
|
||||
eventArchived: "アーカイブ済み",
|
||||
eventUnarchived: "アーカイブ解除済み",
|
||||
eventStale: "古いセッション",
|
||||
gameButton: "ミニゲーム",
|
||||
gameTitle: "Card Chase",
|
||||
gameStart: "ローンチタイルに到達してください。",
|
||||
gameBoundary: "境界に到達しました。",
|
||||
gameBlocked: "ブロックされています。",
|
||||
gameContinue: "続けてください。",
|
||||
gameWin: "ローンチをクリアしました。",
|
||||
gameMoves: "移動 {count}",
|
||||
gameWins: "勝利 {count}",
|
||||
gameBoard: "Card Chase ボード",
|
||||
gameControls: "Card Chase コントロール",
|
||||
gameAgent: "エージェント",
|
||||
gameLaunch: "ローンチ",
|
||||
gameBlockedCell: "ブロック済み",
|
||||
gameOpenCell: "オープン",
|
||||
gameMoveUp: "上に移動",
|
||||
gameMoveLeft: "左に移動",
|
||||
gameMoveDown: "下に移動",
|
||||
gameMoveRight: "右に移動",
|
||||
},
|
||||
overview: {
|
||||
access: {
|
||||
|
||||
31
ui/src/i18n/locales/ko.ts
generated
31
ui/src/i18n/locales/ko.ts
generated
@@ -515,6 +515,9 @@ export const ko: TranslationMap = {
|
||||
runDefaultAgent: "기본 에이전트 실행",
|
||||
start: "시작",
|
||||
dispatch: "디스패처 넛지",
|
||||
dispatchSummary:
|
||||
"Dispatch complete: started {started}, promoted {promoted}, blocked {blocked}, reclaimed {reclaimed}, orchestrated {orchestrated}, failures {failures}.",
|
||||
dispatchSummaryEmpty: "Dispatch complete: no ready work changed.",
|
||||
live: "라이브",
|
||||
fieldTitle: "제목",
|
||||
fieldNotes: "메모",
|
||||
@@ -543,6 +546,7 @@ export const ko: TranslationMap = {
|
||||
badgeTenant: "테넌트 {tenant}",
|
||||
badgeSkills: "{count}개 스킬",
|
||||
badgeDispatches: "{count}회 디스패치",
|
||||
badgeTaskLinked: "task linked",
|
||||
badgeClaimed: "{owner}가 할당함",
|
||||
badgeDiagnostics: "진단 {count}개",
|
||||
badgeStale: "오래됨",
|
||||
@@ -566,6 +570,14 @@ export const ko: TranslationMap = {
|
||||
lifecycleDoneDetail: "검토로 이동됨",
|
||||
lifecycleNeedsReview: "검토 필요",
|
||||
lifecycleNeedsReviewDetail: "실행이 중지되었거나 실패했습니다",
|
||||
taskStatus: {
|
||||
queued: "Task queued",
|
||||
running: "Task running",
|
||||
completed: "Task complete",
|
||||
failed: "Task failed",
|
||||
cancelled: "Task cancelled",
|
||||
timed_out: "Task timed out",
|
||||
},
|
||||
eventsLabel: "카드 이벤트",
|
||||
eventCreated: "생성됨",
|
||||
eventEdited: "편집됨",
|
||||
@@ -592,25 +604,6 @@ export const ko: TranslationMap = {
|
||||
eventArchived: "보관됨",
|
||||
eventUnarchived: "보관 해제됨",
|
||||
eventStale: "오래된 세션",
|
||||
gameButton: "미니 게임",
|
||||
gameTitle: "Card Chase",
|
||||
gameStart: "출발 타일에 도달하세요.",
|
||||
gameBoundary: "경계에 도달했습니다.",
|
||||
gameBlocked: "막혔습니다.",
|
||||
gameContinue: "계속 진행하세요.",
|
||||
gameWin: "출발 완료.",
|
||||
gameMoves: "이동 {count}회",
|
||||
gameWins: "승리 {count}회",
|
||||
gameBoard: "Card Chase 보드",
|
||||
gameControls: "Card Chase 컨트롤",
|
||||
gameAgent: "에이전트",
|
||||
gameLaunch: "출발",
|
||||
gameBlockedCell: "차단됨",
|
||||
gameOpenCell: "열림",
|
||||
gameMoveUp: "위로 이동",
|
||||
gameMoveLeft: "왼쪽으로 이동",
|
||||
gameMoveDown: "아래로 이동",
|
||||
gameMoveRight: "오른쪽으로 이동",
|
||||
},
|
||||
overview: {
|
||||
access: {
|
||||
|
||||
31
ui/src/i18n/locales/nl.ts
generated
31
ui/src/i18n/locales/nl.ts
generated
@@ -518,6 +518,9 @@ export const nl: TranslationMap = {
|
||||
runDefaultAgent: "Standaardagent uitvoeren",
|
||||
start: "Starten",
|
||||
dispatch: "Dispatcher een zetje geven",
|
||||
dispatchSummary:
|
||||
"Dispatch complete: started {started}, promoted {promoted}, blocked {blocked}, reclaimed {reclaimed}, orchestrated {orchestrated}, failures {failures}.",
|
||||
dispatchSummaryEmpty: "Dispatch complete: no ready work changed.",
|
||||
live: "live",
|
||||
fieldTitle: "Titel",
|
||||
fieldNotes: "Notities",
|
||||
@@ -546,6 +549,7 @@ export const nl: TranslationMap = {
|
||||
badgeTenant: "omgeving {tenant}",
|
||||
badgeSkills: "{count} vaardigheden",
|
||||
badgeDispatches: "{count} verzendingen",
|
||||
badgeTaskLinked: "task linked",
|
||||
badgeClaimed: "geclaimd door {owner}",
|
||||
badgeDiagnostics: "{count} diagnoses",
|
||||
badgeStale: "verouderd",
|
||||
@@ -569,6 +573,14 @@ export const nl: TranslationMap = {
|
||||
lifecycleDoneDetail: "Verplaatst naar review",
|
||||
lifecycleNeedsReview: "Review nodig",
|
||||
lifecycleNeedsReviewDetail: "Run gestopt of mislukt",
|
||||
taskStatus: {
|
||||
queued: "Task queued",
|
||||
running: "Task running",
|
||||
completed: "Task complete",
|
||||
failed: "Task failed",
|
||||
cancelled: "Task cancelled",
|
||||
timed_out: "Task timed out",
|
||||
},
|
||||
eventsLabel: "Kaartgebeurtenissen",
|
||||
eventCreated: "Gemaakt",
|
||||
eventEdited: "Bewerkt",
|
||||
@@ -595,25 +607,6 @@ export const nl: TranslationMap = {
|
||||
eventArchived: "Gearchiveerd",
|
||||
eventUnarchived: "Uit archief gehaald",
|
||||
eventStale: "Verlopen sessie",
|
||||
gameButton: "Minigame",
|
||||
gameTitle: "Card Chase",
|
||||
gameStart: "Bereik de lanceringstegel.",
|
||||
gameBoundary: "Grens bereikt.",
|
||||
gameBlocked: "Geblokkeerd.",
|
||||
gameContinue: "Ga door.",
|
||||
gameWin: "Lancering voltooid.",
|
||||
gameMoves: "Zetten {count}",
|
||||
gameWins: "Overwinningen {count}",
|
||||
gameBoard: "Card Chase-bord",
|
||||
gameControls: "Card Chase-bediening",
|
||||
gameAgent: "Agent",
|
||||
gameLaunch: "Starten",
|
||||
gameBlockedCell: "Geblokkeerd",
|
||||
gameOpenCell: "Open",
|
||||
gameMoveUp: "Omhoog verplaatsen",
|
||||
gameMoveLeft: "Naar links verplaatsen",
|
||||
gameMoveDown: "Omlaag verplaatsen",
|
||||
gameMoveRight: "Naar rechts verplaatsen",
|
||||
},
|
||||
overview: {
|
||||
access: {
|
||||
|
||||
31
ui/src/i18n/locales/pl.ts
generated
31
ui/src/i18n/locales/pl.ts
generated
@@ -517,6 +517,9 @@ export const pl: TranslationMap = {
|
||||
runDefaultAgent: "Uruchom domyślnego agenta",
|
||||
start: "Rozpocznij",
|
||||
dispatch: "Daj znać dyspozytorowi",
|
||||
dispatchSummary:
|
||||
"Dispatch complete: started {started}, promoted {promoted}, blocked {blocked}, reclaimed {reclaimed}, orchestrated {orchestrated}, failures {failures}.",
|
||||
dispatchSummaryEmpty: "Dispatch complete: no ready work changed.",
|
||||
live: "na żywo",
|
||||
fieldTitle: "Tytuł",
|
||||
fieldNotes: "Notatki",
|
||||
@@ -545,6 +548,7 @@ export const pl: TranslationMap = {
|
||||
badgeTenant: "dzierżawca {tenant}",
|
||||
badgeSkills: "{count} umiejętności",
|
||||
badgeDispatches: "{count} wysyłki",
|
||||
badgeTaskLinked: "task linked",
|
||||
badgeClaimed: "przejęte przez {owner}",
|
||||
badgeDiagnostics: "{count} diagnostyk",
|
||||
badgeStale: "nieaktualne",
|
||||
@@ -568,6 +572,14 @@ export const pl: TranslationMap = {
|
||||
lifecycleDoneDetail: "Przeniesiono do przeglądu",
|
||||
lifecycleNeedsReview: "Wymaga przeglądu",
|
||||
lifecycleNeedsReviewDetail: "Uruchomienie zatrzymane lub nieudane",
|
||||
taskStatus: {
|
||||
queued: "Task queued",
|
||||
running: "Task running",
|
||||
completed: "Task complete",
|
||||
failed: "Task failed",
|
||||
cancelled: "Task cancelled",
|
||||
timed_out: "Task timed out",
|
||||
},
|
||||
eventsLabel: "Zdarzenia karty",
|
||||
eventCreated: "Utworzono",
|
||||
eventEdited: "Edytowano",
|
||||
@@ -594,25 +606,6 @@ export const pl: TranslationMap = {
|
||||
eventArchived: "Zarchiwizowano",
|
||||
eventUnarchived: "Przywrócono z archiwum",
|
||||
eventStale: "Nieaktualna sesja",
|
||||
gameButton: "Minigra",
|
||||
gameTitle: "Pościg za kartą",
|
||||
gameStart: "Dotrzyj do pola uruchomienia.",
|
||||
gameBoundary: "Osiągnięto granicę.",
|
||||
gameBlocked: "Zablokowane.",
|
||||
gameContinue: "Kontynuuj.",
|
||||
gameWin: "Uruchomienie zakończone.",
|
||||
gameMoves: "Ruchy {count}",
|
||||
gameWins: "Wygrane {count}",
|
||||
gameBoard: "Plansza Card Chase",
|
||||
gameControls: "Sterowanie Card Chase",
|
||||
gameAgent: "Agent",
|
||||
gameLaunch: "Uruchom",
|
||||
gameBlockedCell: "Zablokowane",
|
||||
gameOpenCell: "Otwarte",
|
||||
gameMoveUp: "Przesuń w górę",
|
||||
gameMoveLeft: "Przesuń w lewo",
|
||||
gameMoveDown: "Przesuń w dół",
|
||||
gameMoveRight: "Przesuń w prawo",
|
||||
},
|
||||
overview: {
|
||||
access: {
|
||||
|
||||
31
ui/src/i18n/locales/pt-BR.ts
generated
31
ui/src/i18n/locales/pt-BR.ts
generated
@@ -516,6 +516,9 @@ export const pt_BR: TranslationMap = {
|
||||
runDefaultAgent: "Executar agente padrão",
|
||||
start: "Iniciar",
|
||||
dispatch: "Acionar despachante",
|
||||
dispatchSummary:
|
||||
"Dispatch complete: started {started}, promoted {promoted}, blocked {blocked}, reclaimed {reclaimed}, orchestrated {orchestrated}, failures {failures}.",
|
||||
dispatchSummaryEmpty: "Dispatch complete: no ready work changed.",
|
||||
live: "ao vivo",
|
||||
fieldTitle: "Título",
|
||||
fieldNotes: "Notas",
|
||||
@@ -544,6 +547,7 @@ export const pt_BR: TranslationMap = {
|
||||
badgeTenant: "locatário {tenant}",
|
||||
badgeSkills: "{count} habilidades",
|
||||
badgeDispatches: "{count} despachos",
|
||||
badgeTaskLinked: "task linked",
|
||||
badgeClaimed: "reivindicado por {owner}",
|
||||
badgeDiagnostics: "{count} diagnósticos",
|
||||
badgeStale: "desatualizado",
|
||||
@@ -567,6 +571,14 @@ export const pt_BR: TranslationMap = {
|
||||
lifecycleDoneDetail: "Movido para revisão",
|
||||
lifecycleNeedsReview: "Precisa de revisão",
|
||||
lifecycleNeedsReviewDetail: "A execução foi interrompida ou falhou",
|
||||
taskStatus: {
|
||||
queued: "Task queued",
|
||||
running: "Task running",
|
||||
completed: "Task complete",
|
||||
failed: "Task failed",
|
||||
cancelled: "Task cancelled",
|
||||
timed_out: "Task timed out",
|
||||
},
|
||||
eventsLabel: "Eventos do cartão",
|
||||
eventCreated: "Criado",
|
||||
eventEdited: "Editado",
|
||||
@@ -593,25 +605,6 @@ export const pt_BR: TranslationMap = {
|
||||
eventArchived: "Arquivado",
|
||||
eventUnarchived: "Desarquivado",
|
||||
eventStale: "Sessão obsoleta",
|
||||
gameButton: "Mini game",
|
||||
gameTitle: "Card Chase",
|
||||
gameStart: "Alcance o bloco de lançamento.",
|
||||
gameBoundary: "Limite alcançado.",
|
||||
gameBlocked: "Bloqueado.",
|
||||
gameContinue: "Continue.",
|
||||
gameWin: "Lançamento concluído.",
|
||||
gameMoves: "Movimentos {count}",
|
||||
gameWins: "Vitórias {count}",
|
||||
gameBoard: "Tabuleiro do Card Chase",
|
||||
gameControls: "Controles do Card Chase",
|
||||
gameAgent: "Agente",
|
||||
gameLaunch: "Lançamento",
|
||||
gameBlockedCell: "Bloqueado",
|
||||
gameOpenCell: "Aberto",
|
||||
gameMoveUp: "Mover para cima",
|
||||
gameMoveLeft: "Mover para a esquerda",
|
||||
gameMoveDown: "Mover para baixo",
|
||||
gameMoveRight: "Mover para a direita",
|
||||
},
|
||||
overview: {
|
||||
access: {
|
||||
|
||||
31
ui/src/i18n/locales/th.ts
generated
31
ui/src/i18n/locales/th.ts
generated
@@ -514,6 +514,9 @@ export const th: TranslationMap = {
|
||||
runDefaultAgent: "เรียกใช้เอเจนต์เริ่มต้น",
|
||||
start: "เริ่ม",
|
||||
dispatch: "กระตุ้น dispatcher",
|
||||
dispatchSummary:
|
||||
"Dispatch complete: started {started}, promoted {promoted}, blocked {blocked}, reclaimed {reclaimed}, orchestrated {orchestrated}, failures {failures}.",
|
||||
dispatchSummaryEmpty: "Dispatch complete: no ready work changed.",
|
||||
live: "สด",
|
||||
fieldTitle: "ชื่อเรื่อง",
|
||||
fieldNotes: "บันทึก",
|
||||
@@ -542,6 +545,7 @@ export const th: TranslationMap = {
|
||||
badgeTenant: "ผู้เช่า {tenant}",
|
||||
badgeSkills: "{count} ทักษะ",
|
||||
badgeDispatches: "ส่งงาน {count} ครั้ง",
|
||||
badgeTaskLinked: "task linked",
|
||||
badgeClaimed: "ถูกอ้างสิทธิ์โดย {owner}",
|
||||
badgeDiagnostics: "การวินิจฉัย {count} รายการ",
|
||||
badgeStale: "ค้างนาน",
|
||||
@@ -565,6 +569,14 @@ export const th: TranslationMap = {
|
||||
lifecycleDoneDetail: "ย้ายไปยังการตรวจทานแล้ว",
|
||||
lifecycleNeedsReview: "ต้องตรวจทาน",
|
||||
lifecycleNeedsReviewDetail: "การรันหยุดหรือไม่สำเร็จ",
|
||||
taskStatus: {
|
||||
queued: "Task queued",
|
||||
running: "Task running",
|
||||
completed: "Task complete",
|
||||
failed: "Task failed",
|
||||
cancelled: "Task cancelled",
|
||||
timed_out: "Task timed out",
|
||||
},
|
||||
eventsLabel: "เหตุการณ์ของการ์ด",
|
||||
eventCreated: "สร้างแล้ว",
|
||||
eventEdited: "แก้ไขแล้ว",
|
||||
@@ -591,25 +603,6 @@ export const th: TranslationMap = {
|
||||
eventArchived: "เก็บถาวรแล้ว",
|
||||
eventUnarchived: "ยกเลิกการเก็บถาวรแล้ว",
|
||||
eventStale: "เซสชันหมดอายุ",
|
||||
gameButton: "มินิเกม",
|
||||
gameTitle: "Card Chase",
|
||||
gameStart: "ไปให้ถึงช่องเปิดตัว",
|
||||
gameBoundary: "ถึงขอบเขตแล้ว",
|
||||
gameBlocked: "ถูกบล็อก",
|
||||
gameContinue: "ไปต่อ",
|
||||
gameWin: "เปิดตัวสำเร็จ",
|
||||
gameMoves: "การเดิน {count}",
|
||||
gameWins: "ชนะ {count}",
|
||||
gameBoard: "กระดาน Card Chase",
|
||||
gameControls: "ตัวควบคุม Card Chase",
|
||||
gameAgent: "Agent",
|
||||
gameLaunch: "เปิดใช้งาน",
|
||||
gameBlockedCell: "ถูกบล็อก",
|
||||
gameOpenCell: "เปิด",
|
||||
gameMoveUp: "เลื่อนขึ้น",
|
||||
gameMoveLeft: "เลื่อนไปทางซ้าย",
|
||||
gameMoveDown: "เลื่อนลง",
|
||||
gameMoveRight: "เลื่อนไปทางขวา",
|
||||
},
|
||||
overview: {
|
||||
access: {
|
||||
|
||||
31
ui/src/i18n/locales/tr.ts
generated
31
ui/src/i18n/locales/tr.ts
generated
@@ -519,6 +519,9 @@ export const tr: TranslationMap = {
|
||||
runDefaultAgent: "Varsayılan ajanı çalıştır",
|
||||
start: "Başlat",
|
||||
dispatch: "Dağıtıcıyı dürt",
|
||||
dispatchSummary:
|
||||
"Dispatch complete: started {started}, promoted {promoted}, blocked {blocked}, reclaimed {reclaimed}, orchestrated {orchestrated}, failures {failures}.",
|
||||
dispatchSummaryEmpty: "Dispatch complete: no ready work changed.",
|
||||
live: "canlı",
|
||||
fieldTitle: "Başlık",
|
||||
fieldNotes: "Notlar",
|
||||
@@ -547,6 +550,7 @@ export const tr: TranslationMap = {
|
||||
badgeTenant: "kiracı {tenant}",
|
||||
badgeSkills: "{count} beceri",
|
||||
badgeDispatches: "{count} gönderim",
|
||||
badgeTaskLinked: "task linked",
|
||||
badgeClaimed: "{owner} tarafından üstlenildi",
|
||||
badgeDiagnostics: "{count} tanılama",
|
||||
badgeStale: "eski",
|
||||
@@ -570,6 +574,14 @@ export const tr: TranslationMap = {
|
||||
lifecycleDoneDetail: "İncelemeye taşındı",
|
||||
lifecycleNeedsReview: "İnceleme gerekli",
|
||||
lifecycleNeedsReviewDetail: "Çalışma durduruldu veya başarısız oldu",
|
||||
taskStatus: {
|
||||
queued: "Task queued",
|
||||
running: "Task running",
|
||||
completed: "Task complete",
|
||||
failed: "Task failed",
|
||||
cancelled: "Task cancelled",
|
||||
timed_out: "Task timed out",
|
||||
},
|
||||
eventsLabel: "Kart olayları",
|
||||
eventCreated: "Oluşturuldu",
|
||||
eventEdited: "Düzenlendi",
|
||||
@@ -596,25 +608,6 @@ export const tr: TranslationMap = {
|
||||
eventArchived: "Arşivlendi",
|
||||
eventUnarchived: "Arşivden çıkarıldı",
|
||||
eventStale: "Eski oturum",
|
||||
gameButton: "Mini oyun",
|
||||
gameTitle: "Kart Takibi",
|
||||
gameStart: "Başlatma karesine ulaşın.",
|
||||
gameBoundary: "Sınıra ulaşıldı.",
|
||||
gameBlocked: "Engellendi.",
|
||||
gameContinue: "Devam edin.",
|
||||
gameWin: "Başlatma tamamlandı.",
|
||||
gameMoves: "Hamleler {count}",
|
||||
gameWins: "Galibiyetler {count}",
|
||||
gameBoard: "Kart Takibi tahtası",
|
||||
gameControls: "Kart Takibi kontrolleri",
|
||||
gameAgent: "Aracı",
|
||||
gameLaunch: "Başlat",
|
||||
gameBlockedCell: "Engelli",
|
||||
gameOpenCell: "Açık",
|
||||
gameMoveUp: "Yukarı git",
|
||||
gameMoveLeft: "Sola git",
|
||||
gameMoveDown: "Aşağı git",
|
||||
gameMoveRight: "Sağa git",
|
||||
},
|
||||
overview: {
|
||||
access: {
|
||||
|
||||
31
ui/src/i18n/locales/uk.ts
generated
31
ui/src/i18n/locales/uk.ts
generated
@@ -518,6 +518,9 @@ export const uk: TranslationMap = {
|
||||
runDefaultAgent: "Запустити агента за замовчуванням",
|
||||
start: "Почати",
|
||||
dispatch: "Підштовхнути диспетчер",
|
||||
dispatchSummary:
|
||||
"Dispatch complete: started {started}, promoted {promoted}, blocked {blocked}, reclaimed {reclaimed}, orchestrated {orchestrated}, failures {failures}.",
|
||||
dispatchSummaryEmpty: "Dispatch complete: no ready work changed.",
|
||||
live: "наживо",
|
||||
fieldTitle: "Заголовок",
|
||||
fieldNotes: "Нотатки",
|
||||
@@ -546,6 +549,7 @@ export const uk: TranslationMap = {
|
||||
badgeTenant: "орендар {tenant}",
|
||||
badgeSkills: "{count} навичок",
|
||||
badgeDispatches: "{count} відправлень",
|
||||
badgeTaskLinked: "task linked",
|
||||
badgeClaimed: "призначено {owner}",
|
||||
badgeDiagnostics: "{count} діагностик",
|
||||
badgeStale: "застаріле",
|
||||
@@ -569,6 +573,14 @@ export const uk: TranslationMap = {
|
||||
lifecycleDoneDetail: "Переміщено на перевірку",
|
||||
lifecycleNeedsReview: "Потребує перевірки",
|
||||
lifecycleNeedsReviewDetail: "Запуск зупинено або він завершився помилкою",
|
||||
taskStatus: {
|
||||
queued: "Task queued",
|
||||
running: "Task running",
|
||||
completed: "Task complete",
|
||||
failed: "Task failed",
|
||||
cancelled: "Task cancelled",
|
||||
timed_out: "Task timed out",
|
||||
},
|
||||
eventsLabel: "Події картки",
|
||||
eventCreated: "Створено",
|
||||
eventEdited: "Змінено",
|
||||
@@ -595,25 +607,6 @@ export const uk: TranslationMap = {
|
||||
eventArchived: "Заархівовано",
|
||||
eventUnarchived: "Розархівовано",
|
||||
eventStale: "Застарілий сеанс",
|
||||
gameButton: "Мінігра",
|
||||
gameTitle: "Card Chase",
|
||||
gameStart: "Дістаньтеся клітинки запуску.",
|
||||
gameBoundary: "Досягнуто межі.",
|
||||
gameBlocked: "Заблоковано.",
|
||||
gameContinue: "Продовжуйте.",
|
||||
gameWin: "Запуск завершено.",
|
||||
gameMoves: "Ходи: {count}",
|
||||
gameWins: "Перемоги: {count}",
|
||||
gameBoard: "Ігрове поле Card Chase",
|
||||
gameControls: "Керування Card Chase",
|
||||
gameAgent: "Агент",
|
||||
gameLaunch: "Запуск",
|
||||
gameBlockedCell: "Заблоковано",
|
||||
gameOpenCell: "Відкрито",
|
||||
gameMoveUp: "Рух угору",
|
||||
gameMoveLeft: "Рух ліворуч",
|
||||
gameMoveDown: "Рух униз",
|
||||
gameMoveRight: "Рух праворуч",
|
||||
},
|
||||
overview: {
|
||||
access: {
|
||||
|
||||
31
ui/src/i18n/locales/vi.ts
generated
31
ui/src/i18n/locales/vi.ts
generated
@@ -517,6 +517,9 @@ export const vi: TranslationMap = {
|
||||
runDefaultAgent: "Chạy agent mặc định",
|
||||
start: "Bắt đầu",
|
||||
dispatch: "Nhắc dispatcher",
|
||||
dispatchSummary:
|
||||
"Dispatch complete: started {started}, promoted {promoted}, blocked {blocked}, reclaimed {reclaimed}, orchestrated {orchestrated}, failures {failures}.",
|
||||
dispatchSummaryEmpty: "Dispatch complete: no ready work changed.",
|
||||
live: "trực tiếp",
|
||||
fieldTitle: "Tiêu đề",
|
||||
fieldNotes: "Ghi chú",
|
||||
@@ -545,6 +548,7 @@ export const vi: TranslationMap = {
|
||||
badgeTenant: "đối tượng thuê {tenant}",
|
||||
badgeSkills: "{count} kỹ năng",
|
||||
badgeDispatches: "{count} lượt điều phối",
|
||||
badgeTaskLinked: "task linked",
|
||||
badgeClaimed: "được nhận bởi {owner}",
|
||||
badgeDiagnostics: "{count} chẩn đoán",
|
||||
badgeStale: "cũ",
|
||||
@@ -568,6 +572,14 @@ export const vi: TranslationMap = {
|
||||
lifecycleDoneDetail: "Đã chuyển sang xem xét",
|
||||
lifecycleNeedsReview: "Cần xem xét",
|
||||
lifecycleNeedsReviewDetail: "Lượt chạy đã dừng hoặc thất bại",
|
||||
taskStatus: {
|
||||
queued: "Task queued",
|
||||
running: "Task running",
|
||||
completed: "Task complete",
|
||||
failed: "Task failed",
|
||||
cancelled: "Task cancelled",
|
||||
timed_out: "Task timed out",
|
||||
},
|
||||
eventsLabel: "Sự kiện thẻ",
|
||||
eventCreated: "Đã tạo",
|
||||
eventEdited: "Đã chỉnh sửa",
|
||||
@@ -594,25 +606,6 @@ export const vi: TranslationMap = {
|
||||
eventArchived: "Đã lưu trữ",
|
||||
eventUnarchived: "Đã bỏ lưu trữ",
|
||||
eventStale: "Phiên đã cũ",
|
||||
gameButton: "Trò chơi nhỏ",
|
||||
gameTitle: "Đuổi bắt thẻ",
|
||||
gameStart: "Đến ô khởi chạy.",
|
||||
gameBoundary: "Đã đến ranh giới.",
|
||||
gameBlocked: "Bị chặn.",
|
||||
gameContinue: "Tiếp tục.",
|
||||
gameWin: "Đã vượt qua khởi chạy.",
|
||||
gameMoves: "Lượt đi {count}",
|
||||
gameWins: "Thắng {count}",
|
||||
gameBoard: "Bảng Card Chase",
|
||||
gameControls: "Điều khiển Card Chase",
|
||||
gameAgent: "Tác nhân",
|
||||
gameLaunch: "Khởi chạy",
|
||||
gameBlockedCell: "Bị chặn",
|
||||
gameOpenCell: "Mở",
|
||||
gameMoveUp: "Di chuyển lên",
|
||||
gameMoveLeft: "Di chuyển sang trái",
|
||||
gameMoveDown: "Di chuyển xuống",
|
||||
gameMoveRight: "Di chuyển sang phải",
|
||||
},
|
||||
overview: {
|
||||
access: {
|
||||
|
||||
31
ui/src/i18n/locales/zh-CN.ts
generated
31
ui/src/i18n/locales/zh-CN.ts
generated
@@ -513,6 +513,9 @@ export const zh_CN: TranslationMap = {
|
||||
runDefaultAgent: "运行默认代理",
|
||||
start: "开始",
|
||||
dispatch: "提醒调度器",
|
||||
dispatchSummary:
|
||||
"Dispatch complete: started {started}, promoted {promoted}, blocked {blocked}, reclaimed {reclaimed}, orchestrated {orchestrated}, failures {failures}.",
|
||||
dispatchSummaryEmpty: "Dispatch complete: no ready work changed.",
|
||||
live: "实时",
|
||||
fieldTitle: "标题",
|
||||
fieldNotes: "备注",
|
||||
@@ -541,6 +544,7 @@ export const zh_CN: TranslationMap = {
|
||||
badgeTenant: "租户 {tenant}",
|
||||
badgeSkills: "{count} 个技能",
|
||||
badgeDispatches: "{count} 次调度",
|
||||
badgeTaskLinked: "task linked",
|
||||
badgeClaimed: "由 {owner} 认领",
|
||||
badgeDiagnostics: "{count} 个诊断",
|
||||
badgeStale: "已过期",
|
||||
@@ -564,6 +568,14 @@ export const zh_CN: TranslationMap = {
|
||||
lifecycleDoneDetail: "已移至审核",
|
||||
lifecycleNeedsReview: "需要审核",
|
||||
lifecycleNeedsReviewDetail: "运行已停止或失败",
|
||||
taskStatus: {
|
||||
queued: "Task queued",
|
||||
running: "Task running",
|
||||
completed: "Task complete",
|
||||
failed: "Task failed",
|
||||
cancelled: "Task cancelled",
|
||||
timed_out: "Task timed out",
|
||||
},
|
||||
eventsLabel: "卡片事件",
|
||||
eventCreated: "已创建",
|
||||
eventEdited: "已编辑",
|
||||
@@ -590,25 +602,6 @@ export const zh_CN: TranslationMap = {
|
||||
eventArchived: "已归档",
|
||||
eventUnarchived: "已取消归档",
|
||||
eventStale: "过期会话",
|
||||
gameButton: "迷你游戏",
|
||||
gameTitle: "卡片追逐",
|
||||
gameStart: "到达发布图块。",
|
||||
gameBoundary: "已到达边界。",
|
||||
gameBlocked: "已阻挡。",
|
||||
gameContinue: "继续前进。",
|
||||
gameWin: "发布已完成。",
|
||||
gameMoves: "移动 {count}",
|
||||
gameWins: "获胜 {count}",
|
||||
gameBoard: "卡片追逐棋盘",
|
||||
gameControls: "卡片追逐控制",
|
||||
gameAgent: "代理",
|
||||
gameLaunch: "发布",
|
||||
gameBlockedCell: "已阻挡",
|
||||
gameOpenCell: "开放",
|
||||
gameMoveUp: "向上移动",
|
||||
gameMoveLeft: "向左移动",
|
||||
gameMoveDown: "向下移动",
|
||||
gameMoveRight: "向右移动",
|
||||
},
|
||||
overview: {
|
||||
access: {
|
||||
|
||||
31
ui/src/i18n/locales/zh-TW.ts
generated
31
ui/src/i18n/locales/zh-TW.ts
generated
@@ -513,6 +513,9 @@ export const zh_TW: TranslationMap = {
|
||||
runDefaultAgent: "執行預設代理程式",
|
||||
start: "開始",
|
||||
dispatch: "提醒分派器",
|
||||
dispatchSummary:
|
||||
"Dispatch complete: started {started}, promoted {promoted}, blocked {blocked}, reclaimed {reclaimed}, orchestrated {orchestrated}, failures {failures}.",
|
||||
dispatchSummaryEmpty: "Dispatch complete: no ready work changed.",
|
||||
live: "即時",
|
||||
fieldTitle: "標題",
|
||||
fieldNotes: "備註",
|
||||
@@ -541,6 +544,7 @@ export const zh_TW: TranslationMap = {
|
||||
badgeTenant: "租戶 {tenant}",
|
||||
badgeSkills: "{count} 個技能",
|
||||
badgeDispatches: "{count} 次調度",
|
||||
badgeTaskLinked: "task linked",
|
||||
badgeClaimed: "由 {owner} 認領",
|
||||
badgeDiagnostics: "{count} 個診斷",
|
||||
badgeStale: "過期",
|
||||
@@ -564,6 +568,14 @@ export const zh_TW: TranslationMap = {
|
||||
lifecycleDoneDetail: "已移至審查",
|
||||
lifecycleNeedsReview: "需要審查",
|
||||
lifecycleNeedsReviewDetail: "執行已停止或失敗",
|
||||
taskStatus: {
|
||||
queued: "Task queued",
|
||||
running: "Task running",
|
||||
completed: "Task complete",
|
||||
failed: "Task failed",
|
||||
cancelled: "Task cancelled",
|
||||
timed_out: "Task timed out",
|
||||
},
|
||||
eventsLabel: "卡片事件",
|
||||
eventCreated: "已建立",
|
||||
eventEdited: "已編輯",
|
||||
@@ -590,25 +602,6 @@ export const zh_TW: TranslationMap = {
|
||||
eventArchived: "已封存",
|
||||
eventUnarchived: "已取消封存",
|
||||
eventStale: "過期工作階段",
|
||||
gameButton: "小遊戲",
|
||||
gameTitle: "卡片追逐",
|
||||
gameStart: "到達啟動方格。",
|
||||
gameBoundary: "已到達邊界。",
|
||||
gameBlocked: "已封鎖。",
|
||||
gameContinue: "繼續前進。",
|
||||
gameWin: "啟動已完成。",
|
||||
gameMoves: "移動 {count}",
|
||||
gameWins: "獲勝 {count}",
|
||||
gameBoard: "卡片追逐棋盤",
|
||||
gameControls: "卡片追逐控制項",
|
||||
gameAgent: "代理",
|
||||
gameLaunch: "啟動",
|
||||
gameBlockedCell: "已封鎖",
|
||||
gameOpenCell: "開放",
|
||||
gameMoveUp: "向上移動",
|
||||
gameMoveLeft: "向左移動",
|
||||
gameMoveDown: "向下移動",
|
||||
gameMoveRight: "向右移動",
|
||||
},
|
||||
overview: {
|
||||
access: {
|
||||
|
||||
@@ -129,21 +129,6 @@
|
||||
box-shadow: var(--shadow-xl);
|
||||
}
|
||||
|
||||
.workboard-game {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
width: min(620px, calc(100vw - 44px));
|
||||
padding: 16px;
|
||||
border: 1px solid color-mix(in srgb, var(--border) 86%, transparent);
|
||||
border-radius: 10px;
|
||||
background: color-mix(in srgb, var(--panel) 96%, var(--bg) 4%);
|
||||
box-shadow: var(--shadow-xl);
|
||||
}
|
||||
|
||||
.workboard-game:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.workboard-modal__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -233,159 +218,12 @@
|
||||
}
|
||||
|
||||
.workboard-toolbar .btn,
|
||||
.workboard-draft .btn,
|
||||
.workboard-game .btn {
|
||||
.workboard-draft .btn {
|
||||
min-height: var(--workboard-control-height);
|
||||
padding: 0 12px;
|
||||
border-radius: var(--workboard-control-radius);
|
||||
}
|
||||
|
||||
.workboard-game__stats {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.workboard-game__stats span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
border-radius: 6px;
|
||||
padding: 2px 8px;
|
||||
background: color-mix(in srgb, var(--border) 58%, transparent);
|
||||
color: var(--muted);
|
||||
font-size: 0.76rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.workboard-game__scene {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: min(58vh, 430px);
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 24% 20%,
|
||||
color-mix(in srgb, var(--accent) 24%, transparent),
|
||||
transparent 30%
|
||||
),
|
||||
linear-gradient(180deg, color-mix(in srgb, var(--bg-elevated) 92%, #10171d), var(--bg));
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--border) 72%, transparent);
|
||||
}
|
||||
|
||||
.workboard-game__canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.workboard-game__fallback {
|
||||
position: absolute;
|
||||
inset: 12%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
transform: perspective(720px) rotateX(58deg) rotateZ(-35deg);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.workboard-game__scene--ready .workboard-game__fallback {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.workboard-game__accessible-grid {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.workboard-game__fallback-tile {
|
||||
min-width: 0;
|
||||
border: 1px solid color-mix(in srgb, var(--border) 82%, transparent);
|
||||
border-radius: 7px;
|
||||
background: color-mix(in srgb, var(--bg) 84%, var(--panel) 16%);
|
||||
box-shadow:
|
||||
0 10px 0 color-mix(in srgb, #000 28%, transparent),
|
||||
inset 0 1px 0 color-mix(in srgb, white 8%, transparent);
|
||||
}
|
||||
|
||||
.workboard-game__fallback-tile--player {
|
||||
border-color: color-mix(in srgb, var(--accent) 54%, var(--border));
|
||||
background: color-mix(in srgb, var(--accent) 30%, var(--bg));
|
||||
}
|
||||
|
||||
.workboard-game__fallback-tile--goal {
|
||||
border-color: color-mix(in srgb, var(--ok) 48%, var(--border));
|
||||
background: color-mix(in srgb, var(--ok) 24%, var(--bg));
|
||||
}
|
||||
|
||||
.workboard-game__fallback-tile--blocker {
|
||||
background: color-mix(in srgb, var(--danger) 32%, var(--bg));
|
||||
}
|
||||
|
||||
.workboard-game__controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 38px);
|
||||
grid-template-areas:
|
||||
". up ."
|
||||
"left down right";
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.workboard-game__arrow {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.workboard-game__arrow svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.workboard-game__arrow--up {
|
||||
grid-area: up;
|
||||
}
|
||||
|
||||
.workboard-game__arrow--left {
|
||||
grid-area: left;
|
||||
}
|
||||
|
||||
.workboard-game__controls
|
||||
.workboard-game__arrow:not(
|
||||
.workboard-game__arrow--up,
|
||||
.workboard-game__arrow--left,
|
||||
.workboard-game__arrow--right
|
||||
) {
|
||||
grid-area: down;
|
||||
}
|
||||
|
||||
.workboard-game__arrow--right {
|
||||
grid-area: right;
|
||||
}
|
||||
|
||||
.workboard-game__arrow--up svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.workboard-game__arrow--left svg {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.workboard-game__arrow--right svg {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.workboard-board {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
|
||||
@@ -15,13 +15,4 @@ describe("workboard styles", () => {
|
||||
expect(css).toContain("grid-auto-columns: minmax(260px, 82vw);");
|
||||
expect(css).not.toContain("grid-template-columns: repeat(6");
|
||||
});
|
||||
|
||||
it("keeps the mini game scene framed for a 3d canvas and fallback board", () => {
|
||||
const css = readWorkboardCss();
|
||||
|
||||
expect(css).toContain(".workboard-game__scene {\n position: relative;");
|
||||
expect(css).toContain("min-height: min(58vh, 430px);");
|
||||
expect(css).toContain(".workboard-game__canvas {\n position: absolute;");
|
||||
expect(css).toContain("transform: perspective(720px) rotateX(58deg) rotateZ(-35deg);");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
dismissChatError,
|
||||
switchChatSession,
|
||||
} from "./app-render.helpers.ts";
|
||||
import { hasOperatorWriteAccess, warnQueryToken } from "./app-settings.ts";
|
||||
import { hasOperatorAdminAccess, hasOperatorWriteAccess, warnQueryToken } from "./app-settings.ts";
|
||||
import type { AppViewState } from "./app-view-state.ts";
|
||||
import { reconcileChatRunLifecycle } from "./chat/run-lifecycle.ts";
|
||||
import {
|
||||
@@ -2491,15 +2491,16 @@ export function renderApp(state: AppViewState) {
|
||||
})
|
||||
: nothing}
|
||||
${state.tab === "workboard"
|
||||
? renderLazyView(lazyWorkboard, (m) =>
|
||||
m.renderWorkboard({
|
||||
? renderLazyView(lazyWorkboard, (m) => {
|
||||
const auth =
|
||||
(state.hello as { auth?: { role?: string; scopes?: string[] } } | null)?.auth ??
|
||||
null;
|
||||
return m.renderWorkboard({
|
||||
host: state,
|
||||
client: state.client,
|
||||
connected: state.connected,
|
||||
canWrite: hasOperatorWriteAccess(
|
||||
(state.hello as { auth?: { role?: string; scopes?: string[] } } | null)?.auth ??
|
||||
null,
|
||||
),
|
||||
canWrite: hasOperatorWriteAccess(auth),
|
||||
canModelOverride: hasOperatorAdminAccess(auth),
|
||||
pluginEnabled: isPluginEnabledInConfigSnapshot(state.configSnapshot, "workboard", {
|
||||
enabledByDefault: false,
|
||||
}),
|
||||
@@ -2510,8 +2511,8 @@ export function renderApp(state: AppViewState) {
|
||||
state.setTab("chat" as import("./navigation.ts").Tab);
|
||||
},
|
||||
onRequestUpdate: requestHostUpdate,
|
||||
}),
|
||||
)
|
||||
});
|
||||
})
|
||||
: nothing}
|
||||
${renderUsageTab(state)}
|
||||
${state.tab === "cron" ? renderCronQuickCreateForTab(state, requestHostUpdate) : nothing}
|
||||
|
||||
@@ -824,6 +824,19 @@ export function hasOperatorWriteAccess(
|
||||
});
|
||||
}
|
||||
|
||||
export function hasOperatorAdminAccess(
|
||||
auth: { role?: string; scopes?: readonly string[] } | null,
|
||||
): boolean {
|
||||
if (!auth?.scopes) {
|
||||
return true;
|
||||
}
|
||||
return roleScopesAllow({
|
||||
role: auth.role ?? "operator",
|
||||
requestedScopes: ["operator.admin"],
|
||||
allowedScopes: auth.scopes,
|
||||
});
|
||||
}
|
||||
|
||||
export function hasMissingSkillDependencies(
|
||||
missing: Record<string, unknown> | null | undefined,
|
||||
): boolean {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { GatewaySessionRow } from "../types.ts";
|
||||
import {
|
||||
archiveWorkboardCard,
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
stopWorkboardCard,
|
||||
syncWorkboardLifecycle,
|
||||
type WorkboardCard,
|
||||
type WorkboardTaskSummary,
|
||||
} from "./workboard.ts";
|
||||
|
||||
function createClient(
|
||||
@@ -56,7 +57,22 @@ const sampleSession: GatewaySessionRow = {
|
||||
status: "running",
|
||||
};
|
||||
|
||||
const sampleTaskSessionKey = "subagent:workboard-default-card-1";
|
||||
const sampleTask = {
|
||||
id: "task-1",
|
||||
taskId: "task-1",
|
||||
status: "running",
|
||||
title: "Build board",
|
||||
childSessionKey: sampleTaskSessionKey,
|
||||
runId: "run-1",
|
||||
updatedAt: 2,
|
||||
} satisfies WorkboardTaskSummary;
|
||||
|
||||
describe("workboard controller", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("loads cards through the plugin gateway method", async () => {
|
||||
const host = {};
|
||||
const client = createClient({
|
||||
@@ -69,6 +85,111 @@ describe("workboard controller", () => {
|
||||
expect(getWorkboardState(host).cards).toEqual([sampleCard]);
|
||||
});
|
||||
|
||||
it("links loaded cards to matching Gateway tasks", async () => {
|
||||
const host = {};
|
||||
const linked = {
|
||||
...sampleCard,
|
||||
sessionKey: sampleTaskSessionKey,
|
||||
runId: "run-1",
|
||||
} satisfies WorkboardCard;
|
||||
const client = createClient({
|
||||
"workboard.cards.list": { cards: [linked], statuses: ["todo", "done"] },
|
||||
"tasks.list": { tasks: [sampleTask] },
|
||||
});
|
||||
|
||||
await loadWorkboard({ host, client: client as never, force: true });
|
||||
|
||||
const state = getWorkboardState(host);
|
||||
expect(client.request).toHaveBeenCalledWith("tasks.list", { limit: 500 });
|
||||
expect(state.cards[0]).toMatchObject({ id: "card-1", taskId: "task-1" });
|
||||
expect(state.tasksByCardId.get("card-1")).toMatchObject({
|
||||
taskId: "task-1",
|
||||
status: "running",
|
||||
});
|
||||
});
|
||||
|
||||
it("links cards from paginated Gateway task results", async () => {
|
||||
const host = {};
|
||||
const linked = {
|
||||
...sampleCard,
|
||||
sessionKey: sampleTaskSessionKey,
|
||||
runId: "run-1",
|
||||
} satisfies WorkboardCard;
|
||||
const client = createClient((method, params) => {
|
||||
if (method === "workboard.cards.list") {
|
||||
return { cards: [linked], statuses: ["todo", "done"] };
|
||||
}
|
||||
if (method === "tasks.list" && (params as { cursor?: string }).cursor === "page-2") {
|
||||
return { tasks: [sampleTask] };
|
||||
}
|
||||
if (method === "tasks.list") {
|
||||
return { tasks: [], nextCursor: "page-2" };
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
await loadWorkboard({ host, client: client as never, force: true });
|
||||
|
||||
expect(client.request).toHaveBeenCalledWith("tasks.list", { limit: 500 });
|
||||
expect(client.request).toHaveBeenCalledWith("tasks.list", {
|
||||
limit: 500,
|
||||
cursor: "page-2",
|
||||
});
|
||||
expect(getWorkboardState(host).cards[0]).toMatchObject({ taskId: "task-1" });
|
||||
});
|
||||
|
||||
it("links unassigned default-agent tasks with canonicalized session keys", async () => {
|
||||
const host = {};
|
||||
const linked = {
|
||||
...sampleCard,
|
||||
sessionKey: sampleTaskSessionKey,
|
||||
runId: "run-1",
|
||||
} satisfies WorkboardCard;
|
||||
const client = createClient({
|
||||
"workboard.cards.list": { cards: [linked], statuses: ["todo", "done"] },
|
||||
"tasks.list": {
|
||||
tasks: [
|
||||
{
|
||||
...sampleTask,
|
||||
childSessionKey: `agent:main:${sampleTaskSessionKey}`,
|
||||
runId: "run-1",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await loadWorkboard({ host, client: client as never, force: true });
|
||||
|
||||
expect(getWorkboardState(host).cards[0]).toMatchObject({ taskId: "task-1" });
|
||||
});
|
||||
|
||||
it("does not relink a loaded card to a stale task from another session", async () => {
|
||||
const host = {};
|
||||
const linked = {
|
||||
...sampleCard,
|
||||
sessionKey: "agent:main:dashboard:new",
|
||||
runId: "run-1",
|
||||
} satisfies WorkboardCard;
|
||||
const client = createClient({
|
||||
"workboard.cards.list": { cards: [linked], statuses: ["todo", "done"] },
|
||||
"tasks.list": {
|
||||
tasks: [
|
||||
{
|
||||
...sampleTask,
|
||||
childSessionKey: sampleTaskSessionKey,
|
||||
runId: "run-1",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await loadWorkboard({ host, client: client as never, force: true });
|
||||
|
||||
const state = getWorkboardState(host);
|
||||
expect(state.cards[0]).not.toHaveProperty("taskId");
|
||||
expect(state.tasksByCardId.has("card-1")).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves automation metadata loaded from the plugin gateway method", async () => {
|
||||
const host = {};
|
||||
const client = createClient({
|
||||
@@ -428,11 +549,18 @@ describe("workboard controller", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("starts a session and links it back to the card", async () => {
|
||||
it("starts a task run and links it back to the card", async () => {
|
||||
const host = {};
|
||||
const running = { ...sampleCard, status: "running", sessionKey: "agent:main:dashboard:1" };
|
||||
const running = {
|
||||
...sampleCard,
|
||||
status: "running",
|
||||
sessionKey: sampleTaskSessionKey,
|
||||
runId: "run-1",
|
||||
taskId: "task-1",
|
||||
};
|
||||
const client = createClient({
|
||||
"sessions.create": { key: "agent:main:dashboard:1", runId: "run-1" },
|
||||
agent: { sessionKey: sampleTaskSessionKey, runId: "run-1" },
|
||||
"tasks.list": { tasks: [sampleTask] },
|
||||
"workboard.cards.update": { card: running },
|
||||
});
|
||||
|
||||
@@ -442,7 +570,7 @@ describe("workboard controller", () => {
|
||||
card: sampleCard,
|
||||
});
|
||||
|
||||
expect(sessionKey).toBe("agent:main:dashboard:1");
|
||||
expect(sessionKey).toBe(sampleTaskSessionKey);
|
||||
expect(client.request).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"workboard.cards.update",
|
||||
@@ -453,25 +581,109 @@ describe("workboard controller", () => {
|
||||
);
|
||||
expect(client.request).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"sessions.create",
|
||||
"agent",
|
||||
expect.objectContaining({
|
||||
sessionKey: sampleTaskSessionKey,
|
||||
label: "Build board (card-1)",
|
||||
message: expect.stringContaining("Work on this OpenClaw Workboard card: Build board"),
|
||||
idempotencyKey: "workboard:default:card-1:1",
|
||||
}),
|
||||
);
|
||||
expect(client.request.mock.calls[1]?.[1]).not.toHaveProperty("model");
|
||||
expect(client.request).toHaveBeenNthCalledWith(3, "tasks.list", { limit: 500 });
|
||||
expect(client.request).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
4,
|
||||
"workboard.cards.update",
|
||||
expect.objectContaining({
|
||||
id: "card-1",
|
||||
patch: expect.objectContaining({
|
||||
status: "running",
|
||||
runId: "run-1",
|
||||
taskId: "task-1",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(client.request.mock.calls[2]?.[1]).toHaveProperty("patch.execution", null);
|
||||
expect(client.request.mock.calls[3]?.[1]).toHaveProperty("patch.execution", null);
|
||||
});
|
||||
|
||||
it("starts reassigned cards with the current task session key", async () => {
|
||||
const host = {};
|
||||
const expectedSessionKey = "agent:codex-main:subagent:workboard-default-card-1";
|
||||
const staleLinked = {
|
||||
...sampleCard,
|
||||
agentId: "codex-main",
|
||||
sessionKey: "agent:old-agent:dashboard:stale",
|
||||
} satisfies WorkboardCard;
|
||||
const running = {
|
||||
...staleLinked,
|
||||
status: "running",
|
||||
sessionKey: expectedSessionKey,
|
||||
runId: "run-1",
|
||||
taskId: "task-1",
|
||||
};
|
||||
const client = createClient({
|
||||
agent: { sessionKey: expectedSessionKey, runId: "run-1" },
|
||||
"tasks.list": {
|
||||
tasks: [{ ...sampleTask, childSessionKey: expectedSessionKey }],
|
||||
},
|
||||
"workboard.cards.update": { card: running },
|
||||
});
|
||||
|
||||
const sessionKey = await startWorkboardCard({
|
||||
host,
|
||||
client: client as never,
|
||||
card: staleLinked,
|
||||
});
|
||||
|
||||
expect(sessionKey).toBe(expectedSessionKey);
|
||||
expect(client.request).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"agent",
|
||||
expect.objectContaining({
|
||||
agentId: "codex-main",
|
||||
sessionKey: expectedSessionKey,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("waits briefly for task ledger registration after a started run", async () => {
|
||||
vi.useFakeTimers();
|
||||
const host = {};
|
||||
const running = {
|
||||
...sampleCard,
|
||||
status: "running",
|
||||
sessionKey: sampleTaskSessionKey,
|
||||
runId: "run-1",
|
||||
taskId: "task-1",
|
||||
};
|
||||
let taskLists = 0;
|
||||
const client = createClient((method) => {
|
||||
if (method === "agent") {
|
||||
return { sessionKey: sampleTaskSessionKey, runId: "run-1" };
|
||||
}
|
||||
if (method === "tasks.list") {
|
||||
taskLists += 1;
|
||||
return { tasks: taskLists >= 3 ? [sampleTask] : [] };
|
||||
}
|
||||
return { card: running };
|
||||
});
|
||||
|
||||
const started = startWorkboardCard({
|
||||
host,
|
||||
client: client as never,
|
||||
card: sampleCard,
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(350);
|
||||
const sessionKey = await started;
|
||||
|
||||
expect(sessionKey).toBe(sampleTaskSessionKey);
|
||||
expect(taskLists).toBe(3);
|
||||
expect(client.request).toHaveBeenLastCalledWith(
|
||||
"workboard.cards.update",
|
||||
expect.objectContaining({
|
||||
patch: expect.objectContaining({ taskId: "task-1" }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("lets the gateway preflight decide starts when local parent state is stale", async () => {
|
||||
@@ -490,8 +702,11 @@ describe("workboard controller", () => {
|
||||
if (method === "workboard.cards.list") {
|
||||
return { cards: [parent, child], statuses: ["todo", "running", "done"] };
|
||||
}
|
||||
if (method === "sessions.create") {
|
||||
return { key: "agent:main:dashboard:child" };
|
||||
if (method === "agent") {
|
||||
return { sessionKey: "agent:main:dashboard:child", runId: "run-child" };
|
||||
}
|
||||
if (method === "tasks.list") {
|
||||
return { tasks: [] };
|
||||
}
|
||||
return { card: running };
|
||||
});
|
||||
@@ -538,7 +753,7 @@ describe("workboard controller", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rolls back the running preflight when session creation fails", async () => {
|
||||
it("rolls back the running preflight when task run creation fails", async () => {
|
||||
const host = {};
|
||||
const running = { ...sampleCard, status: "running" } satisfies WorkboardCard;
|
||||
let updateCalls = 0;
|
||||
@@ -547,7 +762,7 @@ describe("workboard controller", () => {
|
||||
updateCalls += 1;
|
||||
return { card: updateCalls === 1 ? running : sampleCard };
|
||||
}
|
||||
if (method === "sessions.create") {
|
||||
if (method === "agent") {
|
||||
throw new Error("gateway disconnected");
|
||||
}
|
||||
return {};
|
||||
@@ -595,8 +810,11 @@ describe("workboard controller", () => {
|
||||
}
|
||||
return { card: sampleCard };
|
||||
}
|
||||
if (method === "sessions.create") {
|
||||
return { key: "agent:main:dashboard:1", runId: "run-1" };
|
||||
if (method === "agent") {
|
||||
return { sessionKey: sampleTaskSessionKey, runId: "run-1" };
|
||||
}
|
||||
if (method === "tasks.list") {
|
||||
return { tasks: [sampleTask] };
|
||||
}
|
||||
if (method === "chat.abort") {
|
||||
return { aborted: true, runIds: ["run-1"] };
|
||||
@@ -611,16 +829,12 @@ describe("workboard controller", () => {
|
||||
});
|
||||
|
||||
expect(sessionKey).toBeNull();
|
||||
expect(client.request).toHaveBeenNthCalledWith(5, "chat.abort", {
|
||||
sessionKey: sampleTaskSessionKey,
|
||||
runId: "run-1",
|
||||
});
|
||||
expect(client.request).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
"chat.abort",
|
||||
{
|
||||
sessionKey: "agent:main:dashboard:1",
|
||||
runId: "run-1",
|
||||
},
|
||||
);
|
||||
expect(client.request).toHaveBeenNthCalledWith(
|
||||
5,
|
||||
6,
|
||||
"workboard.cards.update",
|
||||
expect.objectContaining({
|
||||
patch: expect.objectContaining({
|
||||
@@ -743,12 +957,37 @@ describe("workboard controller", () => {
|
||||
const dueRunning = {
|
||||
...dueScheduled,
|
||||
status: "running",
|
||||
sessionKey: "agent:main:dashboard:1",
|
||||
sessionKey: "subagent:workboard-default-scheduled-3",
|
||||
runId: "run-due",
|
||||
taskId: "task-due",
|
||||
} satisfies WorkboardCard;
|
||||
const dueClient = createClient({
|
||||
"workboard.cards.list": { cards: [dueScheduled], statuses: ["scheduled", "running", "done"] },
|
||||
"sessions.create": { key: "agent:main:dashboard:1", runId: "run-1" },
|
||||
"workboard.cards.update": { card: dueRunning },
|
||||
const dueClient = createClient((method) => {
|
||||
if (method === "workboard.cards.list") {
|
||||
return { cards: [dueScheduled], statuses: ["scheduled", "running", "done"] };
|
||||
}
|
||||
if (method === "agent") {
|
||||
return {
|
||||
sessionKey: "subagent:workboard-default-scheduled-3",
|
||||
runId: "run-due",
|
||||
};
|
||||
}
|
||||
if (method === "tasks.list") {
|
||||
return {
|
||||
tasks: [
|
||||
{
|
||||
...sampleTask,
|
||||
id: "task-due",
|
||||
taskId: "task-due",
|
||||
childSessionKey: "subagent:workboard-default-scheduled-3",
|
||||
runId: "run-due",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (method === "workboard.cards.update") {
|
||||
return { card: dueRunning };
|
||||
}
|
||||
return {};
|
||||
});
|
||||
await loadWorkboard({ host, client: dueClient as never, force: true });
|
||||
dueClient.request.mockClear();
|
||||
@@ -759,9 +998,9 @@ describe("workboard controller", () => {
|
||||
card: dueScheduled,
|
||||
});
|
||||
|
||||
expect(dueSessionKey).toBe("agent:main:dashboard:1");
|
||||
expect(dueSessionKey).toBe("subagent:workboard-default-scheduled-3");
|
||||
expect(dueClient.request).toHaveBeenCalledWith(
|
||||
"sessions.create",
|
||||
"agent",
|
||||
expect.objectContaining({
|
||||
label: "Build board (schedule)",
|
||||
}),
|
||||
@@ -773,7 +1012,8 @@ describe("workboard controller", () => {
|
||||
const running = {
|
||||
...sampleCard,
|
||||
status: "running",
|
||||
sessionKey: "agent:main:dashboard:1",
|
||||
sessionKey: sampleTaskSessionKey,
|
||||
taskId: "task-1",
|
||||
execution: {
|
||||
id: "card-1:codex",
|
||||
kind: "agent-session",
|
||||
@@ -781,14 +1021,15 @@ describe("workboard controller", () => {
|
||||
mode: "autonomous",
|
||||
status: "running",
|
||||
model: "openai/gpt-5.5",
|
||||
sessionKey: "agent:main:dashboard:1",
|
||||
sessionKey: sampleTaskSessionKey,
|
||||
runId: "run-1",
|
||||
startedAt: 10,
|
||||
updatedAt: 10,
|
||||
},
|
||||
};
|
||||
const client = createClient({
|
||||
"sessions.create": { key: "agent:main:dashboard:1", runId: "run-1" },
|
||||
agent: { sessionKey: sampleTaskSessionKey, runId: "run-1" },
|
||||
"tasks.list": { tasks: [sampleTask] },
|
||||
"workboard.cards.update": { card: running },
|
||||
});
|
||||
|
||||
@@ -808,14 +1049,16 @@ describe("workboard controller", () => {
|
||||
);
|
||||
expect(client.request).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"sessions.create",
|
||||
"agent",
|
||||
expect.objectContaining({
|
||||
sessionKey: sampleTaskSessionKey,
|
||||
model: "openai/gpt-5.5",
|
||||
message: expect.stringContaining("Work on this OpenClaw Workboard card: Build board"),
|
||||
}),
|
||||
);
|
||||
expect(client.request).toHaveBeenNthCalledWith(3, "tasks.list", { limit: 500 });
|
||||
expect(client.request).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
4,
|
||||
"workboard.cards.update",
|
||||
expect.objectContaining({
|
||||
id: "card-1",
|
||||
@@ -852,7 +1095,18 @@ describe("workboard controller", () => {
|
||||
},
|
||||
} satisfies WorkboardCard;
|
||||
const client = createClient({
|
||||
"sessions.create": { key: "agent:main:dashboard:1", runId: "run-2" },
|
||||
agent: { sessionKey: "agent:main:dashboard:1", runId: "run-2" },
|
||||
"tasks.list": {
|
||||
tasks: [
|
||||
{
|
||||
...sampleTask,
|
||||
taskId: "task-2",
|
||||
id: "task-2",
|
||||
childSessionKey: "agent:main:dashboard:1",
|
||||
runId: "run-2",
|
||||
},
|
||||
],
|
||||
},
|
||||
"workboard.cards.update": { card: previous },
|
||||
});
|
||||
|
||||
@@ -864,7 +1118,7 @@ describe("workboard controller", () => {
|
||||
});
|
||||
|
||||
expect(client.request).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
4,
|
||||
"workboard.cards.update",
|
||||
expect.objectContaining({
|
||||
patch: expect.objectContaining({
|
||||
@@ -939,16 +1193,86 @@ describe("workboard controller", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("blocks a card when the initial session run fails to start", async () => {
|
||||
it("clears stale task linkage when opening a manual execution", async () => {
|
||||
const host = {};
|
||||
const blocked = { ...sampleCard, status: "blocked", sessionKey: "agent:main:dashboard:1" };
|
||||
const client = createClient({
|
||||
"sessions.create": {
|
||||
key: "agent:main:dashboard:1",
|
||||
runStarted: false,
|
||||
runError: { message: "provider unavailable" },
|
||||
const staleLinkedCard = {
|
||||
...sampleCard,
|
||||
sessionKey: sampleTaskSessionKey,
|
||||
runId: "run-1",
|
||||
taskId: "task-1",
|
||||
execution: {
|
||||
id: "card-1:codex",
|
||||
kind: "agent-session",
|
||||
engine: "codex",
|
||||
mode: "autonomous",
|
||||
status: "blocked",
|
||||
model: "openai/gpt-5.5",
|
||||
sessionKey: sampleTaskSessionKey,
|
||||
runId: "run-1",
|
||||
startedAt: 10,
|
||||
updatedAt: 20,
|
||||
},
|
||||
"workboard.cards.update": { card: blocked },
|
||||
} satisfies WorkboardCard;
|
||||
const reopened = {
|
||||
...sampleCard,
|
||||
sessionKey: "agent:main:dashboard:new",
|
||||
execution: {
|
||||
id: "card-1:claude",
|
||||
kind: "agent-session",
|
||||
engine: "claude",
|
||||
mode: "manual",
|
||||
status: "idle",
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
sessionKey: "agent:main:dashboard:new",
|
||||
startedAt: 10,
|
||||
updatedAt: 10,
|
||||
},
|
||||
} satisfies WorkboardCard;
|
||||
const client = createClient({
|
||||
"sessions.create": { key: "agent:main:dashboard:new", runStarted: false },
|
||||
"workboard.cards.update": { card: reopened },
|
||||
});
|
||||
getWorkboardState(host).tasksByCardId.set("card-1", sampleTask);
|
||||
|
||||
await startWorkboardCard({
|
||||
host,
|
||||
client: client as never,
|
||||
card: staleLinkedCard,
|
||||
engine: "claude",
|
||||
mode: "manual",
|
||||
});
|
||||
|
||||
expect(client.request).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"workboard.cards.update",
|
||||
expect.objectContaining({
|
||||
id: "card-1",
|
||||
patch: expect.objectContaining({
|
||||
sessionKey: "agent:main:dashboard:new",
|
||||
runId: null,
|
||||
taskId: null,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(getWorkboardState(host).tasksByCardId.has("card-1")).toBe(false);
|
||||
});
|
||||
|
||||
it("rolls back when the Gateway does not return a task run id", async () => {
|
||||
const host = {};
|
||||
let updateCalls = 0;
|
||||
const client = createClient((method) => {
|
||||
if (method === "agent") {
|
||||
return {
|
||||
sessionKey: sampleTaskSessionKey,
|
||||
runStarted: false,
|
||||
runError: { message: "provider unavailable" },
|
||||
};
|
||||
}
|
||||
if (method === "workboard.cards.update") {
|
||||
updateCalls += 1;
|
||||
return { card: updateCalls === 1 ? { ...sampleCard, status: "running" } : sampleCard };
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const sessionKey = await startWorkboardCard({
|
||||
@@ -957,20 +1281,14 @@ describe("workboard controller", () => {
|
||||
card: sampleCard,
|
||||
});
|
||||
|
||||
expect(sessionKey).toBe("agent:main:dashboard:1");
|
||||
expect(sessionKey).toBeNull();
|
||||
expect(client.request).toHaveBeenNthCalledWith(2, "agent", expect.any(Object));
|
||||
expect(client.request).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
"workboard.cards.update",
|
||||
expect.objectContaining({
|
||||
id: "card-1",
|
||||
patch: expect.objectContaining({
|
||||
status: "blocked",
|
||||
sessionKey: "agent:main:dashboard:1",
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({ patch: expect.objectContaining({ status: "todo" }) }),
|
||||
);
|
||||
expect(client.request.mock.calls[2]?.[1]).toHaveProperty("patch.execution", null);
|
||||
expect(getWorkboardState(host).error).toBe("Agent run did not start: provider unavailable");
|
||||
expect(getWorkboardState(host).error).toBe("Gateway agent method returned an invalid runId.");
|
||||
});
|
||||
|
||||
it("moves cards through the plugin gateway method", async () => {
|
||||
@@ -1128,6 +1446,37 @@ describe("workboard controller", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("derives lifecycle state from linked Gateway tasks", () => {
|
||||
const linked = { ...sampleCard, sessionKey: sampleTaskSessionKey, runId: "run-1" };
|
||||
|
||||
expect(getWorkboardLifecycle(linked, [], sampleTask)).toMatchObject({
|
||||
state: "running",
|
||||
targetStatus: "running",
|
||||
});
|
||||
expect(getWorkboardLifecycle(linked, [], { ...sampleTask, status: "completed" })).toMatchObject(
|
||||
{
|
||||
state: "succeeded",
|
||||
targetStatus: "review",
|
||||
},
|
||||
);
|
||||
expect(getWorkboardLifecycle(linked, [], { ...sampleTask, status: "timed_out" })).toMatchObject(
|
||||
{
|
||||
state: "failed",
|
||||
targetStatus: "blocked",
|
||||
},
|
||||
);
|
||||
expect(
|
||||
getWorkboardLifecycle(
|
||||
linked,
|
||||
[{ ...sampleSession, key: sampleTaskSessionKey, hasActiveRun: false, status: "done" }],
|
||||
sampleTask,
|
||||
),
|
||||
).toMatchObject({
|
||||
state: "succeeded",
|
||||
targetStatus: "review",
|
||||
});
|
||||
});
|
||||
|
||||
it("syncs linked card status from session lifecycle without overriding manual review", async () => {
|
||||
const host = {};
|
||||
const state = getWorkboardState(host);
|
||||
@@ -1160,6 +1509,40 @@ describe("workboard controller", () => {
|
||||
expect(state.cards.find((card) => card.id === "card-review")?.status).toBe("review");
|
||||
});
|
||||
|
||||
it("refreshes task lifecycle before syncing task-backed cards", async () => {
|
||||
const host = {};
|
||||
const state = getWorkboardState(host);
|
||||
const linked = {
|
||||
...sampleCard,
|
||||
status: "running",
|
||||
sessionKey: sampleTaskSessionKey,
|
||||
runId: "run-1",
|
||||
taskId: "task-1",
|
||||
} satisfies WorkboardCard;
|
||||
state.loaded = true;
|
||||
state.cards = [linked];
|
||||
state.tasksByCardId.set("card-1", sampleTask);
|
||||
const client = createClient({
|
||||
"tasks.list": { tasks: [{ ...sampleTask, status: "completed" }] },
|
||||
"workboard.cards.update": {
|
||||
card: { ...linked, status: "review" },
|
||||
},
|
||||
});
|
||||
|
||||
await syncWorkboardLifecycle({
|
||||
host,
|
||||
client: client as never,
|
||||
sessions: [],
|
||||
});
|
||||
|
||||
expect(client.request).toHaveBeenNthCalledWith(1, "tasks.list", { limit: 500 });
|
||||
expect(client.request).toHaveBeenNthCalledWith(2, "workboard.cards.update", {
|
||||
id: "card-1",
|
||||
patch: { status: "review" },
|
||||
});
|
||||
expect(state.tasksByCardId.get("card-1")).toMatchObject({ status: "completed" });
|
||||
});
|
||||
|
||||
it("moves stale running sessions into running while recording stale metadata", async () => {
|
||||
const host = {};
|
||||
const state = getWorkboardState(host);
|
||||
@@ -1426,6 +1809,109 @@ describe("workboard controller", () => {
|
||||
expect(getWorkboardState(host).cards[0]).toMatchObject({ status: "blocked" });
|
||||
});
|
||||
|
||||
it("cancels active linked tasks and aborts the running session", async () => {
|
||||
const host = {};
|
||||
const linked = {
|
||||
...sampleCard,
|
||||
sessionKey: sampleTaskSessionKey,
|
||||
runId: "run-1",
|
||||
taskId: "task-1",
|
||||
};
|
||||
const blocked = { ...linked, status: "blocked" };
|
||||
const state = getWorkboardState(host);
|
||||
state.cards = [linked];
|
||||
state.tasksByCardId.set("card-1", sampleTask);
|
||||
const client = createClient({
|
||||
"tasks.cancel": { cancelled: true },
|
||||
"chat.abort": { aborted: true, runIds: ["run-1"] },
|
||||
"workboard.cards.update": { card: blocked },
|
||||
});
|
||||
|
||||
await stopWorkboardCard({ host, client: client as never, card: linked });
|
||||
|
||||
expect(client.request).toHaveBeenNthCalledWith(1, "tasks.cancel", {
|
||||
taskId: "task-1",
|
||||
reason: "Stopped from Workboard.",
|
||||
});
|
||||
expect(client.request).toHaveBeenNthCalledWith(2, "chat.abort", {
|
||||
sessionKey: sampleTaskSessionKey,
|
||||
runId: "run-1",
|
||||
});
|
||||
expect(client.request).toHaveBeenNthCalledWith(3, "workboard.cards.update", {
|
||||
id: "card-1",
|
||||
patch: { status: "blocked" },
|
||||
});
|
||||
expect(getWorkboardState(host).cards[0]).toMatchObject({ status: "blocked" });
|
||||
expect(getWorkboardState(host).tasksByCardId.get("card-1")).toMatchObject({
|
||||
taskId: "task-1",
|
||||
status: "cancelled",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps task-linked cards running when ledger cancel does not abort the session", async () => {
|
||||
const host = {};
|
||||
const linked = {
|
||||
...sampleCard,
|
||||
sessionKey: sampleTaskSessionKey,
|
||||
runId: "run-1",
|
||||
taskId: "task-1",
|
||||
};
|
||||
const state = getWorkboardState(host);
|
||||
state.cards = [linked];
|
||||
state.tasksByCardId.set("card-1", sampleTask);
|
||||
const client = createClient({
|
||||
"tasks.cancel": { cancelled: true },
|
||||
"chat.abort": { aborted: false, runIds: [] },
|
||||
});
|
||||
|
||||
await stopWorkboardCard({ host, client: client as never, card: linked });
|
||||
|
||||
expect(client.request).toHaveBeenNthCalledWith(1, "tasks.cancel", {
|
||||
taskId: "task-1",
|
||||
reason: "Stopped from Workboard.",
|
||||
});
|
||||
expect(client.request).toHaveBeenNthCalledWith(2, "chat.abort", {
|
||||
sessionKey: sampleTaskSessionKey,
|
||||
runId: "run-1",
|
||||
});
|
||||
expect(client.request).toHaveBeenNthCalledWith(3, "chat.abort", {
|
||||
sessionKey: sampleTaskSessionKey,
|
||||
});
|
||||
expect(client.request).toHaveBeenCalledTimes(3);
|
||||
expect(state.cards).toEqual([linked]);
|
||||
expect(state.tasksByCardId.get("card-1")).toMatchObject({
|
||||
taskId: "task-1",
|
||||
status: "cancelled",
|
||||
});
|
||||
});
|
||||
|
||||
it("cancels active task-only cards from the local task map", async () => {
|
||||
const host = {};
|
||||
const blocked = { ...sampleCard, status: "blocked" };
|
||||
const state = getWorkboardState(host);
|
||||
state.cards = [sampleCard];
|
||||
state.tasksByCardId.set("card-1", sampleTask);
|
||||
const client = createClient({
|
||||
"tasks.cancel": { cancelled: true },
|
||||
"workboard.cards.update": { card: blocked },
|
||||
});
|
||||
|
||||
await stopWorkboardCard({ host, client: client as never, card: sampleCard });
|
||||
|
||||
expect(client.request).toHaveBeenNthCalledWith(1, "tasks.cancel", {
|
||||
taskId: "task-1",
|
||||
reason: "Stopped from Workboard.",
|
||||
});
|
||||
expect(client.request).toHaveBeenNthCalledWith(2, "workboard.cards.update", {
|
||||
id: "card-1",
|
||||
patch: { status: "blocked" },
|
||||
});
|
||||
expect(getWorkboardState(host).tasksByCardId.get("card-1")).toMatchObject({
|
||||
taskId: "task-1",
|
||||
status: "cancelled",
|
||||
});
|
||||
});
|
||||
|
||||
it("archives cards through the plugin gateway method", async () => {
|
||||
const host = {};
|
||||
const archived = {
|
||||
|
||||
@@ -291,6 +291,40 @@ export type WorkboardLifecycle = {
|
||||
targetStatus?: WorkboardStatus;
|
||||
};
|
||||
|
||||
export type WorkboardTaskStatus =
|
||||
| "queued"
|
||||
| "running"
|
||||
| "completed"
|
||||
| "failed"
|
||||
| "cancelled"
|
||||
| "timed_out";
|
||||
|
||||
export type WorkboardTaskSummary = {
|
||||
id: string;
|
||||
taskId: string;
|
||||
status: WorkboardTaskStatus;
|
||||
title?: string;
|
||||
agentId?: string;
|
||||
sessionKey?: string;
|
||||
childSessionKey?: string;
|
||||
ownerKey?: string;
|
||||
runId?: string;
|
||||
sourceId?: string;
|
||||
updatedAt?: number | string;
|
||||
progressSummary?: string;
|
||||
terminalSummary?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export type WorkboardDispatchSummary = {
|
||||
started: number;
|
||||
failures: number;
|
||||
promoted: number;
|
||||
blocked: number;
|
||||
reclaimed: number;
|
||||
orchestrated: number;
|
||||
};
|
||||
|
||||
export type WorkboardUiState = {
|
||||
loading: boolean;
|
||||
loaded: boolean;
|
||||
@@ -298,6 +332,8 @@ export type WorkboardUiState = {
|
||||
error: string | null;
|
||||
cards: WorkboardCard[];
|
||||
statuses: readonly WorkboardStatus[];
|
||||
tasksByCardId: Map<string, WorkboardTaskSummary>;
|
||||
lastDispatchSummary: WorkboardDispatchSummary | null;
|
||||
query: string;
|
||||
priorityFilter: "all" | WorkboardPriority;
|
||||
draftOpen: boolean;
|
||||
@@ -315,11 +351,6 @@ export type WorkboardUiState = {
|
||||
draggedCardId: string | null;
|
||||
syncingCardIds: Set<string>;
|
||||
capturingSessionKeys: Set<string>;
|
||||
gameOpen: boolean;
|
||||
gamePlayerIndex: number;
|
||||
gameMoves: number;
|
||||
gameWins: number;
|
||||
gameMessage: string;
|
||||
};
|
||||
|
||||
type WorkboardHost = object;
|
||||
@@ -332,6 +363,8 @@ const SESSION_CAPTURE_TEXT_MAX_CHARS = 700;
|
||||
const WORKBOARD_CAPTURE_TITLE_MAX_CHARS = 180;
|
||||
const WORKBOARD_SESSION_LABEL_MAX_CHARS = 512;
|
||||
const WORKBOARD_STALE_SESSION_MS = 30 * 60 * 1000;
|
||||
const WORKBOARD_TASKS_LIST_LIMIT = 500;
|
||||
const WORKBOARD_TASK_LOOKUP_RETRY_DELAYS_MS = [100, 250, 500] as const;
|
||||
|
||||
function createDefaultState(): WorkboardUiState {
|
||||
return {
|
||||
@@ -341,6 +374,8 @@ function createDefaultState(): WorkboardUiState {
|
||||
error: null,
|
||||
cards: [],
|
||||
statuses: WORKBOARD_STATUSES,
|
||||
tasksByCardId: new Map(),
|
||||
lastDispatchSummary: null,
|
||||
query: "",
|
||||
priorityFilter: "all",
|
||||
draftOpen: false,
|
||||
@@ -358,11 +393,6 @@ function createDefaultState(): WorkboardUiState {
|
||||
draggedCardId: null,
|
||||
syncingCardIds: new Set(),
|
||||
capturingSessionKeys: new Set(),
|
||||
gameOpen: false,
|
||||
gamePlayerIndex: 0,
|
||||
gameMoves: 0,
|
||||
gameWins: 0,
|
||||
gameMessage: "workboard.gameStart",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -870,6 +900,181 @@ function normalizeCardPayload(payload: unknown): WorkboardCard {
|
||||
return card;
|
||||
}
|
||||
|
||||
function normalizeTaskStatus(value: unknown): WorkboardTaskStatus | null {
|
||||
switch (value) {
|
||||
case "queued":
|
||||
case "running":
|
||||
case "completed":
|
||||
case "failed":
|
||||
case "cancelled":
|
||||
case "timed_out":
|
||||
return value;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeTaskSummary(value: unknown): WorkboardTaskSummary | null {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
const id = typeof value.id === "string" && value.id.trim() ? value.id.trim() : null;
|
||||
const taskId = typeof value.taskId === "string" && value.taskId.trim() ? value.taskId.trim() : id;
|
||||
const status = normalizeTaskStatus(value.status);
|
||||
if (!id || !taskId || !status) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id,
|
||||
taskId,
|
||||
status,
|
||||
...(typeof value.title === "string" ? { title: value.title } : {}),
|
||||
...(typeof value.agentId === "string" ? { agentId: value.agentId } : {}),
|
||||
...(typeof value.sessionKey === "string" ? { sessionKey: value.sessionKey } : {}),
|
||||
...(typeof value.childSessionKey === "string"
|
||||
? { childSessionKey: value.childSessionKey }
|
||||
: {}),
|
||||
...(typeof value.ownerKey === "string" ? { ownerKey: value.ownerKey } : {}),
|
||||
...(typeof value.runId === "string" ? { runId: value.runId } : {}),
|
||||
...(typeof value.sourceId === "string" ? { sourceId: value.sourceId } : {}),
|
||||
...(typeof value.updatedAt === "number" || typeof value.updatedAt === "string"
|
||||
? { updatedAt: value.updatedAt }
|
||||
: {}),
|
||||
...(typeof value.progressSummary === "string"
|
||||
? { progressSummary: value.progressSummary }
|
||||
: {}),
|
||||
...(typeof value.terminalSummary === "string"
|
||||
? { terminalSummary: value.terminalSummary }
|
||||
: {}),
|
||||
...(typeof value.error === "string" ? { error: value.error } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTasksPage(payload: unknown): {
|
||||
tasks: WorkboardTaskSummary[];
|
||||
nextCursor: string | null;
|
||||
} {
|
||||
if (!isRecord(payload) || !Array.isArray(payload.tasks)) {
|
||||
return { tasks: [], nextCursor: null };
|
||||
}
|
||||
return {
|
||||
tasks: payload.tasks
|
||||
.map(normalizeTaskSummary)
|
||||
.filter((task): task is WorkboardTaskSummary => task !== null),
|
||||
nextCursor:
|
||||
typeof payload.nextCursor === "string" && payload.nextCursor.trim()
|
||||
? payload.nextCursor.trim()
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
async function listWorkboardTasks(client: GatewayBrowserClient): Promise<WorkboardTaskSummary[]> {
|
||||
const tasks: WorkboardTaskSummary[] = [];
|
||||
const seenCursors = new Set<string>();
|
||||
let cursor: string | null = null;
|
||||
while (true) {
|
||||
const payload = await client.request("tasks.list", {
|
||||
limit: WORKBOARD_TASKS_LIST_LIMIT,
|
||||
...(cursor ? { cursor } : {}),
|
||||
});
|
||||
const page = normalizeTasksPage(payload);
|
||||
tasks.push(...page.tasks);
|
||||
if (!page.nextCursor || seenCursors.has(page.nextCursor)) {
|
||||
return tasks;
|
||||
}
|
||||
seenCursors.add(page.nextCursor);
|
||||
cursor = page.nextCursor;
|
||||
}
|
||||
}
|
||||
|
||||
function taskUpdatedAtValue(task: WorkboardTaskSummary): number {
|
||||
if (typeof task.updatedAt === "number") {
|
||||
return task.updatedAt;
|
||||
}
|
||||
if (typeof task.updatedAt === "string") {
|
||||
const parsed = Date.parse(task.updatedAt);
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function taskSessionKeyMatchesCardSession(
|
||||
cardSessionKey: string,
|
||||
taskSessionKey: string | undefined,
|
||||
): boolean {
|
||||
if (!taskSessionKey) {
|
||||
return false;
|
||||
}
|
||||
if (taskSessionKey === cardSessionKey) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
cardSessionKey.startsWith("subagent:workboard-") &&
|
||||
taskSessionKey.endsWith(`:${cardSessionKey}`)
|
||||
);
|
||||
}
|
||||
|
||||
function taskMatchesCard(task: WorkboardTaskSummary, card: WorkboardCard): boolean {
|
||||
const cardTaskId = normalizeString(card.taskId);
|
||||
if (cardTaskId && (task.taskId === cardTaskId || task.id === cardTaskId)) {
|
||||
return true;
|
||||
}
|
||||
const cardSessionKey = workboardCardSessionKey(card);
|
||||
const taskSessionMatches = cardSessionKey
|
||||
? [task.sessionKey, task.childSessionKey, task.ownerKey].some((taskSessionKey) =>
|
||||
taskSessionKeyMatchesCardSession(cardSessionKey, taskSessionKey),
|
||||
)
|
||||
: false;
|
||||
const cardRunId = workboardCardRunId(card);
|
||||
if (cardRunId && task.runId === cardRunId) {
|
||||
return cardSessionKey ? taskSessionMatches : true;
|
||||
}
|
||||
return taskSessionMatches;
|
||||
}
|
||||
|
||||
function applyTaskSummariesToState(
|
||||
state: WorkboardUiState,
|
||||
tasks: readonly WorkboardTaskSummary[],
|
||||
) {
|
||||
const tasksByCardId = new Map<string, WorkboardTaskSummary>();
|
||||
const cards = state.cards.map((card) => {
|
||||
const matches = tasks.filter((task) => taskMatchesCard(task, card));
|
||||
if (matches.length === 0) {
|
||||
return card;
|
||||
}
|
||||
const task = matches.toSorted(
|
||||
(left, right) => taskUpdatedAtValue(right) - taskUpdatedAtValue(left),
|
||||
)[0];
|
||||
if (!task) {
|
||||
return card;
|
||||
}
|
||||
tasksByCardId.set(card.id, task);
|
||||
if (card.taskId === task.taskId) {
|
||||
return card;
|
||||
}
|
||||
return { ...card, taskId: task.taskId };
|
||||
});
|
||||
state.cards = cards;
|
||||
state.tasksByCardId = tasksByCardId;
|
||||
}
|
||||
|
||||
function shouldRefreshWorkboardTasksForLifecycle(state: WorkboardUiState): boolean {
|
||||
return state.tasksByCardId.size > 0 || state.cards.some((card) => Boolean(card.taskId));
|
||||
}
|
||||
|
||||
function normalizeDispatchSummary(value: unknown): WorkboardDispatchSummary {
|
||||
const countArray = (key: string) =>
|
||||
isRecord(value) && Array.isArray(value[key]) ? value[key].length : 0;
|
||||
return {
|
||||
started: countArray("started"),
|
||||
failures: countArray("startFailures"),
|
||||
promoted: countArray("promoted"),
|
||||
blocked: countArray("blocked"),
|
||||
reclaimed: countArray("reclaimed"),
|
||||
orchestrated: countArray("orchestrated"),
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadWorkboard(params: {
|
||||
host: WorkboardHost;
|
||||
client: GatewayBrowserClient | null;
|
||||
@@ -896,6 +1101,10 @@ export async function loadWorkboard(params: {
|
||||
const normalized = normalizeCardsPayload(payload);
|
||||
state.cards = normalized.cards;
|
||||
state.statuses = normalized.statuses;
|
||||
state.tasksByCardId = new Map();
|
||||
if (state.cards.length > 0) {
|
||||
applyTaskSummariesToState(state, await listWorkboardTasks(client));
|
||||
}
|
||||
state.loaded = true;
|
||||
} catch (error) {
|
||||
state.error = formatError(error);
|
||||
@@ -1014,8 +1223,42 @@ function workboardCardRunId(card: WorkboardCard): string | undefined {
|
||||
export function getWorkboardLifecycle(
|
||||
card: WorkboardCard,
|
||||
sessions: readonly GatewaySessionRow[],
|
||||
task?: WorkboardTaskSummary,
|
||||
): WorkboardLifecycle {
|
||||
const session = findWorkboardSession(card, sessions);
|
||||
if (task) {
|
||||
switch (task.status) {
|
||||
case "queued":
|
||||
case "running":
|
||||
if (
|
||||
session &&
|
||||
(session.abortedLastRun ||
|
||||
session.status === "done" ||
|
||||
isFailedSessionStatus(session.status))
|
||||
) {
|
||||
break;
|
||||
}
|
||||
return {
|
||||
session,
|
||||
state: "running",
|
||||
targetStatus: "running",
|
||||
};
|
||||
case "completed":
|
||||
return {
|
||||
session,
|
||||
state: "succeeded",
|
||||
targetStatus: "review",
|
||||
};
|
||||
case "failed":
|
||||
case "cancelled":
|
||||
case "timed_out":
|
||||
return {
|
||||
session,
|
||||
state: "failed",
|
||||
targetStatus: "blocked",
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!workboardCardSessionKey(card)) {
|
||||
return { session: null, state: "unlinked" };
|
||||
}
|
||||
@@ -1305,9 +1548,22 @@ export async function syncWorkboardLifecycle(params: {
|
||||
if (!params.client || !state.loaded || params.canWrite === false) {
|
||||
return;
|
||||
}
|
||||
if (shouldRefreshWorkboardTasksForLifecycle(state)) {
|
||||
try {
|
||||
applyTaskSummariesToState(state, await listWorkboardTasks(params.client));
|
||||
} catch (error) {
|
||||
state.tasksByCardId = new Map();
|
||||
state.error = formatError(error);
|
||||
params.requestUpdate?.();
|
||||
}
|
||||
}
|
||||
const syncKeys = getLifecycleSyncKeys(params.host);
|
||||
for (const card of state.cards) {
|
||||
const lifecycle = getWorkboardLifecycle(card, params.sessions);
|
||||
const lifecycle = getWorkboardLifecycle(
|
||||
card,
|
||||
params.sessions,
|
||||
state.tasksByCardId.get(card.id),
|
||||
);
|
||||
const executionStatus = executionStatusForLifecycle(lifecycle);
|
||||
const patch: Record<string, unknown> = {};
|
||||
if (shouldSyncCardStatus(card, lifecycle.targetStatus)) {
|
||||
@@ -1542,13 +1798,16 @@ export async function dispatchWorkboard(params: {
|
||||
}
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
state.lastDispatchSummary = null;
|
||||
params.requestUpdate?.();
|
||||
try {
|
||||
await params.client.request("workboard.cards.dispatch", {});
|
||||
const dispatchResult = await params.client.request("workboard.cards.dispatch", {});
|
||||
const payload = await params.client.request("workboard.cards.list", {});
|
||||
const normalized = normalizeCardsPayload(payload);
|
||||
state.cards = normalized.cards;
|
||||
state.statuses = normalized.statuses;
|
||||
state.lastDispatchSummary = normalizeDispatchSummary(dispatchResult);
|
||||
applyTaskSummariesToState(state, await listWorkboardTasks(params.client));
|
||||
state.loaded = true;
|
||||
} catch (error) {
|
||||
state.error = formatError(error);
|
||||
@@ -1594,6 +1853,32 @@ function buildCardSessionLabel(card: WorkboardCard): string {
|
||||
return `${title.slice(0, titleMax - 3).trimEnd()}...${suffixText}`;
|
||||
}
|
||||
|
||||
function sanitizeSessionSegment(value: string | undefined, fallback: string): string {
|
||||
const sanitized = (value ?? fallback)
|
||||
.trim()
|
||||
.replace(/[^a-zA-Z0-9_-]/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
return (sanitized || fallback).slice(0, 96);
|
||||
}
|
||||
|
||||
function buildCardTaskSessionKey(card: WorkboardCard): string {
|
||||
const boardId = sanitizeSessionSegment(card.metadata?.automation?.boardId, "default");
|
||||
const cardId = sanitizeSessionSegment(card.id, "card");
|
||||
const suffix = `subagent:workboard-${boardId}-${cardId}`;
|
||||
const sessionKey = card.agentId
|
||||
? `agent:${sanitizeSessionSegment(card.agentId, "agent")}:${suffix}`
|
||||
: suffix;
|
||||
const existing = workboardCardSessionKey(card)?.trim();
|
||||
return existing === sessionKey ? existing : sessionKey;
|
||||
}
|
||||
|
||||
function buildCardRunIdempotencyKey(card: WorkboardCard): string {
|
||||
const boardId = sanitizeSessionSegment(card.metadata?.automation?.boardId, "default");
|
||||
const cardId = sanitizeSessionSegment(card.id, "card");
|
||||
return `workboard:${boardId}:${cardId}:${card.updatedAt}`;
|
||||
}
|
||||
|
||||
function isScheduledForLater(card: WorkboardCard, now = Date.now()): boolean {
|
||||
const scheduledAt = card.metadata?.automation?.scheduledAt;
|
||||
if (typeof scheduledAt === "number") {
|
||||
@@ -1625,6 +1910,35 @@ function buildWorkboardExecution(params: {
|
||||
};
|
||||
}
|
||||
|
||||
async function findTaskForStartedRun(params: {
|
||||
client: GatewayBrowserClient;
|
||||
card: WorkboardCard;
|
||||
sessionKey: string;
|
||||
runId?: string;
|
||||
}): Promise<WorkboardTaskSummary | null> {
|
||||
const probeCard = {
|
||||
...params.card,
|
||||
taskId: undefined,
|
||||
sessionKey: params.sessionKey,
|
||||
...(params.runId ? { runId: params.runId } : {}),
|
||||
};
|
||||
for (const delayMs of [0, ...WORKBOARD_TASK_LOOKUP_RETRY_DELAYS_MS]) {
|
||||
if (delayMs > 0) {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, delayMs);
|
||||
});
|
||||
}
|
||||
const task =
|
||||
(await listWorkboardTasks(params.client))
|
||||
.filter((candidate) => taskMatchesCard(candidate, probeCard))
|
||||
.toSorted((left, right) => taskUpdatedAtValue(right) - taskUpdatedAtValue(left))[0] ?? null;
|
||||
if (task) {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function abortWorkboardSessionRun(params: {
|
||||
client: GatewayBrowserClient;
|
||||
sessionKey: string;
|
||||
@@ -1650,6 +1964,24 @@ async function abortWorkboardSessionRun(params: {
|
||||
return aborted;
|
||||
}
|
||||
|
||||
function taskIsActive(task: WorkboardTaskSummary | undefined): task is WorkboardTaskSummary {
|
||||
return task?.status === "queued" || task?.status === "running";
|
||||
}
|
||||
|
||||
async function cancelWorkboardTaskRun(params: {
|
||||
client: GatewayBrowserClient;
|
||||
taskId: string;
|
||||
}): Promise<{ cancelled: boolean; task: WorkboardTaskSummary | null }> {
|
||||
const result = await params.client.request("tasks.cancel", {
|
||||
taskId: params.taskId,
|
||||
reason: "Stopped from Workboard.",
|
||||
});
|
||||
return {
|
||||
cancelled: isRecord(result) && result.cancelled === true,
|
||||
task: isRecord(result) ? normalizeTaskSummary(result.task) : null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function startWorkboardCard(params: {
|
||||
host: WorkboardHost;
|
||||
client: GatewayBrowserClient | null;
|
||||
@@ -1694,59 +2026,57 @@ export async function startWorkboardCard(params: {
|
||||
card = preflightCard;
|
||||
}
|
||||
}
|
||||
const created = await params.client.request("sessions.create", {
|
||||
...(card.agentId ? { agentId: card.agentId } : {}),
|
||||
label: buildCardSessionLabel(card),
|
||||
...(engine ? { model: WORKBOARD_ENGINE_MODELS[engine] } : {}),
|
||||
...(mode === "autonomous" ? { message: buildCardPrompt(card) } : {}),
|
||||
});
|
||||
const created =
|
||||
mode === "autonomous"
|
||||
? await params.client.request("agent", {
|
||||
sessionKey: buildCardTaskSessionKey(card),
|
||||
...(card.agentId ? { agentId: card.agentId } : {}),
|
||||
label: buildCardSessionLabel(card),
|
||||
...(engine ? { model: WORKBOARD_ENGINE_MODELS[engine] } : {}),
|
||||
message: buildCardPrompt(card),
|
||||
deliver: false,
|
||||
bootstrapContextMode: "lightweight",
|
||||
idempotencyKey: buildCardRunIdempotencyKey(card),
|
||||
})
|
||||
: await params.client.request("sessions.create", {
|
||||
...(card.agentId ? { agentId: card.agentId } : {}),
|
||||
label: buildCardSessionLabel(card),
|
||||
...(engine ? { model: WORKBOARD_ENGINE_MODELS[engine] } : {}),
|
||||
});
|
||||
const sessionKey =
|
||||
isRecord(created) && typeof created.key === "string" && created.key.trim()
|
||||
? created.key.trim()
|
||||
: null;
|
||||
isRecord(created) && typeof created.sessionKey === "string" && created.sessionKey.trim()
|
||||
? created.sessionKey.trim()
|
||||
: isRecord(created) && typeof created.key === "string" && created.key.trim()
|
||||
? created.key.trim()
|
||||
: mode === "autonomous"
|
||||
? buildCardTaskSessionKey(card)
|
||||
: null;
|
||||
const runId =
|
||||
isRecord(created) && typeof created.runId === "string" && created.runId.trim()
|
||||
? created.runId.trim()
|
||||
: undefined;
|
||||
if (mode === "autonomous" && !runId) {
|
||||
throw new Error("Gateway agent method returned an invalid runId.");
|
||||
}
|
||||
createdSessionKey = sessionKey;
|
||||
createdRunId = runId;
|
||||
const initialRunFailed =
|
||||
mode === "autonomous" && isRecord(created) && created.runStarted === false;
|
||||
if (initialRunFailed) {
|
||||
const payload = await params.client.request("workboard.cards.update", {
|
||||
id: params.card.id,
|
||||
patch: {
|
||||
status: "blocked",
|
||||
...(sessionKey ? { sessionKey } : {}),
|
||||
...(engine
|
||||
? {
|
||||
execution: buildWorkboardExecution({
|
||||
card,
|
||||
engine,
|
||||
mode,
|
||||
sessionKey,
|
||||
status: "blocked",
|
||||
}),
|
||||
}
|
||||
: { execution: null }),
|
||||
},
|
||||
});
|
||||
replaceCard(state, normalizeCardPayload(payload));
|
||||
const errorText =
|
||||
isRecord(created) && "runError" in created ? formatError(created.runError) : "";
|
||||
state.error =
|
||||
errorText && errorText !== "Unknown workboard error."
|
||||
? `Agent run did not start: ${errorText}`
|
||||
: "Agent run did not start.";
|
||||
return sessionKey;
|
||||
}
|
||||
const task =
|
||||
mode === "autonomous" && sessionKey
|
||||
? await findTaskForStartedRun({
|
||||
client: params.client,
|
||||
card,
|
||||
sessionKey,
|
||||
runId,
|
||||
})
|
||||
: null;
|
||||
const payload = await params.client.request("workboard.cards.update", {
|
||||
id: params.card.id,
|
||||
patch: {
|
||||
status: nextCardStatus,
|
||||
...(shouldClearManualSchedule ? { scheduledAt: null } : {}),
|
||||
...(sessionKey ? { sessionKey } : {}),
|
||||
...(runId ? { runId } : {}),
|
||||
runId: runId ?? null,
|
||||
taskId: task?.taskId ?? null,
|
||||
...(engine
|
||||
? {
|
||||
execution: buildWorkboardExecution({
|
||||
@@ -1762,6 +2092,11 @@ export async function startWorkboardCard(params: {
|
||||
},
|
||||
});
|
||||
replaceCard(state, normalizeCardPayload(payload));
|
||||
if (task) {
|
||||
state.tasksByCardId.set(params.card.id, task);
|
||||
} else {
|
||||
state.tasksByCardId.delete(params.card.id);
|
||||
}
|
||||
return sessionKey;
|
||||
} catch (error) {
|
||||
if (mode === "autonomous" && createdSessionKey) {
|
||||
@@ -1807,19 +2142,41 @@ export async function stopWorkboardCard(params: {
|
||||
}) {
|
||||
const state = getWorkboardState(params.host);
|
||||
const sessionKey = workboardCardSessionKey(params.card);
|
||||
if (!params.client || !sessionKey) {
|
||||
const task = state.tasksByCardId.get(params.card.id);
|
||||
const taskId = params.card.taskId ?? task?.taskId;
|
||||
if (!params.client || (!sessionKey && !taskId)) {
|
||||
return;
|
||||
}
|
||||
state.busyCardId = params.card.id;
|
||||
state.error = null;
|
||||
params.requestUpdate?.();
|
||||
try {
|
||||
const aborted = await abortWorkboardSessionRun({
|
||||
client: params.client,
|
||||
sessionKey,
|
||||
runId: workboardCardRunId(params.card),
|
||||
});
|
||||
if (!aborted) {
|
||||
let taskCancelled = false;
|
||||
if (taskId && taskIsActive(task)) {
|
||||
const cancelled = await cancelWorkboardTaskRun({
|
||||
client: params.client,
|
||||
taskId,
|
||||
});
|
||||
taskCancelled = cancelled.cancelled;
|
||||
if (cancelled.cancelled) {
|
||||
state.tasksByCardId.set(
|
||||
params.card.id,
|
||||
cancelled.task ?? {
|
||||
...task,
|
||||
status: "cancelled",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
const sessionAborted = sessionKey
|
||||
? await abortWorkboardSessionRun({
|
||||
client: params.client,
|
||||
sessionKey,
|
||||
runId: workboardCardRunId(params.card),
|
||||
})
|
||||
: false;
|
||||
if (sessionKey ? !sessionAborted : !taskCancelled) {
|
||||
return;
|
||||
}
|
||||
const payload = await params.client.request("workboard.cards.update", {
|
||||
|
||||
@@ -1,320 +0,0 @@
|
||||
import type * as THREE from "three";
|
||||
|
||||
type ThreeModule = typeof THREE;
|
||||
|
||||
type WorkboardGameSceneObjects = {
|
||||
camera: THREE.PerspectiveCamera;
|
||||
player: THREE.Group;
|
||||
renderer: THREE.WebGLRenderer;
|
||||
scene: THREE.Scene;
|
||||
};
|
||||
|
||||
const TILE_SPACING = 1.05;
|
||||
|
||||
function parseIntegerAttribute(element: Element, name: string, fallback: number): number {
|
||||
const value = Number.parseInt(element.getAttribute(name) ?? "", 10);
|
||||
return Number.isFinite(value) ? value : fallback;
|
||||
}
|
||||
|
||||
function parseBlockers(value: string | null): Set<number> {
|
||||
if (!value) {
|
||||
return new Set();
|
||||
}
|
||||
return new Set(
|
||||
value
|
||||
.split(",")
|
||||
.map((entry) => Number.parseInt(entry.trim(), 10))
|
||||
.filter((entry) => Number.isFinite(entry)),
|
||||
);
|
||||
}
|
||||
|
||||
function boardPosition(index: number, size: number, three: ThreeModule) {
|
||||
const row = Math.floor(index / size);
|
||||
const column = index % size;
|
||||
const center = (size - 1) / 2;
|
||||
return new three.Vector3((column - center) * TILE_SPACING, 0.25, (row - center) * TILE_SPACING);
|
||||
}
|
||||
|
||||
class Workboard3dGameElement extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ["blockers", "board-size", "goal-index", "player-index", "wins"];
|
||||
}
|
||||
|
||||
private animationFrame = 0;
|
||||
private canvas: HTMLCanvasElement | null = null;
|
||||
private sceneObjects: WorkboardGameSceneObjects | null = null;
|
||||
private resizeObserver: ResizeObserver | null = null;
|
||||
private startVersion = 0;
|
||||
private three: ThreeModule | null = null;
|
||||
|
||||
connectedCallback() {
|
||||
this.ensureDom();
|
||||
void this.start();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.startVersion += 1;
|
||||
if (this.animationFrame) {
|
||||
cancelAnimationFrame(this.animationFrame);
|
||||
this.animationFrame = 0;
|
||||
}
|
||||
this.resizeObserver?.disconnect();
|
||||
this.resizeObserver = null;
|
||||
this.disposeSceneObjects();
|
||||
}
|
||||
|
||||
attributeChangedCallback() {
|
||||
this.updateScene();
|
||||
}
|
||||
|
||||
private ensureDom() {
|
||||
if (this.canvas) {
|
||||
return;
|
||||
}
|
||||
this.replaceChildren();
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.canvas.className = "workboard-game__canvas";
|
||||
this.canvas.setAttribute("aria-hidden", "true");
|
||||
this.append(this.canvas);
|
||||
this.renderFallback();
|
||||
}
|
||||
|
||||
private renderFallback() {
|
||||
const size = parseIntegerAttribute(this, "board-size", 5);
|
||||
const goalIndex = parseIntegerAttribute(this, "goal-index", size * size - 1);
|
||||
const playerIndex = parseIntegerAttribute(this, "player-index", 0);
|
||||
const blockers = parseBlockers(this.getAttribute("blockers"));
|
||||
const fallback = document.createElement("div");
|
||||
fallback.className = "workboard-game__fallback";
|
||||
fallback.setAttribute("aria-hidden", "true");
|
||||
for (let index = 0; index < size * size; index += 1) {
|
||||
const tile = document.createElement("span");
|
||||
tile.className = [
|
||||
"workboard-game__fallback-tile",
|
||||
index === playerIndex ? "workboard-game__fallback-tile--player" : "",
|
||||
index === goalIndex ? "workboard-game__fallback-tile--goal" : "",
|
||||
blockers.has(index) ? "workboard-game__fallback-tile--blocker" : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
fallback.append(tile);
|
||||
}
|
||||
this.querySelector(".workboard-game__fallback")?.remove();
|
||||
this.append(fallback);
|
||||
}
|
||||
|
||||
private resetToFallback() {
|
||||
this.classList.remove("workboard-game__scene--ready");
|
||||
if (this.animationFrame) {
|
||||
cancelAnimationFrame(this.animationFrame);
|
||||
this.animationFrame = 0;
|
||||
}
|
||||
this.resizeObserver?.disconnect();
|
||||
this.resizeObserver = null;
|
||||
this.disposeSceneObjects();
|
||||
this.renderFallback();
|
||||
}
|
||||
|
||||
private disposeSceneObjects() {
|
||||
const objects = this.sceneObjects;
|
||||
if (!objects) {
|
||||
return;
|
||||
}
|
||||
const geometries = new Set<THREE.BufferGeometry>();
|
||||
const materials = new Set<THREE.Material>();
|
||||
objects.scene.traverse((object) => {
|
||||
const mesh = object as THREE.Mesh;
|
||||
if (mesh.geometry) {
|
||||
geometries.add(mesh.geometry);
|
||||
}
|
||||
const material = mesh.material;
|
||||
if (Array.isArray(material)) {
|
||||
for (const entry of material) {
|
||||
materials.add(entry);
|
||||
}
|
||||
} else if (material) {
|
||||
materials.add(material);
|
||||
}
|
||||
});
|
||||
for (const geometry of geometries) {
|
||||
geometry.dispose();
|
||||
}
|
||||
for (const material of materials) {
|
||||
material.dispose();
|
||||
}
|
||||
objects.renderer.dispose();
|
||||
this.sceneObjects = null;
|
||||
}
|
||||
|
||||
private async start() {
|
||||
this.ensureDom();
|
||||
const canvas = this.canvas;
|
||||
if (!canvas || this.sceneObjects) {
|
||||
return;
|
||||
}
|
||||
const startVersion = ++this.startVersion;
|
||||
try {
|
||||
const context = canvas.getContext("webgl2") ?? canvas.getContext("webgl");
|
||||
if (!context) {
|
||||
this.renderFallback();
|
||||
return;
|
||||
}
|
||||
this.three = await import("three");
|
||||
if (!this.isConnected || startVersion !== this.startVersion || this.sceneObjects) {
|
||||
return;
|
||||
}
|
||||
const three = this.three;
|
||||
const renderer = new three.WebGLRenderer({
|
||||
alpha: true,
|
||||
antialias: true,
|
||||
canvas,
|
||||
context,
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||
const scene = new three.Scene();
|
||||
const camera = new three.PerspectiveCamera(42, 1, 0.1, 100);
|
||||
const player = new three.Group();
|
||||
this.sceneObjects = { camera, player, renderer, scene };
|
||||
this.buildScene();
|
||||
this.resize();
|
||||
this.resizeObserver = new ResizeObserver(() => this.resize());
|
||||
this.resizeObserver.observe(this);
|
||||
this.classList.add("workboard-game__scene--ready");
|
||||
this.renderFrame();
|
||||
} catch {
|
||||
this.resetToFallback();
|
||||
}
|
||||
}
|
||||
|
||||
private resize() {
|
||||
const { camera, renderer } = this.sceneObjects ?? {};
|
||||
if (!camera || !renderer) {
|
||||
return;
|
||||
}
|
||||
const width = Math.max(1, this.clientWidth);
|
||||
const height = Math.max(1, this.clientHeight);
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(width, height, false);
|
||||
}
|
||||
|
||||
private buildScene() {
|
||||
const three = this.three;
|
||||
const objects = this.sceneObjects;
|
||||
if (!three || !objects) {
|
||||
return;
|
||||
}
|
||||
const { camera, player, scene } = objects;
|
||||
scene.clear();
|
||||
const size = parseIntegerAttribute(this, "board-size", 5);
|
||||
const goalIndex = parseIntegerAttribute(this, "goal-index", size * size - 1);
|
||||
const blockers = parseBlockers(this.getAttribute("blockers"));
|
||||
const baseMaterial = new three.MeshStandardMaterial({
|
||||
color: 0x21313b,
|
||||
metalness: 0.28,
|
||||
roughness: 0.58,
|
||||
});
|
||||
const openMaterial = new three.MeshStandardMaterial({
|
||||
color: 0x2f4650,
|
||||
metalness: 0.24,
|
||||
roughness: 0.5,
|
||||
});
|
||||
const blockerMaterial = new three.MeshStandardMaterial({
|
||||
color: 0xe55b4d,
|
||||
emissive: 0x3b0804,
|
||||
metalness: 0.18,
|
||||
roughness: 0.42,
|
||||
});
|
||||
const goalMaterial = new three.MeshStandardMaterial({
|
||||
color: 0x52d273,
|
||||
emissive: 0x123d1c,
|
||||
metalness: 0.2,
|
||||
roughness: 0.36,
|
||||
});
|
||||
const playerMaterial = new three.MeshStandardMaterial({
|
||||
color: 0x67a6ff,
|
||||
emissive: 0x112b62,
|
||||
metalness: 0.34,
|
||||
roughness: 0.26,
|
||||
});
|
||||
const tileGeometry = new three.BoxGeometry(0.94, 0.14, 0.94);
|
||||
const blockerGeometry = new three.ConeGeometry(0.28, 0.7, 5);
|
||||
for (let index = 0; index < size * size; index += 1) {
|
||||
const tile = new three.Mesh(
|
||||
tileGeometry,
|
||||
index === goalIndex ? goalMaterial : blockers.has(index) ? baseMaterial : openMaterial,
|
||||
);
|
||||
const position = boardPosition(index, size, three);
|
||||
tile.position.set(position.x, 0, position.z);
|
||||
tile.receiveShadow = true;
|
||||
scene.add(tile);
|
||||
if (blockers.has(index)) {
|
||||
const blocker = new three.Mesh(blockerGeometry, blockerMaterial);
|
||||
blocker.position.set(position.x, 0.42, position.z);
|
||||
blocker.rotation.y = index * 0.61;
|
||||
scene.add(blocker);
|
||||
}
|
||||
}
|
||||
const goalPosition = boardPosition(goalIndex, size, three);
|
||||
const ring = new three.Mesh(new three.TorusGeometry(0.34, 0.045, 10, 28), goalMaterial);
|
||||
ring.position.set(goalPosition.x, 0.25, goalPosition.z);
|
||||
ring.rotation.x = Math.PI / 2;
|
||||
scene.add(ring);
|
||||
|
||||
player.clear();
|
||||
const body = new three.Mesh(new three.IcosahedronGeometry(0.29, 1), playerMaterial);
|
||||
const base = new three.Mesh(new three.CylinderGeometry(0.24, 0.3, 0.16, 16), playerMaterial);
|
||||
base.position.y = -0.2;
|
||||
player.add(body, base);
|
||||
scene.add(player);
|
||||
|
||||
scene.add(new three.AmbientLight(0xa8c7ff, 0.7));
|
||||
const keyLight = new three.DirectionalLight(0xffffff, 1.75);
|
||||
keyLight.position.set(-3.5, 6, 4);
|
||||
scene.add(keyLight);
|
||||
const rimLight = new three.PointLight(0x67a6ff, 6, 9);
|
||||
rimLight.position.set(3, 2.8, -4);
|
||||
scene.add(rimLight);
|
||||
camera.position.set(3.4, 5.5, 6.8);
|
||||
camera.lookAt(0, 0, 0);
|
||||
this.updateScene();
|
||||
}
|
||||
|
||||
private updateScene() {
|
||||
this.renderFallback();
|
||||
const three = this.three;
|
||||
const { player } = this.sceneObjects ?? {};
|
||||
if (!three || !player) {
|
||||
return;
|
||||
}
|
||||
const size = parseIntegerAttribute(this, "board-size", 5);
|
||||
const playerIndex = parseIntegerAttribute(this, "player-index", 0);
|
||||
const target = boardPosition(playerIndex, size, three);
|
||||
player.position.copy(target);
|
||||
}
|
||||
|
||||
private renderFrame = () => {
|
||||
const three = this.three;
|
||||
const objects = this.sceneObjects;
|
||||
if (!three || !objects) {
|
||||
return;
|
||||
}
|
||||
const size = parseIntegerAttribute(this, "board-size", 5);
|
||||
const playerIndex = parseIntegerAttribute(this, "player-index", 0);
|
||||
const target = boardPosition(playerIndex, size, three);
|
||||
objects.player.position.lerp(target, 0.2);
|
||||
objects.player.rotation.y += 0.035;
|
||||
objects.player.position.y = target.y + Math.sin(performance.now() / 210) * 0.06;
|
||||
const orbit = performance.now() / 8200;
|
||||
objects.camera.position.x = Math.sin(orbit) * 1.2 + 3.4;
|
||||
objects.camera.position.z = Math.cos(orbit) * 1.2 + 6.8;
|
||||
objects.camera.lookAt(0, 0, 0);
|
||||
objects.renderer.render(objects.scene, objects.camera);
|
||||
this.animationFrame = requestAnimationFrame(this.renderFrame);
|
||||
};
|
||||
}
|
||||
|
||||
if ("customElements" in globalThis && !customElements.get("workboard-3d-game")) {
|
||||
customElements.define("workboard-3d-game", Workboard3dGameElement);
|
||||
}
|
||||
@@ -197,6 +197,204 @@ describe("renderWorkboard", () => {
|
||||
expect(container.querySelector(".workboard-card")?.getAttribute("role")).toBeNull();
|
||||
});
|
||||
|
||||
it("hides autonomous model override actions for non-admin operators", () => {
|
||||
const host = {};
|
||||
const state = getWorkboardState(host);
|
||||
state.loaded = true;
|
||||
state.cards = [
|
||||
{
|
||||
id: "card-1",
|
||||
title: "Start with default model",
|
||||
status: "todo",
|
||||
priority: "normal",
|
||||
labels: [],
|
||||
position: 1000,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
},
|
||||
];
|
||||
const container = document.createElement("div");
|
||||
|
||||
render(
|
||||
renderWorkboard({
|
||||
host,
|
||||
client: null,
|
||||
connected: true,
|
||||
canModelOverride: false,
|
||||
pluginEnabled: true,
|
||||
agentsList: null,
|
||||
sessions: [],
|
||||
onOpenSession: () => undefined,
|
||||
}),
|
||||
container,
|
||||
);
|
||||
|
||||
const startButtons = [
|
||||
...container.querySelectorAll<HTMLButtonElement>(".workboard-card__start"),
|
||||
];
|
||||
expect(startButtons.map((button) => button.textContent?.trim())).toEqual([
|
||||
"Start",
|
||||
"codex",
|
||||
"claude",
|
||||
]);
|
||||
expect(startButtons.map((button) => button.title)).toEqual([
|
||||
"Run default agent",
|
||||
"Open codex",
|
||||
"Open claude",
|
||||
]);
|
||||
});
|
||||
|
||||
it("renders linked Gateway task status on cards", () => {
|
||||
const host = {};
|
||||
const state = getWorkboardState(host);
|
||||
state.loaded = true;
|
||||
state.cards = [
|
||||
{
|
||||
id: "card-1",
|
||||
title: "Review task result",
|
||||
status: "running",
|
||||
priority: "normal",
|
||||
labels: [],
|
||||
position: 1000,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
sessionKey: "agent:main:subagent:workboard-default-card-1",
|
||||
runId: "run-1",
|
||||
taskId: "task-1",
|
||||
},
|
||||
];
|
||||
state.tasksByCardId.set("card-1", {
|
||||
id: "task-1",
|
||||
taskId: "task-1",
|
||||
status: "completed",
|
||||
title: "Review task result",
|
||||
childSessionKey: "agent:main:subagent:workboard-default-card-1",
|
||||
runId: "run-1",
|
||||
terminalSummary: "Ready for operator review.",
|
||||
});
|
||||
const container = document.createElement("div");
|
||||
|
||||
render(
|
||||
renderWorkboard({
|
||||
host,
|
||||
client: null,
|
||||
connected: true,
|
||||
pluginEnabled: true,
|
||||
agentsList: null,
|
||||
sessions: [],
|
||||
onOpenSession: () => undefined,
|
||||
}),
|
||||
container,
|
||||
);
|
||||
|
||||
expect(container.textContent).toContain("task linked");
|
||||
expect(container.textContent).toContain("Task complete");
|
||||
expect(container.textContent).toContain("Ready for operator review.");
|
||||
});
|
||||
|
||||
it("uses terminal session lifecycle when cached task status is stale", () => {
|
||||
const host = {};
|
||||
const state = getWorkboardState(host);
|
||||
state.loaded = true;
|
||||
state.cards = [
|
||||
{
|
||||
id: "card-1",
|
||||
title: "Finished despite stale task",
|
||||
status: "running",
|
||||
priority: "normal",
|
||||
labels: [],
|
||||
position: 1000,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
sessionKey: "agent:main:subagent:workboard-default-card-1",
|
||||
runId: "run-1",
|
||||
taskId: "task-1",
|
||||
},
|
||||
];
|
||||
state.tasksByCardId.set("card-1", {
|
||||
id: "task-1",
|
||||
taskId: "task-1",
|
||||
status: "running",
|
||||
title: "Finished despite stale task",
|
||||
childSessionKey: "agent:main:subagent:workboard-default-card-1",
|
||||
runId: "run-1",
|
||||
progressSummary: "Still running according to stale cache.",
|
||||
});
|
||||
const container = document.createElement("div");
|
||||
|
||||
render(
|
||||
renderWorkboard({
|
||||
host,
|
||||
client: null,
|
||||
connected: true,
|
||||
pluginEnabled: true,
|
||||
agentsList: null,
|
||||
sessions: [
|
||||
{
|
||||
key: "agent:main:subagent:workboard-default-card-1",
|
||||
kind: "direct",
|
||||
displayName: "Finished session",
|
||||
updatedAt: 2,
|
||||
hasActiveRun: false,
|
||||
status: "done",
|
||||
},
|
||||
],
|
||||
onOpenSession: () => undefined,
|
||||
}),
|
||||
container,
|
||||
);
|
||||
|
||||
expect(container.textContent).toContain("Done");
|
||||
expect(container.textContent).toContain("Finished session");
|
||||
expect(container.textContent).not.toContain("Task running");
|
||||
expect(container.textContent).not.toContain("Still running according to stale cache.");
|
||||
});
|
||||
|
||||
it("shows stop controls without start controls for active task-only cards", () => {
|
||||
const host = {};
|
||||
const state = getWorkboardState(host);
|
||||
state.loaded = true;
|
||||
state.cards = [
|
||||
{
|
||||
id: "card-1",
|
||||
title: "Task only run",
|
||||
status: "running",
|
||||
priority: "normal",
|
||||
labels: [],
|
||||
position: 1000,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
taskId: "task-1",
|
||||
},
|
||||
];
|
||||
state.tasksByCardId.set("card-1", {
|
||||
id: "task-1",
|
||||
taskId: "task-1",
|
||||
status: "running",
|
||||
title: "Task only run",
|
||||
progressSummary: "Worker is active.",
|
||||
});
|
||||
const container = document.createElement("div");
|
||||
|
||||
render(
|
||||
renderWorkboard({
|
||||
host,
|
||||
client: null,
|
||||
connected: true,
|
||||
pluginEnabled: true,
|
||||
agentsList: null,
|
||||
sessions: [],
|
||||
onOpenSession: () => undefined,
|
||||
}),
|
||||
container,
|
||||
);
|
||||
|
||||
expect(container.textContent).toContain("Task running");
|
||||
expect(container.querySelector('button[title="Stop session"]')).not.toBeNull();
|
||||
expect(container.querySelectorAll<HTMLButtonElement>(".workboard-card__start")).toHaveLength(0);
|
||||
expect(container.querySelector(".workboard-card")?.getAttribute("role")).toBeNull();
|
||||
});
|
||||
|
||||
it("hides write controls for read-only operators", () => {
|
||||
const host = {};
|
||||
const state = getWorkboardState(host);
|
||||
@@ -347,47 +545,6 @@ describe("renderWorkboard", () => {
|
||||
).toContain("Verification:");
|
||||
});
|
||||
|
||||
it("opens and plays the mini game", () => {
|
||||
const host = {};
|
||||
const state = getWorkboardState(host);
|
||||
state.loaded = true;
|
||||
const container = document.createElement("div");
|
||||
const props = {
|
||||
host,
|
||||
client: null,
|
||||
connected: true,
|
||||
pluginEnabled: true,
|
||||
agentsList: null,
|
||||
sessions: [],
|
||||
onOpenSession: () => undefined,
|
||||
onRequestUpdate: () => undefined,
|
||||
};
|
||||
|
||||
render(renderWorkboard(props), container);
|
||||
[...container.querySelectorAll<HTMLButtonElement>(".workboard-toolbar__actions .btn")]
|
||||
.find((button) => button.textContent?.includes("Mini game"))
|
||||
?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
render(renderWorkboard(props), container);
|
||||
|
||||
expect(container.querySelector('[role="dialog"]')?.textContent).toContain("Card Chase");
|
||||
expect(container.querySelector("workboard-3d-game")?.getAttribute("player-index")).toBe("0");
|
||||
expect(container.querySelector("workboard-3d-game")?.getAttribute("aria-hidden")).toBe("true");
|
||||
expect(container.querySelector('[role="grid"]')?.getAttribute("aria-label")).toBe(
|
||||
"Card Chase board",
|
||||
);
|
||||
expect(container.querySelector('[role="gridcell"][aria-label="Agent 1"]')).not.toBeNull();
|
||||
const controls = container.querySelector<HTMLElement>(".workboard-game__controls");
|
||||
controls
|
||||
?.querySelector<HTMLButtonElement>('button[aria-label="Move right"]')
|
||||
?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
render(renderWorkboard(props), container);
|
||||
|
||||
expect(state.gamePlayerIndex).toBe(1);
|
||||
expect(container.querySelector("workboard-3d-game")?.getAttribute("player-index")).toBe("1");
|
||||
expect(container.querySelector('[role="gridcell"][aria-label="Agent 2"]')).not.toBeNull();
|
||||
expect(container.querySelector(".workboard-game__stats")?.textContent).toContain("Moves 1");
|
||||
});
|
||||
|
||||
it("renders card event history", () => {
|
||||
const host = {};
|
||||
const state = getWorkboardState(host);
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
type WorkboardLifecycle,
|
||||
type WorkboardPriority,
|
||||
type WorkboardStatus,
|
||||
type WorkboardTaskSummary,
|
||||
type WorkboardTemplateId,
|
||||
type WorkboardUiState,
|
||||
} from "../controllers/workboard.ts";
|
||||
@@ -29,13 +30,13 @@ import { formatDateMs } from "../format.ts";
|
||||
import type { GatewayBrowserClient } from "../gateway.ts";
|
||||
import { icons } from "../icons.ts";
|
||||
import type { AgentsListResult, GatewaySessionRow } from "../types.ts";
|
||||
import "./workboard-3d-game.ts";
|
||||
|
||||
type WorkboardProps = {
|
||||
host: object;
|
||||
client: GatewayBrowserClient | null;
|
||||
connected: boolean;
|
||||
canWrite?: boolean;
|
||||
canModelOverride?: boolean;
|
||||
pluginEnabled: boolean;
|
||||
agentsList: AgentsListResult | null;
|
||||
sessions: GatewaySessionRow[];
|
||||
@@ -43,9 +44,6 @@ type WorkboardProps = {
|
||||
onRequestUpdate?: () => void;
|
||||
};
|
||||
|
||||
const WORKBOARD_GAME_SIZE = 5;
|
||||
const WORKBOARD_GAME_GOAL = WORKBOARD_GAME_SIZE * WORKBOARD_GAME_SIZE - 1;
|
||||
const WORKBOARD_GAME_BLOCKERS = new Set([6, 8, 12, 16, 18]);
|
||||
const WORKBOARD_TEMPLATES: Array<{
|
||||
id: WorkboardTemplateId;
|
||||
title: string;
|
||||
@@ -189,52 +187,50 @@ function renderEvents(card: WorkboardCard) {
|
||||
|
||||
function renderMetadataBadges(card: WorkboardCard) {
|
||||
const metadata = card.metadata;
|
||||
if (!metadata) {
|
||||
return nothing;
|
||||
}
|
||||
const badges = [
|
||||
metadata.templateId ? t(`workboard.template.${metadata.templateId}`) : null,
|
||||
metadata.attempts?.length
|
||||
metadata?.templateId ? t(`workboard.template.${metadata.templateId}`) : null,
|
||||
card.taskId ? t("workboard.badgeTaskLinked") : null,
|
||||
metadata?.attempts?.length
|
||||
? t("workboard.badgeAttempts", { count: String(metadata.attempts.length) })
|
||||
: null,
|
||||
metadata.failureCount
|
||||
metadata?.failureCount
|
||||
? t("workboard.badgeFailures", { count: String(metadata.failureCount) })
|
||||
: null,
|
||||
metadata.comments?.length
|
||||
metadata?.comments?.length
|
||||
? t("workboard.badgeComments", { count: String(metadata.comments.length) })
|
||||
: null,
|
||||
metadata.links?.length
|
||||
metadata?.links?.length
|
||||
? t("workboard.badgeLinks", { count: String(metadata.links.length) })
|
||||
: null,
|
||||
metadata.proof?.length
|
||||
metadata?.proof?.length
|
||||
? t("workboard.badgeProof", { count: String(metadata.proof.length) })
|
||||
: null,
|
||||
metadata.artifacts?.length
|
||||
metadata?.artifacts?.length
|
||||
? t("workboard.badgeArtifacts", { count: String(metadata.artifacts.length) })
|
||||
: null,
|
||||
metadata.attachments?.length
|
||||
metadata?.attachments?.length
|
||||
? t("workboard.badgeAttachments", { count: String(metadata.attachments.length) })
|
||||
: null,
|
||||
metadata.workerLogs?.length
|
||||
metadata?.workerLogs?.length
|
||||
? t("workboard.badgeWorkerLogs", { count: String(metadata.workerLogs.length) })
|
||||
: null,
|
||||
metadata.workerProtocol?.state
|
||||
metadata?.workerProtocol?.state
|
||||
? t("workboard.badgeWorkerProtocol", { state: metadata.workerProtocol.state })
|
||||
: null,
|
||||
metadata.automation?.tenant
|
||||
metadata?.automation?.tenant
|
||||
? t("workboard.badgeTenant", { tenant: metadata.automation.tenant })
|
||||
: null,
|
||||
metadata.automation?.skills?.length
|
||||
metadata?.automation?.skills?.length
|
||||
? t("workboard.badgeSkills", { count: String(metadata.automation.skills.length) })
|
||||
: null,
|
||||
metadata.automation?.dispatchCount
|
||||
metadata?.automation?.dispatchCount
|
||||
? t("workboard.badgeDispatches", { count: String(metadata.automation.dispatchCount) })
|
||||
: null,
|
||||
metadata.claim ? t("workboard.badgeClaimed", { owner: metadata.claim.ownerId }) : null,
|
||||
metadata.diagnostics?.length
|
||||
metadata?.claim ? t("workboard.badgeClaimed", { owner: metadata.claim.ownerId }) : null,
|
||||
metadata?.diagnostics?.length
|
||||
? t("workboard.badgeDiagnostics", { count: String(metadata.diagnostics.length) })
|
||||
: null,
|
||||
metadata.stale ? t("workboard.badgeStale") : null,
|
||||
metadata?.stale ? t("workboard.badgeStale") : null,
|
||||
].filter((badge): badge is string => Boolean(badge));
|
||||
if (badges.length === 0) {
|
||||
return nothing;
|
||||
@@ -361,41 +357,6 @@ function openCreateModal(state: WorkboardUiState) {
|
||||
state.draftOpen = true;
|
||||
}
|
||||
|
||||
function resetGame(state: WorkboardUiState) {
|
||||
state.gamePlayerIndex = 0;
|
||||
state.gameMoves = 0;
|
||||
state.gameMessage = "workboard.gameStart";
|
||||
}
|
||||
|
||||
function moveGamePlayer(state: WorkboardUiState, delta: number) {
|
||||
if (state.gamePlayerIndex === WORKBOARD_GAME_GOAL) {
|
||||
resetGame(state);
|
||||
}
|
||||
const currentRow = Math.floor(state.gamePlayerIndex / WORKBOARD_GAME_SIZE);
|
||||
const nextIndex = state.gamePlayerIndex + delta;
|
||||
const nextRow = Math.floor(nextIndex / WORKBOARD_GAME_SIZE);
|
||||
if (
|
||||
nextIndex < 0 ||
|
||||
nextIndex > WORKBOARD_GAME_GOAL ||
|
||||
(delta === -1 && nextRow !== currentRow) ||
|
||||
(delta === 1 && nextRow !== currentRow)
|
||||
) {
|
||||
state.gameMessage = "workboard.gameBoundary";
|
||||
return;
|
||||
}
|
||||
if (WORKBOARD_GAME_BLOCKERS.has(nextIndex)) {
|
||||
state.gameMessage = "workboard.gameBlocked";
|
||||
return;
|
||||
}
|
||||
state.gamePlayerIndex = nextIndex;
|
||||
state.gameMoves += 1;
|
||||
state.gameMessage =
|
||||
nextIndex === WORKBOARD_GAME_GOAL ? "workboard.gameWin" : "workboard.gameContinue";
|
||||
if (nextIndex === WORKBOARD_GAME_GOAL) {
|
||||
state.gameWins += 1;
|
||||
}
|
||||
}
|
||||
|
||||
function openEditModal(state: WorkboardUiState, card: WorkboardCard) {
|
||||
state.draftOpen = true;
|
||||
state.editingCardId = card.id;
|
||||
@@ -422,148 +383,6 @@ function applyTemplate(state: WorkboardUiState, templateId: WorkboardTemplateId)
|
||||
state.draftPriority = template.priority;
|
||||
}
|
||||
|
||||
function renderGameArrow(
|
||||
label: string,
|
||||
className: string,
|
||||
delta: number,
|
||||
props: Pick<WorkboardProps, "host" | "onRequestUpdate">,
|
||||
) {
|
||||
const state = getWorkboardState(props.host);
|
||||
return html`
|
||||
<button
|
||||
class="btn btn--icon workboard-game__arrow ${className}"
|
||||
type="button"
|
||||
title=${label}
|
||||
aria-label=${label}
|
||||
@click=${() => {
|
||||
moveGamePlayer(state, delta);
|
||||
props.onRequestUpdate?.();
|
||||
}}
|
||||
>
|
||||
${icons.arrowDown}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
function gameCellLabel(state: WorkboardUiState, index: number) {
|
||||
const label =
|
||||
index === state.gamePlayerIndex
|
||||
? t("workboard.gameAgent")
|
||||
: index === WORKBOARD_GAME_GOAL
|
||||
? t("workboard.gameLaunch")
|
||||
: WORKBOARD_GAME_BLOCKERS.has(index)
|
||||
? t("workboard.gameBlockedCell")
|
||||
: t("workboard.gameOpenCell");
|
||||
return `${label} ${index + 1}`;
|
||||
}
|
||||
|
||||
function renderAccessibleGameGrid(state: WorkboardUiState) {
|
||||
return html`
|
||||
<div class="workboard-game__accessible-grid" role="grid" aria-label=${t("workboard.gameBoard")}>
|
||||
${Array.from({ length: WORKBOARD_GAME_SIZE * WORKBOARD_GAME_SIZE }, (_, index) => {
|
||||
return html`<span role="gridcell" aria-label=${gameCellLabel(state, index)}></span>`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderGameModal(props: WorkboardProps) {
|
||||
const state = getWorkboardState(props.host);
|
||||
if (!state.gameOpen) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<div
|
||||
class="workboard-modal"
|
||||
role="presentation"
|
||||
@click=${(event: MouseEvent) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
state.gameOpen = false;
|
||||
props.onRequestUpdate?.();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="workboard-game"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="workboard-game-title"
|
||||
tabindex="0"
|
||||
@keydown=${(event: KeyboardEvent) => {
|
||||
const moves: Record<string, number> = {
|
||||
ArrowDown: WORKBOARD_GAME_SIZE,
|
||||
ArrowLeft: -1,
|
||||
ArrowRight: 1,
|
||||
ArrowUp: -WORKBOARD_GAME_SIZE,
|
||||
};
|
||||
const delta = moves[event.key];
|
||||
if (typeof delta !== "number") {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
moveGamePlayer(state, delta);
|
||||
props.onRequestUpdate?.();
|
||||
}}
|
||||
>
|
||||
<div class="workboard-modal__header">
|
||||
<div>
|
||||
<h2 id="workboard-game-title">${t("workboard.gameTitle")}</h2>
|
||||
<p>${t(state.gameMessage)}</p>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn--icon workboard-card__icon"
|
||||
type="button"
|
||||
title=${t("common.cancel")}
|
||||
@click=${() => {
|
||||
state.gameOpen = false;
|
||||
props.onRequestUpdate?.();
|
||||
}}
|
||||
>
|
||||
${icons.x}
|
||||
</button>
|
||||
</div>
|
||||
<div class="workboard-game__stats">
|
||||
<span>${t("workboard.gameMoves", { count: String(state.gameMoves) })}</span>
|
||||
<span>${t("workboard.gameWins", { count: String(state.gameWins) })}</span>
|
||||
</div>
|
||||
<workboard-3d-game
|
||||
class="workboard-game__scene"
|
||||
board-size=${String(WORKBOARD_GAME_SIZE)}
|
||||
goal-index=${String(WORKBOARD_GAME_GOAL)}
|
||||
player-index=${String(state.gamePlayerIndex)}
|
||||
blockers=${[...WORKBOARD_GAME_BLOCKERS].join(",")}
|
||||
wins=${String(state.gameWins)}
|
||||
aria-hidden="true"
|
||||
></workboard-3d-game>
|
||||
${renderAccessibleGameGrid(state)}
|
||||
<div class="workboard-game__controls" aria-label=${t("workboard.gameControls")}>
|
||||
${renderGameArrow(
|
||||
t("workboard.gameMoveUp"),
|
||||
"workboard-game__arrow--up",
|
||||
-WORKBOARD_GAME_SIZE,
|
||||
props,
|
||||
)}
|
||||
${renderGameArrow(t("workboard.gameMoveLeft"), "workboard-game__arrow--left", -1, props)}
|
||||
${renderGameArrow(t("workboard.gameMoveDown"), "", WORKBOARD_GAME_SIZE, props)}
|
||||
${renderGameArrow(t("workboard.gameMoveRight"), "workboard-game__arrow--right", 1, props)}
|
||||
</div>
|
||||
<div class="workboard-modal__actions">
|
||||
<button
|
||||
class="btn"
|
||||
type="button"
|
||||
@click=${() => {
|
||||
resetGame(state);
|
||||
props.onRequestUpdate?.();
|
||||
}}
|
||||
>
|
||||
${t("common.reset")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderCardModal(props: WorkboardProps) {
|
||||
const state = getWorkboardState(props.host);
|
||||
const agents = props.agentsList?.agents ?? [];
|
||||
@@ -869,19 +688,56 @@ function formatLifecycle(lifecycle: WorkboardLifecycle): {
|
||||
throw new Error("Unknown workboard lifecycle state.");
|
||||
}
|
||||
|
||||
function renderLifecycle(card: WorkboardCard, sessions: readonly GatewaySessionRow[]) {
|
||||
const lifecycle = getWorkboardLifecycle(card, sessions);
|
||||
function taskDetail(task: WorkboardTaskSummary): string {
|
||||
if (task.status === "queued" || task.status === "running") {
|
||||
return task.progressSummary ?? task.title ?? task.taskId;
|
||||
}
|
||||
return task.terminalSummary ?? task.error ?? task.progressSummary ?? task.title ?? task.taskId;
|
||||
}
|
||||
|
||||
function taskMatchesLifecycle(task: WorkboardTaskSummary, lifecycle: WorkboardLifecycle): boolean {
|
||||
switch (task.status) {
|
||||
case "queued":
|
||||
case "running":
|
||||
return lifecycle.state === "running";
|
||||
case "completed":
|
||||
return lifecycle.state === "succeeded";
|
||||
case "failed":
|
||||
case "cancelled":
|
||||
case "timed_out":
|
||||
return lifecycle.state === "failed";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function taskIsActive(task: WorkboardTaskSummary | undefined): boolean {
|
||||
return task?.status === "queued" || task?.status === "running";
|
||||
}
|
||||
|
||||
function renderLifecycle(
|
||||
card: WorkboardCard,
|
||||
sessions: readonly GatewaySessionRow[],
|
||||
task?: WorkboardTaskSummary,
|
||||
) {
|
||||
const lifecycle = getWorkboardLifecycle(card, sessions, task);
|
||||
const formatted = formatLifecycle(lifecycle);
|
||||
const session = lifecycle.session;
|
||||
const execution = card.execution;
|
||||
const stale = lifecycle.state === "stale";
|
||||
const taskIsAuthoritative = task ? taskMatchesLifecycle(task, lifecycle) : false;
|
||||
const taskStatus = task && taskIsAuthoritative ? t(`workboard.taskStatus.${task.status}`) : null;
|
||||
return html`
|
||||
<div class="workboard-card__lifecycle">
|
||||
<span class="workboard-lifecycle workboard-lifecycle--${formatted.tone}">
|
||||
${stale || !execution ? formatted.label : `${execution.engine} ${execution.mode}`}
|
||||
${taskStatus ??
|
||||
(stale || !execution ? formatted.label : `${execution.engine} ${execution.mode}`)}
|
||||
</span>
|
||||
<span class="workboard-card__lifecycle-detail">
|
||||
${stale ? formatted.detail : (session?.displayName ?? session?.label ?? formatted.detail)}
|
||||
${task && taskIsAuthoritative
|
||||
? taskDetail(task)
|
||||
: stale
|
||||
? formatted.detail
|
||||
: (session?.displayName ?? session?.label ?? formatted.detail)}
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
@@ -927,37 +783,70 @@ function renderStartExecutionButton(
|
||||
}
|
||||
|
||||
function renderStartExecutionControls(props: WorkboardProps, card: WorkboardCard) {
|
||||
const canModelOverride = props.canModelOverride !== false;
|
||||
return html`
|
||||
<div class="workboard-card__execution-controls">
|
||||
${renderStartExecutionButton(props, card, null, "autonomous")}
|
||||
${renderStartExecutionButton(props, card, "codex", "autonomous")}
|
||||
${renderStartExecutionButton(props, card, "claude", "autonomous")}
|
||||
${canModelOverride
|
||||
? html`${renderStartExecutionButton(props, card, "codex", "autonomous")}
|
||||
${renderStartExecutionButton(props, card, "claude", "autonomous")}`
|
||||
: nothing}
|
||||
${renderStartExecutionButton(props, card, "codex", "manual")}
|
||||
${renderStartExecutionButton(props, card, "claude", "manual")}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderDispatchSummary(state: WorkboardUiState) {
|
||||
const summary = state.lastDispatchSummary;
|
||||
if (!summary) {
|
||||
return nothing;
|
||||
}
|
||||
const total =
|
||||
summary.started +
|
||||
summary.failures +
|
||||
summary.promoted +
|
||||
summary.blocked +
|
||||
summary.reclaimed +
|
||||
summary.orchestrated;
|
||||
const key = total === 0 ? "workboard.dispatchSummaryEmpty" : "workboard.dispatchSummary";
|
||||
return html`
|
||||
<div class="callout">
|
||||
${t(key, {
|
||||
started: String(summary.started),
|
||||
failures: String(summary.failures),
|
||||
promoted: String(summary.promoted),
|
||||
blocked: String(summary.blocked),
|
||||
reclaimed: String(summary.reclaimed),
|
||||
orchestrated: String(summary.orchestrated),
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderCard(props: WorkboardProps, card: WorkboardCard) {
|
||||
const state = getWorkboardState(props.host);
|
||||
const task = state.tasksByCardId.get(card.id);
|
||||
const session = findWorkboardSession(card, props.sessions);
|
||||
const busy = state.busyCardId === card.id;
|
||||
const syncing = state.syncingCardIds.has(card.id);
|
||||
const activeTask = taskIsActive(task);
|
||||
const live =
|
||||
activeTask ||
|
||||
session?.hasActiveRun === true ||
|
||||
(session?.hasActiveRun !== false && session?.status === "running");
|
||||
const linkedSessionKey = card.sessionKey ?? card.execution?.sessionKey;
|
||||
const linked = Boolean(linkedSessionKey);
|
||||
const openable = Boolean(linkedSessionKey);
|
||||
const writable = canMutate(props);
|
||||
const showStartControls = writable && (!linked || !session);
|
||||
const showStartControls = writable && !activeTask && (!linkedSessionKey || !session);
|
||||
return html`
|
||||
<article
|
||||
class="workboard-card priority-${card.priority} ${busy ? "workboard-card--busy" : ""} ${linked
|
||||
? "workboard-card--openable"
|
||||
: ""}"
|
||||
role=${linked ? "button" : nothing}
|
||||
tabindex=${linked ? 0 : nothing}
|
||||
title=${linked ? t("workboard.openLinkedSession") : nothing}
|
||||
class="workboard-card priority-${card.priority} ${busy
|
||||
? "workboard-card--busy"
|
||||
: ""} ${openable ? "workboard-card--openable" : ""}"
|
||||
role=${openable ? "button" : nothing}
|
||||
tabindex=${openable ? 0 : nothing}
|
||||
title=${openable ? t("workboard.openLinkedSession") : nothing}
|
||||
draggable=${writable ? "true" : "false"}
|
||||
@click=${(event: MouseEvent) => {
|
||||
if (!isCardActionTarget(event)) {
|
||||
@@ -993,7 +882,8 @@ function renderCard(props: WorkboardProps, card: WorkboardCard) {
|
||||
${syncing ? html`<span class="workboard-live">${t("common.saving")}</span>` : nothing}
|
||||
</div>
|
||||
<h3>${card.title}</h3>
|
||||
${card.notes ? html`<p>${card.notes}</p>` : nothing} ${renderLifecycle(card, props.sessions)}
|
||||
${card.notes ? html`<p>${card.notes}</p>` : nothing}
|
||||
${renderLifecycle(card, props.sessions, task)}
|
||||
${card.labels.length
|
||||
? html`<div class="workboard-labels">
|
||||
${card.labels.map((label) => html`<span>${label}</span>`)}
|
||||
@@ -1022,12 +912,12 @@ function renderCard(props: WorkboardProps, card: WorkboardCard) {
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
${linked
|
||||
${linkedSessionKey
|
||||
? html`
|
||||
<button
|
||||
class="btn btn--icon workboard-card__icon"
|
||||
title=${t("workboard.openSession")}
|
||||
@click=${() => props.onOpenSession(linkedSessionKey!)}
|
||||
@click=${() => props.onOpenSession(linkedSessionKey)}
|
||||
>
|
||||
${icons.messageSquare}
|
||||
</button>
|
||||
@@ -1051,6 +941,24 @@ function renderCard(props: WorkboardProps, card: WorkboardCard) {
|
||||
: nothing}
|
||||
`
|
||||
: nothing}
|
||||
${!linkedSessionKey && writable && activeTask
|
||||
? html`
|
||||
<button
|
||||
class="btn btn--icon workboard-card__icon"
|
||||
title=${t("workboard.stopSession")}
|
||||
?disabled=${busy || !props.connected}
|
||||
@click=${() =>
|
||||
stopWorkboardCard({
|
||||
host: props.host,
|
||||
client: props.client,
|
||||
card,
|
||||
requestUpdate: props.onRequestUpdate,
|
||||
})}
|
||||
>
|
||||
${icons.stop}
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
${showStartControls ? renderStartExecutionControls(props, card) : nothing}
|
||||
${writable
|
||||
? html`
|
||||
@@ -1231,15 +1139,6 @@ export function renderWorkboard(props: WorkboardProps) {
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
<button
|
||||
class="btn"
|
||||
@click=${() => {
|
||||
state.gameOpen = true;
|
||||
props.onRequestUpdate?.();
|
||||
}}
|
||||
>
|
||||
${icons.play} ${t("workboard.gameButton")}
|
||||
</button>
|
||||
${writable
|
||||
? html`
|
||||
<button
|
||||
@@ -1256,7 +1155,7 @@ export function renderWorkboard(props: WorkboardProps) {
|
||||
</div>
|
||||
</div>
|
||||
${state.error ? html`<div class="callout danger">${state.error}</div>` : nothing}
|
||||
${renderGameModal(props)} ${renderCardModal(props)}
|
||||
${renderDispatchSummary(state)} ${renderCardModal(props)}
|
||||
<div class="workboard-board">
|
||||
${state.statuses.map((status) => renderColumn(props, status, byStatus.get(status) ?? []))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user