fix(workboard): tighten controls and track card events

This commit is contained in:
Peter Steinberger
2026-05-29 01:17:11 +01:00
parent 7e59e43ce6
commit ab3eca14f1
48 changed files with 1224 additions and 241 deletions

View File

@@ -48,10 +48,16 @@ Each card stores:
- optional agent id
- optional linked session, run, task, or source URL
- optional execution metadata for a Codex or Claude session started from the card
- recent card events such as created, moved, linked, or agent-updated changes
Cards are stored in the plugin's Gateway state. They are local to the Gateway
state directory and move with the rest of that Gateway's OpenClaw state.
Workboard keeps a compact per-card event history so operators can see how a
card moved through the board without opening the linked session. The event trail
is intentionally local metadata; it does not replace session transcripts or
GitHub issue history.
## Card executions
Unlinked cards can start work from the card. Start uses the Gateway's configured
@@ -74,6 +80,9 @@ Cards can be linked to existing dashboard sessions or to the session created
when you start work from a card. Linked cards show the session lifecycle inline:
running, linked idle, done, failed, or missing.
If the linked session is missing, the card stays linked for context and still
offers start controls so you can restart work into a fresh dashboard session.
You can also capture an existing dashboard session from the Sessions tab with
Add to Workboard. The card is linked to that session, uses the session label or
recent user prompt as the title, and seeds notes from the recent user prompt plus

View File

@@ -34,6 +34,15 @@ describe("WorkboardStore", () => {
expect((await store.list()).map((card) => card.id)).toEqual([todo.id, review.id]);
expect(review.labels).toEqual(["release", "docs"]);
expect(review.priority).toBe("high");
expect(review.events?.[0]).toMatchObject({ kind: "created", toStatus: "review" });
});
it("preserves explicit zero positions", async () => {
const store = new WorkboardStore(createMemoryStore());
const card = await store.create({ title: "Top card", status: "todo", position: 0 });
expect(card.position).toBe(0);
});
it("keeps initial session, run, and task links when creating cards", async () => {
@@ -77,6 +86,11 @@ describe("WorkboardStore", () => {
expect(running.status).toBe("running");
expect(running.position).toBe(500);
expect(running.startedAt).toBeGreaterThanOrEqual(card.createdAt);
expect(running.events?.at(-1)).toMatchObject({
kind: "moved",
fromStatus: "todo",
toStatus: "running",
});
const done = await store.update(card.id, { status: "done" });
expect(done.completedAt).toBeGreaterThanOrEqual(done.startedAt ?? 0);
@@ -103,6 +117,10 @@ describe("WorkboardStore", () => {
const relinked = await store.update(card.id, { sessionKey: "agent:main:dashboard:2" });
expect(relinked.sessionKey).toBe("agent:main:dashboard:2");
expect(relinked.execution?.sessionKey).toBe("agent:main:dashboard:2");
expect(relinked.events?.at(-1)).toMatchObject({
kind: "linked",
sessionKey: "agent:main:dashboard:2",
});
const unlinked = await store.update(card.id, { sessionKey: "" });
expect(unlinked.sessionKey).toBeUndefined();

View File

@@ -3,9 +3,12 @@ import {
WORKBOARD_EXECUTION_ENGINES,
WORKBOARD_EXECUTION_MODES,
WORKBOARD_EXECUTION_STATUSES,
WORKBOARD_EVENT_KINDS,
WORKBOARD_PRIORITIES,
WORKBOARD_STATUSES,
type WorkboardCard,
type WorkboardEvent,
type WorkboardEventKind,
type WorkboardExecution,
type WorkboardExecutionEngine,
type WorkboardExecutionMode,
@@ -16,6 +19,7 @@ import {
const POSITION_STEP = 1000;
const MAX_CARDS = 2000;
const MAX_CARD_EVENTS = 50;
export type PersistedWorkboardCard = {
version: 1;
@@ -170,6 +174,52 @@ function normalizeTimestamp(value: unknown, fallback: number): number {
: fallback;
}
function normalizeEvent(value: unknown): WorkboardEvent | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
const record = value as Record<string, unknown>;
const id = normalizeOptionalString(record.id);
const kind = WORKBOARD_EVENT_KINDS.includes(record.kind as WorkboardEventKind)
? (record.kind as WorkboardEventKind)
: null;
const at = normalizeTimestamp(record.at, 0);
if (!id || !kind || !at) {
return null;
}
const fromStatus =
typeof record.fromStatus === "string" &&
WORKBOARD_STATUSES.includes(record.fromStatus as WorkboardStatus)
? (record.fromStatus as WorkboardStatus)
: undefined;
const toStatus =
typeof record.toStatus === "string" &&
WORKBOARD_STATUSES.includes(record.toStatus as WorkboardStatus)
? (record.toStatus as WorkboardStatus)
: undefined;
const sessionKey = normalizeOptionalString(record.sessionKey);
const runId = normalizeOptionalString(record.runId);
return {
id,
kind,
at,
...(fromStatus ? { fromStatus } : {}),
...(toStatus ? { toStatus } : {}),
...(sessionKey ? { sessionKey } : {}),
...(runId ? { runId } : {}),
};
}
function normalizeEvents(value: unknown): WorkboardEvent[] {
if (!Array.isArray(value)) {
return [];
}
return value
.map(normalizeEvent)
.filter((event): event is WorkboardEvent => event !== null)
.slice(-MAX_CARD_EVENTS);
}
function normalizeExecution(value: unknown): WorkboardExecution | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return undefined;
@@ -234,6 +284,60 @@ function compareCards(left: WorkboardCard, right: WorkboardCard): number {
return left.createdAt - right.createdAt;
}
function cardSessionKey(card: WorkboardCard): string | undefined {
return card.sessionKey ?? card.execution?.sessionKey;
}
function cardRunId(card: WorkboardCard): string | undefined {
return card.runId ?? card.execution?.runId;
}
function appendEvent(
card: WorkboardCard,
event: Omit<WorkboardEvent, "id" | "at">,
at = Date.now(),
): WorkboardEvent[] {
return [
...normalizeEvents(card.events),
{
id: randomUUID(),
at,
...event,
},
].slice(-MAX_CARD_EVENTS);
}
function updateEvent(
existing: WorkboardCard,
next: WorkboardCard,
): Omit<WorkboardEvent, "id" | "at"> {
if (existing.status !== next.status || existing.position !== next.position) {
return {
kind: "moved",
fromStatus: existing.status,
toStatus: next.status,
};
}
if (cardSessionKey(existing) !== cardSessionKey(next)) {
return {
kind: "linked",
...(cardSessionKey(next) ? { sessionKey: cardSessionKey(next) } : {}),
};
}
if (
existing.execution?.status !== next.execution?.status ||
existing.execution?.engine !== next.execution?.engine ||
cardRunId(existing) !== cardRunId(next)
) {
return {
kind: "execution_updated",
...(cardSessionKey(next) ? { sessionKey: cardSessionKey(next) } : {}),
...(cardRunId(next) ? { runId: cardRunId(next) } : {}),
};
}
return { kind: "edited" };
}
function removeUndefinedCardFields(card: WorkboardCard): WorkboardCard {
const next = { ...card };
for (const key of [
@@ -277,10 +381,13 @@ export class WorkboardStore {
const now = Date.now();
const status = normalizeStatus(input.status, "todo");
const cards = await this.list();
const position =
normalizePosition(input.position, 0) ||
Math.max(0, ...cards.filter((card) => card.status === status).map((card) => card.position)) +
POSITION_STEP;
const normalizedPosition = normalizePosition(input.position, Number.NaN);
const position = Number.isFinite(normalizedPosition)
? normalizedPosition
: Math.max(
0,
...cards.filter((card) => card.status === status).map((card) => card.position),
) + POSITION_STEP;
const notes = normalizeNotes(input.notes);
const agentId = normalizeOptionalString(input.agentId);
const sessionKey = normalizeOptionalString(input.sessionKey);
@@ -297,6 +404,16 @@ export class WorkboardStore {
position,
createdAt: now,
updatedAt: now,
events: [
{
id: randomUUID(),
kind: "created",
at: now,
toStatus: status,
...(sessionKey ? { sessionKey } : {}),
...(runId ? { runId } : {}),
},
],
...(notes ? { notes } : {}),
...(agentId ? { agentId } : {}),
...(sessionKey ? { sessionKey } : {}),
@@ -356,6 +473,7 @@ export class WorkboardStore {
...(startedAt ? { startedAt } : {}),
...(completedAt ? { completedAt } : {}),
});
next.events = appendEvent(next, updateEvent(existing, next), now);
if (status !== "done") {
delete next.completedAt;
}

View File

@@ -17,12 +17,20 @@ export const WORKBOARD_EXECUTION_STATUSES = [
"blocked",
"done",
] as const;
export const WORKBOARD_EVENT_KINDS = [
"created",
"edited",
"moved",
"linked",
"execution_updated",
] as const;
export type WorkboardStatus = (typeof WORKBOARD_STATUSES)[number];
export type WorkboardPriority = (typeof WORKBOARD_PRIORITIES)[number];
export type WorkboardExecutionEngine = (typeof WORKBOARD_EXECUTION_ENGINES)[number];
export type WorkboardExecutionMode = (typeof WORKBOARD_EXECUTION_MODES)[number];
export type WorkboardExecutionStatus = (typeof WORKBOARD_EXECUTION_STATUSES)[number];
export type WorkboardEventKind = (typeof WORKBOARD_EVENT_KINDS)[number];
export type WorkboardExecution = {
id: string;
@@ -37,6 +45,16 @@ export type WorkboardExecution = {
updatedAt: number;
};
export type WorkboardEvent = {
id: string;
kind: WorkboardEventKind;
at: number;
fromStatus?: WorkboardStatus;
toStatus?: WorkboardStatus;
sessionKey?: string;
runId?: string;
};
export type WorkboardCard = {
id: string;
title: string;
@@ -55,6 +73,7 @@ export type WorkboardCard = {
updatedAt: number;
startedAt?: number;
completedAt?: number;
events?: WorkboardEvent[];
};
export type WorkboardListResult = {

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-05-28T18:48:31.545Z",
"generatedAt": "2026-05-28T23:49:35.013Z",
"locale": "ar",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "7c867296be27a09ab0e35f76d2518f479e24ab667179c5b3fabf83d6c57f3ef9",
"totalKeys": 1158,
"translatedKeys": 1158,
"sourceHash": "b966847cbbaca64097b1e105dcc26ad803434cfb2d3b39cae3edc2be3874cfda",
"totalKeys": 1238,
"translatedKeys": 1238,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-05-28T18:48:21.334Z",
"generatedAt": "2026-05-28T23:49:02.619Z",
"locale": "de",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "7c867296be27a09ab0e35f76d2518f479e24ab667179c5b3fabf83d6c57f3ef9",
"totalKeys": 1158,
"translatedKeys": 1158,
"sourceHash": "b966847cbbaca64097b1e105dcc26ad803434cfb2d3b39cae3edc2be3874cfda",
"totalKeys": 1238,
"translatedKeys": 1238,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-05-28T18:48:23.802Z",
"generatedAt": "2026-05-28T23:49:07.283Z",
"locale": "es",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "7c867296be27a09ab0e35f76d2518f479e24ab667179c5b3fabf83d6c57f3ef9",
"totalKeys": 1158,
"translatedKeys": 1158,
"sourceHash": "b966847cbbaca64097b1e105dcc26ad803434cfb2d3b39cae3edc2be3874cfda",
"totalKeys": 1238,
"translatedKeys": 1238,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-05-28T18:48:52.280Z",
"generatedAt": "2026-05-28T23:51:04.444Z",
"locale": "fa",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "7c867296be27a09ab0e35f76d2518f479e24ab667179c5b3fabf83d6c57f3ef9",
"totalKeys": 1158,
"translatedKeys": 1158,
"sourceHash": "b966847cbbaca64097b1e105dcc26ad803434cfb2d3b39cae3edc2be3874cfda",
"totalKeys": 1238,
"translatedKeys": 1238,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-05-28T18:48:29.028Z",
"generatedAt": "2026-05-28T23:49:27.030Z",
"locale": "fr",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "7c867296be27a09ab0e35f76d2518f479e24ab667179c5b3fabf83d6c57f3ef9",
"totalKeys": 1158,
"translatedKeys": 1158,
"sourceHash": "b966847cbbaca64097b1e105dcc26ad803434cfb2d3b39cae3edc2be3874cfda",
"totalKeys": 1238,
"translatedKeys": 1238,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-05-28T18:48:42.075Z",
"generatedAt": "2026-05-28T23:50:00.392Z",
"locale": "id",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "7c867296be27a09ab0e35f76d2518f479e24ab667179c5b3fabf83d6c57f3ef9",
"totalKeys": 1158,
"translatedKeys": 1158,
"sourceHash": "b966847cbbaca64097b1e105dcc26ad803434cfb2d3b39cae3edc2be3874cfda",
"totalKeys": 1238,
"translatedKeys": 1238,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-05-28T18:48:33.241Z",
"generatedAt": "2026-05-28T23:49:40.467Z",
"locale": "it",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "7c867296be27a09ab0e35f76d2518f479e24ab667179c5b3fabf83d6c57f3ef9",
"totalKeys": 1158,
"translatedKeys": 1158,
"sourceHash": "b966847cbbaca64097b1e105dcc26ad803434cfb2d3b39cae3edc2be3874cfda",
"totalKeys": 1238,
"translatedKeys": 1238,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-05-28T18:48:25.623Z",
"generatedAt": "2026-05-28T23:49:14.097Z",
"locale": "ja-JP",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "7c867296be27a09ab0e35f76d2518f479e24ab667179c5b3fabf83d6c57f3ef9",
"totalKeys": 1158,
"translatedKeys": 1158,
"sourceHash": "b966847cbbaca64097b1e105dcc26ad803434cfb2d3b39cae3edc2be3874cfda",
"totalKeys": 1238,
"translatedKeys": 1238,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-05-28T18:48:27.331Z",
"generatedAt": "2026-05-28T23:49:19.581Z",
"locale": "ko",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "7c867296be27a09ab0e35f76d2518f479e24ab667179c5b3fabf83d6c57f3ef9",
"totalKeys": 1158,
"translatedKeys": 1158,
"sourceHash": "b966847cbbaca64097b1e105dcc26ad803434cfb2d3b39cae3edc2be3874cfda",
"totalKeys": 1238,
"translatedKeys": 1238,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-05-28T18:48:49.875Z",
"generatedAt": "2026-05-28T23:50:48.437Z",
"locale": "nl",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "7c867296be27a09ab0e35f76d2518f479e24ab667179c5b3fabf83d6c57f3ef9",
"totalKeys": 1158,
"translatedKeys": 1158,
"sourceHash": "b966847cbbaca64097b1e105dcc26ad803434cfb2d3b39cae3edc2be3874cfda",
"totalKeys": 1238,
"translatedKeys": 1238,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-05-28T18:48:43.834Z",
"generatedAt": "2026-05-28T23:50:12.160Z",
"locale": "pl",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "7c867296be27a09ab0e35f76d2518f479e24ab667179c5b3fabf83d6c57f3ef9",
"totalKeys": 1158,
"translatedKeys": 1158,
"sourceHash": "b966847cbbaca64097b1e105dcc26ad803434cfb2d3b39cae3edc2be3874cfda",
"totalKeys": 1238,
"translatedKeys": 1238,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-05-28T18:48:18.990Z",
"generatedAt": "2026-05-28T23:48:56.314Z",
"locale": "pt-BR",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "7c867296be27a09ab0e35f76d2518f479e24ab667179c5b3fabf83d6c57f3ef9",
"totalKeys": 1158,
"translatedKeys": 1158,
"sourceHash": "b966847cbbaca64097b1e105dcc26ad803434cfb2d3b39cae3edc2be3874cfda",
"totalKeys": 1238,
"translatedKeys": 1238,
"workflow": 1
}

View File

@@ -4655,83 +4655,6 @@
"name": "aria-label",
"path": "ui/src/ui/views/usage-render-overview.ts",
"text": "Remove session filter"
},
{
"count": 1,
"kind": "html-attribute",
"name": "placeholder",
"path": "ui/src/ui/views/workboard.ts",
"text": "Card title"
},
{
"count": 1,
"kind": "html-attribute",
"name": "placeholder",
"path": "ui/src/ui/views/workboard.ts",
"text": "Notes, acceptance criteria, links"
},
{
"count": 1,
"kind": "html-attribute",
"name": "placeholder",
"path": "ui/src/ui/views/workboard.ts",
"text": "Search cards"
},
{
"count": 1,
"kind": "html-attribute",
"name": "title",
"path": "ui/src/ui/views/workboard.ts",
"text": "Delete card"
},
{
"count": 1,
"kind": "html-attribute",
"name": "title",
"path": "ui/src/ui/views/workboard.ts",
"text": "Open session"
},
{
"count": 1,
"kind": "html-attribute",
"name": "title",
"path": "ui/src/ui/views/workboard.ts",
"text": "Start session"
},
{
"count": 1,
"kind": "html-text",
"name": "text",
"path": "ui/src/ui/views/workboard.ts",
"text": "All priorities"
},
{
"count": 1,
"kind": "html-text",
"name": "text",
"path": "ui/src/ui/views/workboard.ts",
"text": "default agent"
},
{
"count": 1,
"kind": "html-text",
"name": "text",
"path": "ui/src/ui/views/workboard.ts",
"text": "Default agent"
},
{
"count": 1,
"kind": "html-text",
"name": "text",
"path": "ui/src/ui/views/workboard.ts",
"text": "Drop work here"
},
{
"count": 1,
"kind": "html-text",
"name": "text",
"path": "ui/src/ui/views/workboard.ts",
"text": "live"
}
]
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-05-28T18:48:45.871Z",
"generatedAt": "2026-05-28T23:50:26.593Z",
"locale": "th",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "7c867296be27a09ab0e35f76d2518f479e24ab667179c5b3fabf83d6c57f3ef9",
"totalKeys": 1158,
"translatedKeys": 1158,
"sourceHash": "b966847cbbaca64097b1e105dcc26ad803434cfb2d3b39cae3edc2be3874cfda",
"totalKeys": 1238,
"translatedKeys": 1238,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-05-28T18:48:35.897Z",
"generatedAt": "2026-05-28T23:49:47.972Z",
"locale": "tr",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "7c867296be27a09ab0e35f76d2518f479e24ab667179c5b3fabf83d6c57f3ef9",
"totalKeys": 1158,
"translatedKeys": 1158,
"sourceHash": "b966847cbbaca64097b1e105dcc26ad803434cfb2d3b39cae3edc2be3874cfda",
"totalKeys": 1238,
"translatedKeys": 1238,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-05-28T18:48:39.972Z",
"generatedAt": "2026-05-28T23:49:54.214Z",
"locale": "uk",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "7c867296be27a09ab0e35f76d2518f479e24ab667179c5b3fabf83d6c57f3ef9",
"totalKeys": 1158,
"translatedKeys": 1158,
"sourceHash": "b966847cbbaca64097b1e105dcc26ad803434cfb2d3b39cae3edc2be3874cfda",
"totalKeys": 1238,
"translatedKeys": 1238,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-05-28T18:48:47.804Z",
"generatedAt": "2026-05-28T23:50:37.343Z",
"locale": "vi",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "7c867296be27a09ab0e35f76d2518f479e24ab667179c5b3fabf83d6c57f3ef9",
"totalKeys": 1158,
"translatedKeys": 1158,
"sourceHash": "b966847cbbaca64097b1e105dcc26ad803434cfb2d3b39cae3edc2be3874cfda",
"totalKeys": 1238,
"translatedKeys": 1238,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-05-28T18:48:15.047Z",
"generatedAt": "2026-05-28T23:48:43.195Z",
"locale": "zh-CN",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "7c867296be27a09ab0e35f76d2518f479e24ab667179c5b3fabf83d6c57f3ef9",
"totalKeys": 1158,
"translatedKeys": 1158,
"sourceHash": "b966847cbbaca64097b1e105dcc26ad803434cfb2d3b39cae3edc2be3874cfda",
"totalKeys": 1238,
"translatedKeys": 1238,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-05-28T18:48:17.187Z",
"generatedAt": "2026-05-28T23:48:49.571Z",
"locale": "zh-TW",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "7c867296be27a09ab0e35f76d2518f479e24ab667179c5b3fabf83d6c57f3ef9",
"totalKeys": 1158,
"translatedKeys": 1158,
"sourceHash": "b966847cbbaca64097b1e105dcc26ad803434cfb2d3b39cae3edc2be3874cfda",
"totalKeys": 1238,
"translatedKeys": 1238,
"workflow": 1
}

View File

@@ -492,6 +492,18 @@ export const ar: TranslationMap = {
noLinkedSession: "لا توجد جلسة مرتبطة",
stopSession: "إيقاف الجلسة",
editCard: "تعديل البطاقة",
editCardHelp: "حدّث بيانات تعريف قائمة الانتظار وتسليم الجلسة.",
newCard: "بطاقة جديدة",
newCardHelp: "أضف العمل إلى قائمة الانتظار لجلسة وكيل.",
deleteCard: "حذف البطاقة",
openSession: "فتح الجلسة",
openLinkedSession: "فتح الجلسة المرتبطة",
defaultAgent: "الوكيل الافتراضي",
runEngine: "تشغيل {engine}",
openEngine: "فتح {engine}",
runDefaultAgent: "تشغيل الوكيل الافتراضي",
start: "بدء",
live: "مباشر",
fieldTitle: "العنوان",
fieldNotes: "ملاحظات",
fieldStatus: "الحالة",
@@ -499,7 +511,12 @@ export const ar: TranslationMap = {
fieldAgent: "الوكيل",
fieldSession: "الجلسة",
fieldLabels: "التصنيفات",
titlePlaceholder: "عنوان البطاقة",
notesPlaceholder: "ملاحظات، معايير القبول، روابط",
labelsPlaceholder: "ui, docs",
searchPlaceholder: "البحث في البطاقات",
allPriorities: "كل الأولويات",
emptyColumn: "أفلِت العمل هنا",
lifecycleUnlinked: "لا توجد جلسة",
lifecycleUnlinkedDetail: "ابدأ جلسة أو اربطها",
lifecycleMissing: "الجلسة مفقودة",
@@ -512,6 +529,13 @@ export const ar: TranslationMap = {
lifecycleDoneDetail: "تم النقل إلى المراجعة",
lifecycleNeedsReview: "تحتاج إلى مراجعة",
lifecycleNeedsReviewDetail: "توقف التشغيل أو فشل",
eventsLabel: "أحداث البطاقة",
eventCreated: "تم الإنشاء",
eventEdited: "تم التعديل",
eventMoved: "تم النقل",
eventMovedTo: "تم النقل إلى {status}",
eventLinked: "تم ربط الجلسة",
eventExecutionUpdated: "تم تحديث الوكيل",
gameButton: "لعبة مصغرة",
gameTitle: "مطاردة البطاقات",
gameStart: "صِل إلى مربع الإطلاق.",
@@ -1172,7 +1196,7 @@ export const ar: TranslationMap = {
},
queue: {
retry: "إعادة المحاولة",
retrySend: "إعادة الإرسال",
retrySend: "إعادة محاولة الإرسال",
retryQueuedMessage: "إعادة محاولة الرسالة في قائمة الانتظار",
},
composer: {

View File

@@ -496,6 +496,18 @@ export const de: TranslationMap = {
noLinkedSession: "Keine verknüpfte Sitzung",
stopSession: "Sitzung stoppen",
editCard: "Karte bearbeiten",
editCardHelp: "Warteschlangen-Metadaten und Sitzungsübergabe aktualisieren.",
newCard: "Neue Karte",
newCardHelp: "Arbeit für eine Agentensitzung in die Warteschlange einreihen.",
deleteCard: "Karte löschen",
openSession: "Sitzung öffnen",
openLinkedSession: "Verknüpfte Sitzung öffnen",
defaultAgent: "Standard-Agent",
runEngine: "{engine} ausführen",
openEngine: "{engine} öffnen",
runDefaultAgent: "Standard-Agent ausführen",
start: "Starten",
live: "live",
fieldTitle: "Titel",
fieldNotes: "Notizen",
fieldStatus: "Status",
@@ -503,7 +515,12 @@ export const de: TranslationMap = {
fieldAgent: "Agent",
fieldSession: "Sitzung",
fieldLabels: "Labels",
titlePlaceholder: "Kartentitel",
notesPlaceholder: "Notizen, Akzeptanzkriterien, Links",
labelsPlaceholder: "ui, docs",
searchPlaceholder: "Karten suchen",
allPriorities: "Alle Prioritäten",
emptyColumn: "Arbeit hier ablegen",
lifecycleUnlinked: "Keine Sitzung",
lifecycleUnlinkedDetail: "Sitzung starten oder verknüpfen",
lifecycleMissing: "Sitzung fehlt",
@@ -516,6 +533,13 @@ export const de: TranslationMap = {
lifecycleDoneDetail: "Zur Überprüfung verschoben",
lifecycleNeedsReview: "Überprüfung erforderlich",
lifecycleNeedsReviewDetail: "Lauf gestoppt oder fehlgeschlagen",
eventsLabel: "Kartenereignisse",
eventCreated: "Erstellt",
eventEdited: "Bearbeitet",
eventMoved: "Verschoben",
eventMovedTo: "Verschoben nach {status}",
eventLinked: "Sitzung verknüpft",
eventExecutionUpdated: "Agent aktualisiert",
gameButton: "Minispiel",
gameTitle: "Card Chase",
gameStart: "Erreiche das Startfeld.",
@@ -1195,9 +1219,9 @@ export const de: TranslationMap = {
sendMessage: "Send message",
},
queue: {
retry: "Erneut versuchen",
retrySend: "Senden erneut versuchen",
retryQueuedMessage: "Nachricht in der Warteschlange erneut versuchen",
retry: "Wiederholen",
retrySend: "Senden wiederholen",
retryQueuedMessage: "Nachricht in der Warteschlange erneut senden",
},
composer: {
placeholder: "Message {name} (Enter to send)",

View File

@@ -491,6 +491,18 @@ export const en: TranslationMap = {
noLinkedSession: "No linked session",
stopSession: "Stop session",
editCard: "Edit card",
editCardHelp: "Update queue metadata and session handoff.",
newCard: "New card",
newCardHelp: "Queue work for an agent session.",
deleteCard: "Delete card",
openSession: "Open session",
openLinkedSession: "Open linked session",
defaultAgent: "Default agent",
runEngine: "Run {engine}",
openEngine: "Open {engine}",
runDefaultAgent: "Run default agent",
start: "Start",
live: "live",
fieldTitle: "Title",
fieldNotes: "Notes",
fieldStatus: "Status",
@@ -498,7 +510,12 @@ export const en: TranslationMap = {
fieldAgent: "Agent",
fieldSession: "Session",
fieldLabels: "Labels",
titlePlaceholder: "Card title",
notesPlaceholder: "Notes, acceptance criteria, links",
labelsPlaceholder: "ui, docs",
searchPlaceholder: "Search cards",
allPriorities: "All priorities",
emptyColumn: "Drop work here",
lifecycleUnlinked: "No session",
lifecycleUnlinkedDetail: "Start or link a session",
lifecycleMissing: "Session missing",
@@ -511,6 +528,13 @@ export const en: TranslationMap = {
lifecycleDoneDetail: "Moved to review",
lifecycleNeedsReview: "Needs review",
lifecycleNeedsReviewDetail: "Run stopped or failed",
eventsLabel: "Card events",
eventCreated: "Created",
eventEdited: "Edited",
eventMoved: "Moved",
eventMovedTo: "Moved to {status}",
eventLinked: "Linked session",
eventExecutionUpdated: "Agent updated",
gameButton: "Mini game",
gameTitle: "Card Chase",
gameStart: "Reach the launch tile.",

View File

@@ -493,6 +493,18 @@ export const es: TranslationMap = {
noLinkedSession: "Sin sesión vinculada",
stopSession: "Detener sesión",
editCard: "Editar tarjeta",
editCardHelp: "Actualiza los metadatos de la cola y la transferencia de sesión.",
newCard: "Nueva tarjeta",
newCardHelp: "Pon trabajo en cola para una sesión de agente.",
deleteCard: "Eliminar tarjeta",
openSession: "Abrir sesión",
openLinkedSession: "Abrir sesión vinculada",
defaultAgent: "Agente predeterminado",
runEngine: "Ejecutar {engine}",
openEngine: "Abrir {engine}",
runDefaultAgent: "Ejecutar agente predeterminado",
start: "Iniciar",
live: "en vivo",
fieldTitle: "Título",
fieldNotes: "Notas",
fieldStatus: "Estado",
@@ -500,7 +512,12 @@ export const es: TranslationMap = {
fieldAgent: "Agente",
fieldSession: "Sesión",
fieldLabels: "Etiquetas",
titlePlaceholder: "Título de la tarjeta",
notesPlaceholder: "Notas, criterios de aceptación, enlaces",
labelsPlaceholder: "ui, docs",
searchPlaceholder: "Buscar tarjetas",
allPriorities: "Todas las prioridades",
emptyColumn: "Suelta el trabajo aquí",
lifecycleUnlinked: "Sin sesión",
lifecycleUnlinkedDetail: "Inicia o vincula una sesión",
lifecycleMissing: "Falta la sesión",
@@ -513,6 +530,13 @@ export const es: TranslationMap = {
lifecycleDoneDetail: "Movido a revisión",
lifecycleNeedsReview: "Necesita revisión",
lifecycleNeedsReviewDetail: "La ejecución se detuvo o falló",
eventsLabel: "Eventos de la tarjeta",
eventCreated: "Creada",
eventEdited: "Editada",
eventMoved: "Movido",
eventMovedTo: "Movido a {status}",
eventLinked: "Sesión vinculada",
eventExecutionUpdated: "Agente actualizado",
gameButton: "Minijuego",
gameTitle: "Card Chase",
gameStart: "Alcanza la casilla de lanzamiento.",

View File

@@ -493,6 +493,32 @@ export const fa: TranslationMap = {
},
noLinkedSession: "جلسه‌ای پیوند نشده است",
stopSession: "توقف جلسه",
editCard: "ویرایش کارت",
editCardHelp: "به‌روزرسانی فرادادهٔ صف و واگذاری نشست.",
newCard: "کارت جدید",
newCardHelp: "کار را برای یک نشست عامل در صف قرار دهید.",
deleteCard: "حذف کارت",
openSession: "باز کردن نشست",
openLinkedSession: "باز کردن نشست پیوندشده",
defaultAgent: "عامل پیش‌فرض",
runEngine: "اجرای {engine}",
openEngine: "باز کردن {engine}",
runDefaultAgent: "اجرای عامل پیش‌فرض",
start: "شروع",
live: "زنده",
fieldTitle: "عنوان",
fieldNotes: "یادداشت‌ها",
fieldStatus: "وضعیت",
fieldPriority: "اولویت",
fieldAgent: "عامل",
fieldSession: "نشست",
fieldLabels: "برچسب‌ها",
titlePlaceholder: "عنوان کارت",
notesPlaceholder: "یادداشت‌ها، معیارهای پذیرش، پیوندها",
labelsPlaceholder: "ui, docs",
searchPlaceholder: "جستجوی کارت‌ها",
allPriorities: "همه اولویت‌ها",
emptyColumn: "کار را اینجا رها کنید",
lifecycleUnlinked: "بدون جلسه",
lifecycleUnlinkedDetail: "یک جلسه را شروع یا پیوند کنید",
lifecycleMissing: "جلسه پیدا نشد",
@@ -505,6 +531,32 @@ export const fa: TranslationMap = {
lifecycleDoneDetail: "به بازبینی منتقل شد",
lifecycleNeedsReview: "نیازمند بازبینی",
lifecycleNeedsReviewDetail: "اجرا متوقف شد یا ناموفق بود",
eventsLabel: "رویدادهای کارت",
eventCreated: "ایجاد شد",
eventEdited: "ویرایش شد",
eventMoved: "منتقل شد",
eventMovedTo: "به {status} منتقل شد",
eventLinked: "نشست پیوند داده شد",
eventExecutionUpdated: "Agent به‌روزرسانی شد",
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: {
@@ -1160,9 +1212,9 @@ export const fa: TranslationMap = {
sendMessage: "Send message",
},
queue: {
retry: "تلاش مجدد",
retrySend: "ارسال مجدد",
retryQueuedMessage: "تلاش مجدد برای پیام در صف",
retry: "تلاش دوباره",
retrySend: "تلاش دوباره برای ارسال",
retryQueuedMessage: "تلاش دوباره برای پیام در صف",
},
composer: {
placeholder: "Message {name} (Enter to send)",

View File

@@ -495,6 +495,18 @@ export const fr: TranslationMap = {
noLinkedSession: "Aucune session liée",
stopSession: "Arrêter la session",
editCard: "Modifier la carte",
editCardHelp: "Mettez à jour les métadonnées de la file dattente et le transfert de session.",
newCard: "Nouvelle carte",
newCardHelp: "Mettez du travail en file dattente pour une session dagent.",
deleteCard: "Supprimer la carte",
openSession: "Ouvrir la session",
openLinkedSession: "Ouvrir la session liée",
defaultAgent: "Agent par défaut",
runEngine: "Exécuter {engine}",
openEngine: "Ouvrir {engine}",
runDefaultAgent: "Exécuter lagent par défaut",
start: "Démarrer",
live: "en direct",
fieldTitle: "Titre",
fieldNotes: "Notes",
fieldStatus: "Statut",
@@ -502,7 +514,12 @@ export const fr: TranslationMap = {
fieldAgent: "Agent",
fieldSession: "Session",
fieldLabels: "Étiquettes",
titlePlaceholder: "Titre de la carte",
notesPlaceholder: "Notes, critères dacceptation, liens",
labelsPlaceholder: "ui, docs",
searchPlaceholder: "Rechercher des cartes",
allPriorities: "Toutes les priorités",
emptyColumn: "Déposez le travail ici",
lifecycleUnlinked: "Aucune session",
lifecycleUnlinkedDetail: "Démarrer ou lier une session",
lifecycleMissing: "Session manquante",
@@ -515,6 +532,13 @@ 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",
eventsLabel: "Événements de la carte",
eventCreated: "Créé",
eventEdited: "Modifié",
eventMoved: "Déplacé",
eventMovedTo: "Déplacé vers {status}",
eventLinked: "Session liée",
eventExecutionUpdated: "Agent mis à jour",
gameButton: "Mini-jeu",
gameTitle: "Card Chase",
gameStart: "Atteignez la tuile de lancement.",

View File

@@ -493,6 +493,18 @@ export const id: TranslationMap = {
noLinkedSession: "Tidak ada sesi tertaut",
stopSession: "Hentikan sesi",
editCard: "Edit kartu",
editCardHelp: "Perbarui metadata antrean dan serah terima sesi.",
newCard: "Kartu baru",
newCardHelp: "Antrekan pekerjaan untuk sesi agen.",
deleteCard: "Hapus kartu",
openSession: "Buka sesi",
openLinkedSession: "Buka sesi tertaut",
defaultAgent: "Agen default",
runEngine: "Jalankan {engine}",
openEngine: "Buka {engine}",
runDefaultAgent: "Jalankan agen default",
start: "Mulai",
live: "langsung",
fieldTitle: "Judul",
fieldNotes: "Catatan",
fieldStatus: "Status",
@@ -500,7 +512,12 @@ export const id: TranslationMap = {
fieldAgent: "Agen",
fieldSession: "Sesi",
fieldLabels: "Label",
titlePlaceholder: "Judul kartu",
notesPlaceholder: "Catatan, kriteria penerimaan, tautan",
labelsPlaceholder: "ui, docs",
searchPlaceholder: "Cari kartu",
allPriorities: "Semua prioritas",
emptyColumn: "Letakkan pekerjaan di sini",
lifecycleUnlinked: "Tidak ada sesi",
lifecycleUnlinkedDetail: "Mulai atau tautkan sesi",
lifecycleMissing: "Sesi hilang",
@@ -513,6 +530,13 @@ export const id: TranslationMap = {
lifecycleDoneDetail: "Dipindahkan ke peninjauan",
lifecycleNeedsReview: "Perlu ditinjau",
lifecycleNeedsReviewDetail: "Run dihentikan atau gagal",
eventsLabel: "Peristiwa kartu",
eventCreated: "Dibuat",
eventEdited: "Diedit",
eventMoved: "Dipindahkan",
eventMovedTo: "Dipindahkan ke {status}",
eventLinked: "Sesi ditautkan",
eventExecutionUpdated: "Agen diperbarui",
gameButton: "Mini game",
gameTitle: "Card Chase",
gameStart: "Capai petak peluncuran.",

View File

@@ -495,6 +495,18 @@ export const it: TranslationMap = {
noLinkedSession: "Nessuna sessione collegata",
stopSession: "Interrompi sessione",
editCard: "Modifica scheda",
editCardHelp: "Aggiorna i metadati della coda e il passaggio di sessione.",
newCard: "Nuova scheda",
newCardHelp: "Metti in coda il lavoro per una sessione dell'agente.",
deleteCard: "Elimina scheda",
openSession: "Apri sessione",
openLinkedSession: "Apri sessione collegata",
defaultAgent: "Agente predefinito",
runEngine: "Esegui {engine}",
openEngine: "Apri {engine}",
runDefaultAgent: "Esegui agente predefinito",
start: "Avvia",
live: "live",
fieldTitle: "Titolo",
fieldNotes: "Note",
fieldStatus: "Stato",
@@ -502,7 +514,12 @@ export const it: TranslationMap = {
fieldAgent: "Agente",
fieldSession: "Sessione",
fieldLabels: "Etichette",
titlePlaceholder: "Titolo scheda",
notesPlaceholder: "Note, criteri di accettazione, link",
labelsPlaceholder: "ui, docs",
searchPlaceholder: "Cerca schede",
allPriorities: "Tutte le priorità",
emptyColumn: "Rilascia qui il lavoro",
lifecycleUnlinked: "Nessuna sessione",
lifecycleUnlinkedDetail: "Avvia o collega una sessione",
lifecycleMissing: "Sessione mancante",
@@ -515,6 +532,13 @@ export const it: TranslationMap = {
lifecycleDoneDetail: "Spostata in revisione",
lifecycleNeedsReview: "Richiede revisione",
lifecycleNeedsReviewDetail: "Esecuzione interrotta o non riuscita",
eventsLabel: "Eventi della scheda",
eventCreated: "Creato",
eventEdited: "Modificato",
eventMoved: "Spostato",
eventMovedTo: "Spostato in {status}",
eventLinked: "Sessione collegata",
eventExecutionUpdated: "Agente aggiornato",
gameButton: "Mini gioco",
gameTitle: "Card Chase",
gameStart: "Raggiungi la casella di lancio.",

View File

@@ -496,6 +496,18 @@ export const ja_JP: TranslationMap = {
noLinkedSession: "リンクされたセッションがありません",
stopSession: "セッションを停止",
editCard: "カードを編集",
editCardHelp: "キューのメタデータとセッションの引き継ぎを更新します。",
newCard: "新規カード",
newCardHelp: "エージェントセッションの作業をキューに追加します。",
deleteCard: "カードを削除",
openSession: "セッションを開く",
openLinkedSession: "リンクされたセッションを開く",
defaultAgent: "デフォルトエージェント",
runEngine: "{engine} を実行",
openEngine: "{engine} を開く",
runDefaultAgent: "デフォルトエージェントを実行",
start: "開始",
live: "ライブ",
fieldTitle: "タイトル",
fieldNotes: "メモ",
fieldStatus: "ステータス",
@@ -503,7 +515,12 @@ export const ja_JP: TranslationMap = {
fieldAgent: "エージェント",
fieldSession: "セッション",
fieldLabels: "ラベル",
titlePlaceholder: "カードのタイトル",
notesPlaceholder: "メモ、受け入れ条件、リンク",
labelsPlaceholder: "ui, docs",
searchPlaceholder: "カードを検索",
allPriorities: "すべての優先度",
emptyColumn: "ここに作業をドロップ",
lifecycleUnlinked: "セッションなし",
lifecycleUnlinkedDetail: "セッションを開始またはリンク",
lifecycleMissing: "セッションが見つかりません",
@@ -516,6 +533,13 @@ export const ja_JP: TranslationMap = {
lifecycleDoneDetail: "レビューに移動しました",
lifecycleNeedsReview: "レビューが必要",
lifecycleNeedsReviewDetail: "実行が停止または失敗しました",
eventsLabel: "カードイベント",
eventCreated: "作成済み",
eventEdited: "編集済み",
eventMoved: "移動しました",
eventMovedTo: "{status} に移動しました",
eventLinked: "セッションをリンクしました",
eventExecutionUpdated: "Agent が更新されました",
gameButton: "ミニゲーム",
gameTitle: "Card Chase",
gameStart: "ローンチタイルに到達してください。",

View File

@@ -492,6 +492,18 @@ export const ko: TranslationMap = {
noLinkedSession: "연결된 세션 없음",
stopSession: "세션 중지",
editCard: "카드 편집",
editCardHelp: "대기열 메타데이터와 세션 인계를 업데이트합니다.",
newCard: "새 카드",
newCardHelp: "에이전트 세션을 위한 작업을 대기열에 추가합니다.",
deleteCard: "카드 삭제",
openSession: "세션 열기",
openLinkedSession: "연결된 세션 열기",
defaultAgent: "기본 에이전트",
runEngine: "{engine} 실행",
openEngine: "{engine} 열기",
runDefaultAgent: "기본 에이전트 실행",
start: "시작",
live: "라이브",
fieldTitle: "제목",
fieldNotes: "메모",
fieldStatus: "상태",
@@ -499,7 +511,12 @@ export const ko: TranslationMap = {
fieldAgent: "에이전트",
fieldSession: "세션",
fieldLabels: "레이블",
titlePlaceholder: "카드 제목",
notesPlaceholder: "메모, 승인 기준, 링크",
labelsPlaceholder: "ui, docs",
searchPlaceholder: "카드 검색",
allPriorities: "모든 우선순위",
emptyColumn: "여기에 작업을 놓으세요",
lifecycleUnlinked: "세션 없음",
lifecycleUnlinkedDetail: "세션을 시작하거나 연결하세요",
lifecycleMissing: "세션 누락",
@@ -512,6 +529,13 @@ export const ko: TranslationMap = {
lifecycleDoneDetail: "검토로 이동됨",
lifecycleNeedsReview: "검토 필요",
lifecycleNeedsReviewDetail: "실행이 중지되었거나 실패했습니다",
eventsLabel: "카드 이벤트",
eventCreated: "생성됨",
eventEdited: "편집됨",
eventMoved: "이동됨",
eventMovedTo: "{status}(으)로 이동됨",
eventLinked: "연결된 세션",
eventExecutionUpdated: "Agent 업데이트됨",
gameButton: "미니 게임",
gameTitle: "Card Chase",
gameStart: "출발 타일에 도달하세요.",
@@ -1180,7 +1204,7 @@ export const ko: TranslationMap = {
},
queue: {
retry: "다시 시도",
retrySend: "전송 다시 시도",
retrySend: "보내기 다시 시도",
retryQueuedMessage: "대기 중인 메시지 다시 시도",
},
composer: {

View File

@@ -494,6 +494,32 @@ export const nl: TranslationMap = {
},
noLinkedSession: "Geen gekoppelde sessie",
stopSession: "Sessie stoppen",
editCard: "Kaart bewerken",
editCardHelp: "Werk wachtrijmetadata en sessieoverdracht bij.",
newCard: "Nieuwe kaart",
newCardHelp: "Zet werk in de wachtrij voor een agentsessie.",
deleteCard: "Kaart verwijderen",
openSession: "Sessie openen",
openLinkedSession: "Gekoppelde sessie openen",
defaultAgent: "Standaardagent",
runEngine: "{engine} uitvoeren",
openEngine: "{engine} openen",
runDefaultAgent: "Standaardagent uitvoeren",
start: "Starten",
live: "live",
fieldTitle: "Titel",
fieldNotes: "Notities",
fieldStatus: "Status",
fieldPriority: "Prioriteit",
fieldAgent: "Agent",
fieldSession: "Sessie",
fieldLabels: "Labels",
titlePlaceholder: "Kaarttitel",
notesPlaceholder: "Notities, acceptatiecriteria, links",
labelsPlaceholder: "ui, docs",
searchPlaceholder: "Kaarten zoeken",
allPriorities: "Alle prioriteiten",
emptyColumn: "Sleep werk hierheen",
lifecycleUnlinked: "Geen sessie",
lifecycleUnlinkedDetail: "Start of koppel een sessie",
lifecycleMissing: "Sessie ontbreekt",
@@ -506,6 +532,32 @@ export const nl: TranslationMap = {
lifecycleDoneDetail: "Verplaatst naar review",
lifecycleNeedsReview: "Review nodig",
lifecycleNeedsReviewDetail: "Run gestopt of mislukt",
eventsLabel: "Kaartgebeurtenissen",
eventCreated: "Gemaakt",
eventEdited: "Bewerkt",
eventMoved: "Verplaatst",
eventMovedTo: "Verplaatst naar {status}",
eventLinked: "Gekoppelde sessie",
eventExecutionUpdated: "Agent bijgewerkt",
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: {

View File

@@ -493,6 +493,32 @@ export const pl: TranslationMap = {
},
noLinkedSession: "Brak połączonej sesji",
stopSession: "Zatrzymaj sesję",
editCard: "Edytuj kartę",
editCardHelp: "Zaktualizuj metadane kolejki i przekazanie sesji.",
newCard: "Nowa karta",
newCardHelp: "Dodaj zadanie do kolejki dla sesji agenta.",
deleteCard: "Usuń kartę",
openSession: "Otwórz sesję",
openLinkedSession: "Otwórz powiązaną sesję",
defaultAgent: "Domyślny agent",
runEngine: "Uruchom {engine}",
openEngine: "Otwórz {engine}",
runDefaultAgent: "Uruchom domyślnego agenta",
start: "Rozpocznij",
live: "na żywo",
fieldTitle: "Tytuł",
fieldNotes: "Notatki",
fieldStatus: "Status",
fieldPriority: "Priorytet",
fieldAgent: "Agent",
fieldSession: "Sesja",
fieldLabels: "Etykiety",
titlePlaceholder: "Tytuł karty",
notesPlaceholder: "Notatki, kryteria akceptacji, linki",
labelsPlaceholder: "ui, docs",
searchPlaceholder: "Szukaj kart",
allPriorities: "Wszystkie priorytety",
emptyColumn: "Upuść pracę tutaj",
lifecycleUnlinked: "Brak sesji",
lifecycleUnlinkedDetail: "Rozpocznij lub połącz sesję",
lifecycleMissing: "Brak sesji",
@@ -505,6 +531,32 @@ export const pl: TranslationMap = {
lifecycleDoneDetail: "Przeniesiono do przeglądu",
lifecycleNeedsReview: "Wymaga przeglądu",
lifecycleNeedsReviewDetail: "Uruchomienie zatrzymane lub nieudane",
eventsLabel: "Zdarzenia karty",
eventCreated: "Utworzono",
eventEdited: "Edytowano",
eventMoved: "Przeniesiono",
eventMovedTo: "Przeniesiono do {status}",
eventLinked: "Połączona sesja",
eventExecutionUpdated: "Agent zaktualizowany",
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: {
@@ -1164,8 +1216,8 @@ export const pl: TranslationMap = {
},
queue: {
retry: "Ponów",
retrySend: "Ponów wysyłanie",
retryQueuedMessage: "Ponów wysłanie wiadomości w kolejce",
retrySend: "Ponów wysłanie",
retryQueuedMessage: "Ponów wiadomość w kolejce",
},
composer: {
placeholder: "Message {name} (Enter to send)",

View File

@@ -493,6 +493,18 @@ export const pt_BR: TranslationMap = {
noLinkedSession: "Nenhuma sessão vinculada",
stopSession: "Parar sessão",
editCard: "Editar cartão",
editCardHelp: "Atualize os metadados da fila e a transferência de sessão.",
newCard: "Novo cartão",
newCardHelp: "Enfileire trabalho para uma sessão de agente.",
deleteCard: "Excluir cartão",
openSession: "Abrir sessão",
openLinkedSession: "Abrir sessão vinculada",
defaultAgent: "Agente padrão",
runEngine: "Executar {engine}",
openEngine: "Abrir {engine}",
runDefaultAgent: "Executar agente padrão",
start: "Iniciar",
live: "ao vivo",
fieldTitle: "Título",
fieldNotes: "Notas",
fieldStatus: "Status",
@@ -500,7 +512,12 @@ export const pt_BR: TranslationMap = {
fieldAgent: "Agente",
fieldSession: "Sessão",
fieldLabels: "Etiquetas",
titlePlaceholder: "Título do cartão",
notesPlaceholder: "Notas, critérios de aceitação, links",
labelsPlaceholder: "ui, docs",
searchPlaceholder: "Pesquisar cartões",
allPriorities: "Todas as prioridades",
emptyColumn: "Solte o trabalho aqui",
lifecycleUnlinked: "Nenhuma sessão",
lifecycleUnlinkedDetail: "Inicie ou vincule uma sessão",
lifecycleMissing: "Sessão ausente",
@@ -513,6 +530,13 @@ export const pt_BR: TranslationMap = {
lifecycleDoneDetail: "Movido para revisão",
lifecycleNeedsReview: "Precisa de revisão",
lifecycleNeedsReviewDetail: "A execução foi interrompida ou falhou",
eventsLabel: "Eventos do cartão",
eventCreated: "Criado",
eventEdited: "Editado",
eventMoved: "Movido",
eventMovedTo: "Movido para {status}",
eventLinked: "Sessão vinculada",
eventExecutionUpdated: "Agente atualizado",
gameButton: "Mini game",
gameTitle: "Card Chase",
gameStart: "Alcance o bloco de lançamento.",
@@ -1189,7 +1213,7 @@ export const pt_BR: TranslationMap = {
queue: {
retry: "Tentar novamente",
retrySend: "Tentar enviar novamente",
retryQueuedMessage: "Tentar novamente a mensagem na fila",
retryQueuedMessage: "Tentar novamente mensagem na fila",
},
composer: {
placeholder: "Message {name} (Enter to send)",

View File

@@ -490,6 +490,32 @@ export const th: TranslationMap = {
},
noLinkedSession: "ไม่มีเซสชันที่เชื่อมโยง",
stopSession: "หยุดเซสชัน",
editCard: "แก้ไขการ์ด",
editCardHelp: "อัปเดตข้อมูลเมตาของคิวและการส่งต่อเซสชัน",
newCard: "การ์ดใหม่",
newCardHelp: "จัดคิวงานสำหรับเซสชันของเอเจนต์",
deleteCard: "ลบการ์ด",
openSession: "เปิดเซสชัน",
openLinkedSession: "เปิดเซสชันที่ลิงก์ไว้",
defaultAgent: "เอเจนต์เริ่มต้น",
runEngine: "เรียกใช้ {engine}",
openEngine: "เปิด {engine}",
runDefaultAgent: "เรียกใช้เอเจนต์เริ่มต้น",
start: "เริ่ม",
live: "สด",
fieldTitle: "ชื่อเรื่อง",
fieldNotes: "บันทึก",
fieldStatus: "สถานะ",
fieldPriority: "ลำดับความสำคัญ",
fieldAgent: "เอเจนต์",
fieldSession: "เซสชัน",
fieldLabels: "ป้ายกำกับ",
titlePlaceholder: "ชื่อการ์ด",
notesPlaceholder: "บันทึก, เกณฑ์การยอมรับ, ลิงก์",
labelsPlaceholder: "ui, docs",
searchPlaceholder: "ค้นหาการ์ด",
allPriorities: "ทุกลำดับความสำคัญ",
emptyColumn: "วางงานที่นี่",
lifecycleUnlinked: "ไม่มีเซสชัน",
lifecycleUnlinkedDetail: "เริ่มหรือเชื่อมโยงเซสชัน",
lifecycleMissing: "ไม่พบเซสชัน",
@@ -502,6 +528,32 @@ export const th: TranslationMap = {
lifecycleDoneDetail: "ย้ายไปยังการตรวจทานแล้ว",
lifecycleNeedsReview: "ต้องตรวจทาน",
lifecycleNeedsReviewDetail: "การรันหยุดหรือไม่สำเร็จ",
eventsLabel: "เหตุการณ์ของการ์ด",
eventCreated: "สร้างแล้ว",
eventEdited: "แก้ไขแล้ว",
eventMoved: "ย้ายแล้ว",
eventMovedTo: "ย้ายไปยัง {status}",
eventLinked: "เซสชันที่ลิงก์แล้ว",
eventExecutionUpdated: "เอเจนต์อัปเดตแล้ว",
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: {

View File

@@ -495,6 +495,18 @@ export const tr: TranslationMap = {
noLinkedSession: "Bağlı oturum yok",
stopSession: "Oturumu durdur",
editCard: "Kartı düzenle",
editCardHelp: "Kuyruk meta verilerini ve oturum devrini güncelleyin.",
newCard: "Yeni kart",
newCardHelp: "Bir ajan oturumu için işi kuyruğa alın.",
deleteCard: "Kartı sil",
openSession: "Oturumu aç",
openLinkedSession: "Bağlantılı oturumu aç",
defaultAgent: "Varsayılan ajan",
runEngine: "{engine} çalıştır",
openEngine: "{engine} aç",
runDefaultAgent: "Varsayılan ajanı çalıştır",
start: "Başlat",
live: "canlı",
fieldTitle: "Başlık",
fieldNotes: "Notlar",
fieldStatus: "Durum",
@@ -502,7 +514,12 @@ export const tr: TranslationMap = {
fieldAgent: "Aracı",
fieldSession: "Oturum",
fieldLabels: "Etiketler",
titlePlaceholder: "Kart başlığı",
notesPlaceholder: "Notlar, kabul kriterleri, bağlantılar",
labelsPlaceholder: "ui, docs",
searchPlaceholder: "Kartlarda ara",
allPriorities: "Tüm öncelikler",
emptyColumn: "İşi buraya bırakın",
lifecycleUnlinked: "Oturum yok",
lifecycleUnlinkedDetail: "Bir oturum başlatın veya bağlayın",
lifecycleMissing: "Oturum eksik",
@@ -515,6 +532,13 @@ export const tr: TranslationMap = {
lifecycleDoneDetail: "İncelemeye taşındı",
lifecycleNeedsReview: "İnceleme gerekli",
lifecycleNeedsReviewDetail: "Çalışma durduruldu veya başarısız oldu",
eventsLabel: "Kart olayları",
eventCreated: "Oluşturuldu",
eventEdited: "Düzenlendi",
eventMoved: "Taşındı",
eventMovedTo: "{status} durumuna taşındı",
eventLinked: "Oturum bağlandı",
eventExecutionUpdated: "Aracı güncellendi",
gameButton: "Mini oyun",
gameTitle: "Kart Takibi",
gameStart: "Başlatma karesine ulaşın.",
@@ -1194,7 +1218,7 @@ export const tr: TranslationMap = {
queue: {
retry: "Yeniden dene",
retrySend: "Göndermeyi yeniden dene",
retryQueuedMessage: "Kuyruktaki mesajı yeniden dene",
retryQueuedMessage: "Kuyruğa alınan iletiyi yeniden dene",
},
composer: {
placeholder: "Message {name} (Enter to send)",

View File

@@ -494,6 +494,18 @@ export const uk: TranslationMap = {
noLinkedSession: "Немає пов’язаної сесії",
stopSession: "Зупинити сесію",
editCard: "Редагувати картку",
editCardHelp: "Оновіть метадані черги та передавання сесії.",
newCard: "Нова картка",
newCardHelp: "Поставте роботу в чергу для сесії агента.",
deleteCard: "Видалити картку",
openSession: "Відкрити сесію",
openLinkedSession: "Відкрити пов’язану сесію",
defaultAgent: "Агент за замовчуванням",
runEngine: "Запустити {engine}",
openEngine: "Відкрити {engine}",
runDefaultAgent: "Запустити агента за замовчуванням",
start: "Почати",
live: "наживо",
fieldTitle: "Заголовок",
fieldNotes: "Нотатки",
fieldStatus: "Статус",
@@ -501,7 +513,12 @@ export const uk: TranslationMap = {
fieldAgent: "Агент",
fieldSession: "Сеанс",
fieldLabels: "Мітки",
titlePlaceholder: "Заголовок картки",
notesPlaceholder: "Нотатки, критерії приймання, посилання",
labelsPlaceholder: "ui, docs",
searchPlaceholder: "Шукати картки",
allPriorities: "Усі пріоритети",
emptyColumn: "Перетягніть роботу сюди",
lifecycleUnlinked: "Немає сесії",
lifecycleUnlinkedDetail: "Запустіть або пов’яжіть сесію",
lifecycleMissing: "Сесію не знайдено",
@@ -514,6 +531,13 @@ export const uk: TranslationMap = {
lifecycleDoneDetail: "Переміщено на перевірку",
lifecycleNeedsReview: "Потребує перевірки",
lifecycleNeedsReviewDetail: "Запуск зупинено або він завершився помилкою",
eventsLabel: "Події картки",
eventCreated: "Створено",
eventEdited: "Змінено",
eventMoved: "Переміщено",
eventMovedTo: "Переміщено до {status}",
eventLinked: "Пов’язаний сеанс",
eventExecutionUpdated: "Агент оновлено",
gameButton: "Мінігра",
gameTitle: "Card Chase",
gameStart: "Дістаньтеся клітинки запуску.",
@@ -1191,7 +1215,7 @@ export const uk: TranslationMap = {
queue: {
retry: "Повторити",
retrySend: "Повторити надсилання",
retryQueuedMessage: "Повторити надсилання повідомлення в черзі",
retryQueuedMessage: "Повторити повідомлення в черзі",
},
composer: {
placeholder: "Message {name} (Enter to send)",

View File

@@ -492,6 +492,32 @@ export const vi: TranslationMap = {
},
noLinkedSession: "Không có phiên được liên kết",
stopSession: "Dừng phiên",
editCard: "Chỉnh sửa thẻ",
editCardHelp: "Cập nhật siêu dữ liệu hàng đợi và bàn giao phiên.",
newCard: "Thẻ mới",
newCardHelp: "Đưa công việc vào hàng đợi cho một phiên agent.",
deleteCard: "Xóa thẻ",
openSession: "Mở phiên",
openLinkedSession: "Mở phiên được liên kết",
defaultAgent: "Agent mặc định",
runEngine: "Chạy {engine}",
openEngine: "Mở {engine}",
runDefaultAgent: "Chạy agent mặc định",
start: "Bắt đầu",
live: "trực tiếp",
fieldTitle: "Tiêu đề",
fieldNotes: "Ghi chú",
fieldStatus: "Trạng thái",
fieldPriority: "Mức ưu tiên",
fieldAgent: "Agent",
fieldSession: "Phiên",
fieldLabels: "Nhãn",
titlePlaceholder: "Tiêu đề thẻ",
notesPlaceholder: "Ghi chú, tiêu chí chấp nhận, liên kết",
labelsPlaceholder: "ui, docs",
searchPlaceholder: "Tìm kiếm thẻ",
allPriorities: "Tất cả mức ưu tiên",
emptyColumn: "Thả công việc vào đây",
lifecycleUnlinked: "Không có phiên",
lifecycleUnlinkedDetail: "Bắt đầu hoặc liên kết một phiên",
lifecycleMissing: "Thiếu phiên",
@@ -504,6 +530,32 @@ 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",
eventsLabel: "Sự kiện thẻ",
eventCreated: "Đã tạo",
eventEdited: "Đã chỉnh sửa",
eventMoved: "Đã di chuyển",
eventMovedTo: "Đã di chuyển đến {status}",
eventLinked: "Phiên đã liên kết",
eventExecutionUpdated: "Agent đã cập nhật",
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: {

View File

@@ -490,6 +490,18 @@ export const zh_CN: TranslationMap = {
noLinkedSession: "没有已关联的会话",
stopSession: "停止会话",
editCard: "编辑卡片",
editCardHelp: "更新队列元数据和会话交接。",
newCard: "新建卡片",
newCardHelp: "为代理会话排队工作。",
deleteCard: "删除卡片",
openSession: "打开会话",
openLinkedSession: "打开关联会话",
defaultAgent: "默认代理",
runEngine: "运行 {engine}",
openEngine: "打开 {engine}",
runDefaultAgent: "运行默认代理",
start: "开始",
live: "实时",
fieldTitle: "标题",
fieldNotes: "备注",
fieldStatus: "状态",
@@ -497,7 +509,12 @@ export const zh_CN: TranslationMap = {
fieldAgent: "代理",
fieldSession: "会话",
fieldLabels: "标签",
titlePlaceholder: "卡片标题",
notesPlaceholder: "备注、验收标准、链接",
labelsPlaceholder: "ui, docs",
searchPlaceholder: "搜索卡片",
allPriorities: "所有优先级",
emptyColumn: "将工作拖放到此处",
lifecycleUnlinked: "无会话",
lifecycleUnlinkedDetail: "启动或关联会话",
lifecycleMissing: "会话缺失",
@@ -510,6 +527,13 @@ export const zh_CN: TranslationMap = {
lifecycleDoneDetail: "已移至审核",
lifecycleNeedsReview: "需要审核",
lifecycleNeedsReviewDetail: "运行已停止或失败",
eventsLabel: "卡片事件",
eventCreated: "已创建",
eventEdited: "已编辑",
eventMoved: "已移动",
eventMovedTo: "已移至 {status}",
eventLinked: "已关联会话",
eventExecutionUpdated: "Agent 已更新",
gameButton: "迷你游戏",
gameTitle: "卡片追逐",
gameStart: "到达发布图块。",
@@ -1153,7 +1177,7 @@ export const zh_CN: TranslationMap = {
queue: {
retry: "重试",
retrySend: "重试发送",
retryQueuedMessage: "重试队列中的消息",
retryQueuedMessage: "重试队消息",
},
composer: {
placeholder: "给 {name} 发消息Enter 发送)",

View File

@@ -490,6 +490,18 @@ export const zh_TW: TranslationMap = {
noLinkedSession: "沒有已連結的工作階段",
stopSession: "停止工作階段",
editCard: "編輯卡片",
editCardHelp: "更新佇列中繼資料與工作階段交接。",
newCard: "新增卡片",
newCardHelp: "為代理程式工作階段排入工作。",
deleteCard: "刪除卡片",
openSession: "開啟工作階段",
openLinkedSession: "開啟連結的工作階段",
defaultAgent: "預設代理程式",
runEngine: "執行 {engine}",
openEngine: "開啟 {engine}",
runDefaultAgent: "執行預設代理程式",
start: "開始",
live: "即時",
fieldTitle: "標題",
fieldNotes: "備註",
fieldStatus: "狀態",
@@ -497,7 +509,12 @@ export const zh_TW: TranslationMap = {
fieldAgent: "代理",
fieldSession: "工作階段",
fieldLabels: "標籤",
titlePlaceholder: "卡片標題",
notesPlaceholder: "備註、驗收標準、連結",
labelsPlaceholder: "ui, docs",
searchPlaceholder: "搜尋卡片",
allPriorities: "所有優先順序",
emptyColumn: "將工作拖放到這裡",
lifecycleUnlinked: "沒有工作階段",
lifecycleUnlinkedDetail: "開始或連結工作階段",
lifecycleMissing: "找不到工作階段",
@@ -510,6 +527,13 @@ export const zh_TW: TranslationMap = {
lifecycleDoneDetail: "已移至審查",
lifecycleNeedsReview: "需要審查",
lifecycleNeedsReviewDetail: "執行已停止或失敗",
eventsLabel: "卡片事件",
eventCreated: "已建立",
eventEdited: "已編輯",
eventMoved: "已移動",
eventMovedTo: "已移動至 {status}",
eventLinked: "已連結工作階段",
eventExecutionUpdated: "代理程式已更新",
gameButton: "小遊戲",
gameTitle: "卡片追逐",
gameStart: "到達啟動方格。",
@@ -1154,7 +1178,7 @@ export const zh_TW: TranslationMap = {
},
queue: {
retry: "重試",
retrySend: "重傳送",
retrySend: "重傳送",
retryQueuedMessage: "重試佇列中的訊息",
},
composer: {

View File

@@ -533,6 +533,37 @@
white-space: nowrap;
}
.workboard-events {
display: grid;
gap: 4px;
margin: 0;
padding: 7px 0 0;
border-top: 1px solid color-mix(in srgb, var(--border) 62%, transparent);
list-style: none;
}
.workboard-events li {
display: flex;
justify-content: space-between;
gap: 8px;
min-width: 0;
color: var(--muted);
font-size: 0.72rem;
line-height: 1.25;
}
.workboard-events span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.workboard-events time {
flex: 0 0 auto;
color: color-mix(in srgb, var(--muted) 72%, transparent);
}
.workboard-lifecycle {
flex: 0 0 auto;
border-radius: 6px;

View File

@@ -2044,6 +2044,10 @@ export function renderApp(state: AppViewState) {
enabledByDefault: false,
},
);
const operatorCanWrite = hasOperatorWriteAccess(
(state.hello as { auth?: { role?: string; scopes?: string[] } } | null)?.auth ??
null,
);
return m.renderSessions({
loading: state.sessionsLoading,
result: state.sessionsResult,
@@ -2064,7 +2068,7 @@ export function renderApp(state: AppViewState) {
selectedKeys: state.sessionsSelectedKeys,
workboardSessionKeys: new Set(
workboardState.cards
.map((card) => card.sessionKey)
.flatMap((card) => [card.sessionKey, card.execution?.sessionKey])
.filter((key): key is string => typeof key === "string" && key.length > 0),
),
workboardBusySessionKey: [...workboardState.capturingSessionKeys][0] ?? null,
@@ -2168,17 +2172,18 @@ export function renderApp(state: AppViewState) {
switchChatSession(state, sessionKey);
state.setTab("chat" as import("./navigation.ts").Tab);
},
onAddToWorkboard: workboardEnabled
? async (session) => {
await captureSessionToWorkboard({
host: state,
client: state.client,
session,
requestUpdate: requestHostUpdate,
});
state.setTab("workboard" as import("./navigation.ts").Tab);
}
: undefined,
onAddToWorkboard:
workboardEnabled && operatorCanWrite
? async (session) => {
await captureSessionToWorkboard({
host: state,
client: state.client,
session,
requestUpdate: requestHostUpdate,
});
state.setTab("workboard" as import("./navigation.ts").Tab);
}
: undefined,
onToggleCheckpointDetails: (sessionKey) =>
toggleSessionCompactionCheckpoints(state, sessionKey),
onBranchFromCheckpoint: async (sessionKey, checkpointId) => {

View File

@@ -204,7 +204,20 @@ describe("workboard controller", () => {
it("does not duplicate existing captured sessions", async () => {
const host = {};
const state = getWorkboardState(host);
const existing = { ...sampleCard, sessionKey: sampleSession.key };
const existing = {
...sampleCard,
execution: {
id: "exec-1",
kind: "agent-session",
engine: "codex",
mode: "autonomous",
status: "running",
model: "openai/gpt-5.5",
sessionKey: sampleSession.key,
startedAt: 1,
updatedAt: 1,
},
} satisfies WorkboardCard;
state.loaded = true;
state.cards = [existing];
const client = createClient({});
@@ -223,6 +236,8 @@ describe("workboard controller", () => {
const host = {};
const state = getWorkboardState(host);
state.capturingSessionKeys.add(sampleSession.key);
const existing = { ...sampleCard, sessionKey: sampleSession.key };
state.cards = [existing];
const client = createClient({});
const card = await captureSessionToWorkboard({
@@ -231,7 +246,7 @@ describe("workboard controller", () => {
session: sampleSession,
});
expect(card).toBeNull();
expect(card).toBe(existing);
expect(client.request).not.toHaveBeenCalled();
});
@@ -537,6 +552,12 @@ describe("workboard controller", () => {
state: "running",
targetStatus: "running",
});
expect(
getWorkboardLifecycle(linked, [{ ...sampleSession, hasActiveRun: false, status: "running" }]),
).toMatchObject({
state: "running",
targetStatus: "running",
});
expect(
getWorkboardLifecycle(linked, [{ ...sampleSession, hasActiveRun: false, status: "done" }]),
).toMatchObject({

View File

@@ -20,6 +20,13 @@ export const WORKBOARD_EXECUTION_STATUSES = [
"blocked",
"done",
] as const;
export const WORKBOARD_EVENT_KINDS = [
"created",
"edited",
"moved",
"linked",
"execution_updated",
] as const;
export const WORKBOARD_ENGINE_MODELS = {
codex: "openai/gpt-5.5",
@@ -31,6 +38,7 @@ export type WorkboardPriority = (typeof WORKBOARD_PRIORITIES)[number];
export type WorkboardExecutionEngine = (typeof WORKBOARD_EXECUTION_ENGINES)[number];
export type WorkboardExecutionMode = (typeof WORKBOARD_EXECUTION_MODES)[number];
export type WorkboardExecutionStatus = (typeof WORKBOARD_EXECUTION_STATUSES)[number];
export type WorkboardEventKind = (typeof WORKBOARD_EVENT_KINDS)[number];
export type WorkboardExecution = {
id: string;
@@ -45,6 +53,16 @@ export type WorkboardExecution = {
updatedAt: number;
};
export type WorkboardEvent = {
id: string;
kind: WorkboardEventKind;
at: number;
fromStatus?: WorkboardStatus;
toStatus?: WorkboardStatus;
sessionKey?: string;
runId?: string;
};
export type WorkboardCard = {
id: string;
title: string;
@@ -63,6 +81,7 @@ export type WorkboardCard = {
updatedAt: number;
startedAt?: number;
completedAt?: number;
events?: WorkboardEvent[];
};
export type WorkboardLifecycleState =
@@ -209,6 +228,41 @@ function normalizeExecution(value: unknown): WorkboardExecution | undefined {
};
}
function normalizeEvent(value: unknown): WorkboardEvent | null {
if (!isRecord(value)) {
return null;
}
const id = typeof value.id === "string" && value.id.trim() ? value.id.trim() : "";
const kind = WORKBOARD_EVENT_KINDS.includes(value.kind as WorkboardEventKind)
? (value.kind as WorkboardEventKind)
: null;
const at = typeof value.at === "number" && Number.isFinite(value.at) ? value.at : 0;
if (!id || !kind || !at) {
return null;
}
const fromStatus = WORKBOARD_STATUSES.includes(value.fromStatus as WorkboardStatus)
? (value.fromStatus as WorkboardStatus)
: undefined;
const toStatus = WORKBOARD_STATUSES.includes(value.toStatus as WorkboardStatus)
? (value.toStatus as WorkboardStatus)
: undefined;
return {
id,
kind,
at,
...(fromStatus ? { fromStatus } : {}),
...(toStatus ? { toStatus } : {}),
...(typeof value.sessionKey === "string" ? { sessionKey: value.sessionKey } : {}),
...(typeof value.runId === "string" ? { runId: value.runId } : {}),
};
}
function normalizeEvents(value: unknown): WorkboardEvent[] {
return Array.isArray(value)
? value.map(normalizeEvent).filter((event): event is WorkboardEvent => event !== null)
: [];
}
function normalizeCard(value: unknown): WorkboardCard | null {
if (!isRecord(value)) {
return null;
@@ -225,6 +279,7 @@ function normalizeCard(value: unknown): WorkboardCard | null {
return null;
}
const execution = normalizeExecution(value.execution);
const events = normalizeEvents(value.events);
return {
id,
title,
@@ -245,6 +300,7 @@ function normalizeCard(value: unknown): WorkboardCard | null {
...(execution ? { execution } : {}),
...(typeof value.startedAt === "number" ? { startedAt: value.startedAt } : {}),
...(typeof value.completedAt === "number" ? { completedAt: value.completedAt } : {}),
...(events.length ? { events } : {}),
};
}
@@ -422,6 +478,7 @@ function executionStatusForLifecycle(
case "unlinked":
return undefined;
}
return undefined;
}
function shouldSyncExecutionStatus(
@@ -584,7 +641,7 @@ export async function captureSessionToWorkboard(params: {
return null;
}
if (state.capturingSessionKeys.has(params.session.key)) {
return state.cards.find((card) => card.sessionKey === params.session.key) ?? null;
return state.cards.find((card) => workboardCardSessionKey(card) === params.session.key) ?? null;
}
state.error = null;
state.capturingSessionKeys.add(params.session.key);
@@ -601,7 +658,9 @@ export async function captureSessionToWorkboard(params: {
if (!state.loaded) {
return null;
}
const existing = state.cards.find((card) => card.sessionKey === params.session.key);
const existing = state.cards.find(
(card) => workboardCardSessionKey(card) === params.session.key,
);
if (existing) {
return existing;
}

View File

@@ -160,6 +160,83 @@ describe("renderWorkboard", () => {
expect(container.querySelector(".workboard-card")?.getAttribute("role")).toBeNull();
});
it("hides write controls for read-only operators", () => {
const host = {};
const state = getWorkboardState(host);
state.loaded = true;
state.cards = [
{
id: "card-1",
title: "Inspect only",
status: "todo",
priority: "normal",
labels: [],
position: 1000,
createdAt: 1,
updatedAt: 1,
},
];
const container = document.createElement("div");
render(
renderWorkboard({
host,
client: null,
connected: true,
canWrite: false,
pluginEnabled: true,
agentsList: null,
sessions: [],
onOpenSession: () => undefined,
}),
container,
);
expect(container.querySelector<HTMLButtonElement>('button[title="Edit card"]')).toBeNull();
expect(container.querySelector<HTMLButtonElement>('button[title="Delete card"]')).toBeNull();
expect(container.querySelectorAll<HTMLButtonElement>(".workboard-card__start")).toHaveLength(0);
expect(
container.querySelector<HTMLButtonElement>(".workboard-toolbar__actions .btn.primary"),
).toBeNull();
expect(container.querySelector(".workboard-card")?.getAttribute("draggable")).toBe("false");
});
it("offers start controls when a linked session no longer exists", () => {
const host = {};
const state = getWorkboardState(host);
state.loaded = true;
state.cards = [
{
id: "card-1",
title: "Restart this",
status: "blocked",
priority: "normal",
labels: [],
position: 1000,
createdAt: 1,
updatedAt: 1,
sessionKey: "agent:main:missing:1",
},
];
const container = document.createElement("div");
render(
renderWorkboard({
host,
client: null,
connected: true,
pluginEnabled: true,
agentsList: null,
sessions: [],
onOpenSession: () => undefined,
}),
container,
);
expect(container.textContent).toContain("Session missing");
expect(container.querySelectorAll<HTMLButtonElement>(".workboard-card__start")).toHaveLength(5);
});
it("opens a modal for new cards", () => {
const host = {};
getWorkboardState(host).loaded = true;
@@ -231,6 +308,44 @@ describe("renderWorkboard", () => {
expect(container.querySelector(".workboard-game__stats")?.textContent).toContain("Moves 1");
});
it("renders card event history", () => {
const host = {};
const state = getWorkboardState(host);
state.loaded = true;
state.cards = [
{
id: "card-1",
title: "Tracked task",
status: "review",
priority: "normal",
labels: [],
position: 1000,
createdAt: 1,
updatedAt: 2,
events: [
{ id: "event-1", kind: "created", at: 1, toStatus: "todo" },
{ id: "event-2", kind: "moved", at: 2, fromStatus: "todo", toStatus: "review" },
],
},
];
const container = document.createElement("div");
render(
renderWorkboard({
host,
client: null,
connected: true,
pluginEnabled: true,
agentsList: null,
sessions: [],
onOpenSession: () => undefined,
}),
container,
);
expect(container.querySelector(".workboard-events")?.textContent).toContain("Moved to Review");
});
it("opens an edit modal and submits card updates", async () => {
const host = {};
const state = getWorkboardState(host);

View File

@@ -15,6 +15,7 @@ import {
type WorkboardExecutionEngine,
type WorkboardExecutionMode,
type WorkboardCard,
type WorkboardEvent,
type WorkboardLifecycle,
type WorkboardPriority,
type WorkboardStatus,
@@ -54,6 +55,47 @@ function formatTime(value: number | undefined): string {
});
}
function canMutate(props: WorkboardProps): boolean {
return props.canWrite !== false;
}
function formatEventLabel(event: WorkboardEvent): string {
switch (event.kind) {
case "created":
return t("workboard.eventCreated");
case "edited":
return t("workboard.eventEdited");
case "moved":
return event.toStatus
? t("workboard.eventMovedTo", { status: formatStatusLabel(event.toStatus) })
: t("workboard.eventMoved");
case "linked":
return t("workboard.eventLinked");
case "execution_updated":
return t("workboard.eventExecutionUpdated");
}
return "";
}
function renderEvents(card: WorkboardCard) {
const events = (card.events ?? []).toReversed().slice(0, 4);
if (events.length === 0) {
return nothing;
}
return html`
<ol class="workboard-events" aria-label=${t("workboard.eventsLabel")}>
${events.map(
(event) => html`
<li>
<span>${formatEventLabel(event)}</span>
<time>${formatTime(event.at)}</time>
</li>
`,
)}
</ol>
`;
}
function matchesFilter(
card: WorkboardCard,
options: { query: string; priority: "all" | WorkboardPriority },
@@ -347,12 +389,10 @@ function renderCardModal(props: WorkboardProps) {
>
<div class="workboard-modal__header">
<div>
<h2 id="workboard-card-modal-title">${editing ? "Edit card" : "New card"}</h2>
<p>
${editing
? "Update queue metadata and session handoff."
: "Queue work for an agent session."}
</p>
<h2 id="workboard-card-modal-title">
${editing ? t("workboard.editCard") : t("workboard.newCard")}
</h2>
<p>${editing ? t("workboard.editCardHelp") : t("workboard.newCardHelp")}</p>
</div>
<button
class="btn btn--icon workboard-card__icon"
@@ -371,7 +411,7 @@ function renderCardModal(props: WorkboardProps) {
<span>${t("workboard.fieldTitle")}</span>
<input
class="input workboard-draft__title"
placeholder="Card title"
placeholder=${t("workboard.titlePlaceholder")}
.value=${state.draftTitle}
@input=${(event: InputEvent) => {
state.draftTitle = (event.currentTarget as HTMLInputElement).value;
@@ -383,7 +423,7 @@ function renderCardModal(props: WorkboardProps) {
<span>${t("workboard.fieldNotes")}</span>
<textarea
class="input workboard-draft__notes"
placeholder="Notes, acceptance criteria, links"
placeholder=${t("workboard.notesPlaceholder")}
.value=${state.draftNotes}
@input=${(event: InputEvent) => {
state.draftNotes = (event.currentTarget as HTMLTextAreaElement).value;
@@ -435,7 +475,7 @@ function renderCardModal(props: WorkboardProps) {
props.onRequestUpdate?.();
}}
>
<option value="">Default agent</option>
<option value="">${t("workboard.defaultAgent")}</option>
${agents.map(
(agent) =>
html`<option value=${agent.id}>
@@ -568,8 +608,10 @@ function renderStartExecutionButton(
const state = getWorkboardState(props.host);
const busy = state.busyCardId === card.id;
const title = engine
? `${mode === "autonomous" ? "Run" : "Open"} ${engine}`
: "Run default agent";
? mode === "autonomous"
? t("workboard.runEngine", { engine })
: t("workboard.openEngine", { engine })
: t("workboard.runDefaultAgent");
return html`
<button
class="btn btn--xs workboard-card__start workboard-card__start--${mode} ${engine
@@ -591,7 +633,7 @@ function renderStartExecutionButton(
}
}}
>
${mode === "autonomous" ? icons.play : icons.penLine} ${engine ?? "Start"}
${mode === "autonomous" ? icons.play : icons.penLine} ${engine ?? t("workboard.start")}
</button>
`;
}
@@ -613,9 +655,11 @@ function renderCard(props: WorkboardProps, card: WorkboardCard) {
const session = findWorkboardSession(card, props.sessions);
const busy = state.busyCardId === card.id;
const syncing = state.syncingCardIds.has(card.id);
const live = session?.hasActiveRun === true;
const live = session?.hasActiveRun === true || session?.status === "running";
const linkedSessionKey = card.sessionKey ?? card.execution?.sessionKey;
const linked = Boolean(linkedSessionKey);
const writable = canMutate(props);
const showStartControls = writable && (!linked || !session);
return html`
<article
class="workboard-card priority-${card.priority} ${busy ? "workboard-card--busy" : ""} ${linked
@@ -623,8 +667,8 @@ function renderCard(props: WorkboardProps, card: WorkboardCard) {
: ""}"
role=${linked ? "button" : nothing}
tabindex=${linked ? 0 : nothing}
title=${linked ? "Open linked session" : nothing}
draggable="true"
title=${linked ? t("workboard.openLinkedSession") : nothing}
draggable=${writable ? "true" : "false"}
@click=${(event: MouseEvent) => {
if (!isCardActionTarget(event)) {
openCardSession(props, card);
@@ -639,6 +683,10 @@ function renderCard(props: WorkboardProps, card: WorkboardCard) {
}
}}
@dragstart=${(event: DragEvent) => {
if (!writable) {
event.preventDefault();
return;
}
state.draggedCardId = card.id;
event.dataTransfer?.setData("text/plain", card.id);
event.dataTransfer?.setDragImage(event.currentTarget as Element, 16, 16);
@@ -651,7 +699,7 @@ function renderCard(props: WorkboardProps, card: WorkboardCard) {
>
<div class="workboard-card__top">
<span class="workboard-card__priority">${card.priority}</span>
${live ? html`<span class="workboard-live">live</span>` : nothing}
${live ? html`<span class="workboard-live">${t("workboard.live")}</span>` : nothing}
${syncing ? html`<span class="workboard-live">${t("common.saving")}</span>` : nothing}
</div>
<h3>${card.title}</h3>
@@ -662,30 +710,37 @@ function renderCard(props: WorkboardProps, card: WorkboardCard) {
</div>`
: nothing}
<div class="workboard-card__meta">
${card.agentId ? html`<span>${card.agentId}</span>` : html`<span>default agent</span>`}
${card.agentId
? html`<span>${card.agentId}</span>`
: html`<span>${t("workboard.defaultAgent")}</span>`}
<span>${formatTime(card.updatedAt)}</span>
</div>
${renderEvents(card)}
<div class="workboard-card__actions">
<button
class="btn btn--icon workboard-card__icon"
title=${t("workboard.editCard")}
@click=${() => {
openEditModal(state, card);
props.onRequestUpdate?.();
}}
>
${icons.edit}
</button>
${writable
? html`
<button
class="btn btn--icon workboard-card__icon"
title=${t("workboard.editCard")}
@click=${() => {
openEditModal(state, card);
props.onRequestUpdate?.();
}}
>
${icons.edit}
</button>
`
: nothing}
${linked
? html`
<button
class="btn btn--icon workboard-card__icon"
title="Open session"
title=${t("workboard.openSession")}
@click=${() => props.onOpenSession(linkedSessionKey!)}
>
${icons.messageSquare}
</button>
${live
${writable && live
? html`
<button
class="btn btn--icon workboard-card__icon"
@@ -704,21 +759,26 @@ function renderCard(props: WorkboardProps, card: WorkboardCard) {
`
: nothing}
`
: renderStartExecutionControls(props, card)}
<button
class="btn btn--icon workboard-card__icon workboard-card__delete"
title="Delete card"
?disabled=${busy}
@click=${() =>
deleteWorkboardCard({
host: props.host,
client: props.client,
cardId: card.id,
requestUpdate: props.onRequestUpdate,
})}
>
${icons.trash}
</button>
: nothing}
${showStartControls ? renderStartExecutionControls(props, card) : nothing}
${writable
? html`
<button
class="btn btn--icon workboard-card__icon workboard-card__delete"
title=${t("workboard.deleteCard")}
?disabled=${busy}
@click=${() =>
deleteWorkboardCard({
host: props.host,
client: props.client,
cardId: card.id,
requestUpdate: props.onRequestUpdate,
})}
>
${icons.trash}
</button>
`
: nothing}
</div>
</article>
`;
@@ -726,16 +786,20 @@ function renderCard(props: WorkboardProps, card: WorkboardCard) {
function renderColumn(props: WorkboardProps, status: WorkboardStatus, cards: WorkboardCard[]) {
const state = getWorkboardState(props.host);
const writable = canMutate(props);
return html`
<section
class="workboard-column ${state.draggedCardId ? "workboard-column--drop" : ""}"
@dragover=${(event: DragEvent) => {
if (state.draggedCardId) {
if (writable && state.draggedCardId) {
event.preventDefault();
}
}}
@drop=${(event: DragEvent) => {
event.preventDefault();
if (!writable) {
return;
}
const cardId = event.dataTransfer?.getData("text/plain") || state.draggedCardId;
if (!cardId) {
return;
@@ -757,7 +821,7 @@ function renderColumn(props: WorkboardProps, status: WorkboardStatus, cards: Wor
<div class="workboard-column__cards">
${cards.length
? cards.map((card) => renderCard(props, card))
: html`<div class="workboard-empty">Drop work here</div>`}
: html`<div class="workboard-empty">${t("workboard.emptyColumn")}</div>`}
</div>
</section>
`;
@@ -794,6 +858,7 @@ export function renderWorkboard(props: WorkboardProps) {
const filtered = state.cards.filter((card) =>
matchesFilter(card, { query: state.query, priority: state.priorityFilter }),
);
const writable = canMutate(props);
const byStatus = new Map<WorkboardStatus, WorkboardCard[]>();
for (const status of state.statuses) {
byStatus.set(status, []);
@@ -809,7 +874,7 @@ export function renderWorkboard(props: WorkboardProps) {
<input
class="input"
type="search"
placeholder="Search cards"
placeholder=${t("workboard.searchPlaceholder")}
.value=${state.query}
@input=${(event: InputEvent) => {
state.query = (event.currentTarget as HTMLInputElement).value;
@@ -825,7 +890,7 @@ export function renderWorkboard(props: WorkboardProps) {
props.onRequestUpdate?.();
}}
>
<option value="all">All priorities</option>
<option value="all">${t("workboard.allPriorities")}</option>
${WORKBOARD_PRIORITIES.map(
(priority) => html`<option value=${priority}>${priority}</option>`,
)}
@@ -854,15 +919,19 @@ export function renderWorkboard(props: WorkboardProps) {
>
${icons.play} ${t("workboard.gameButton")}
</button>
<button
class="btn primary"
@click=${() => {
openCreateModal(state);
props.onRequestUpdate?.();
}}
>
${icons.plus} New card
</button>
${writable
? html`
<button
class="btn primary"
@click=${() => {
openCreateModal(state);
props.onRequestUpdate?.();
}}
>
${icons.plus} ${t("workboard.newCard")}
</button>
`
: nothing}
</div>
</div>
${state.error ? html`<div class="callout danger">${state.error}</div>` : nothing}