@a-t-h-i/bot-lobby 0.6.44 → 0.6.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +15 -4
  2. package/package.json +1 -6
  3. package/src/execution/agent-runner.ts +0 -2
  4. package/src/execution/delegation.ts +1 -2
  5. package/src/execution/pi-runner.ts +2 -6
  6. package/src/execution/task-cancellation.ts +27 -0
  7. package/src/index.ts +0 -3
  8. package/src/lobby/host.ts +5 -12
  9. package/src/lobby/planner.ts +1 -6
  10. package/src/lobby/prompts.ts +4 -7
  11. package/src/lobby/quickfix.ts +0 -2
  12. package/src/lobby/runtime.ts +1 -5
  13. package/src/lobby/service.ts +77 -7
  14. package/src/lobby/topics.ts +0 -1
  15. package/src/pi/tools.ts +2 -2
  16. package/src/state/persistence.ts +26 -1
  17. package/src/webui/api/index.ts +13 -45
  18. package/src/webui/api/tasks.ts +39 -2
  19. package/src/webui/dev/fake-service.ts +0 -57
  20. package/src/webui/dev/fixtures.ts +0 -6
  21. package/src/webui/palette-core.ts +233 -2
  22. package/src/webui/protocol.ts +6 -103
  23. package/src/workflow/workflow.ts +16 -20
  24. package/webui/dist/assets/index-BJ1fEle9.js +110 -0
  25. package/webui/dist/assets/index-BdtiIB9L.css +2 -0
  26. package/webui/dist/build.json +39 -107
  27. package/webui/dist/index.html +2 -2
  28. package/src/excalidraw/check.ts +0 -31
  29. package/src/excalidraw/client.ts +0 -334
  30. package/src/excalidraw/room.ts +0 -118
  31. package/src/excalidraw/scene.ts +0 -746
  32. package/src/excalidraw/sessions.ts +0 -302
  33. package/src/excalidraw/tools.ts +0 -223
  34. package/src/lobby/models/metrics.ts +0 -14
  35. package/src/webui/api/excalidraw.ts +0 -105
  36. package/src/webui/api/metrics.ts +0 -96
  37. package/webui/dist/assets/index-C0T8aK16.js +0 -110
  38. package/webui/dist/assets/index-XtSiJAAR.css +0 -2
@@ -15,10 +15,8 @@ import { lobbyAbort, lobbyHistory, lobbySend, lobbySnapshot } from "./lobby.ts";
15
15
  import { promptsAnswer, promptsDismiss, promptsList } from "./prompts.ts";
16
16
  import { plannerAnswer, plannerCommentLine, plannerGet, plannerNew, plannerRetry, plannerSend, plannerEditMessage, plannerToggleSeat, plannerSave, plannerPrevious, plannerPreviousArchive, plannerPreviousDelete, plannerPreviousGet, plannerPreviousOpen } from "./planner.ts";
17
17
  import { quickfixCancel, quickfixList, quickfixMovedToTask, quickfixRunAnyway, quickfixSubmit } from "./quickfix.ts";
18
- import { tasksArchive, tasksArchived, tasksAuto, tasksComment, tasksComments, tasksDelete, tasksEditComment, tasksGet, tasksList, tasksMessage, tasksRestore, tasksOpen, tasksResume, tasksDeliveryReview, tasksDeliveryDefer, tasksDeliver } from "./tasks.ts";
19
- import { metricsGet } from "./metrics.ts";
18
+ import { tasksArchive, tasksArchived, tasksAuto, tasksComment, tasksComments, tasksDelete, tasksEditComment, tasksGet, tasksList, tasksMessage, tasksRestore, tasksOpen, tasksResume, tasksDeliveryReview, tasksDeliveryDefer, tasksDeliver, tasksAccept, tasksForceClose, tasksResolveApproval } from "./tasks.ts";
20
19
  import { knowledgeAdd, knowledgeAsk, knowledgeComment, knowledgeEdit, knowledgeFiles, knowledgeOpen, knowledgeRemove, knowledgeReplaceFile, knowledgeUnnote } from "./knowledge.ts";
21
- import { excalidrawAdd, excalidrawCheck, excalidrawCreate, excalidrawList, excalidrawRemove, excalidrawRename, excalidrawReveal, excalidrawToggleAgent, excalidrawToggleAll, excalidrawToggleContribute } from "./excalidraw.ts";
22
20
  import { plansDiscard, plansGet, plansStart } from "./plans.ts";
23
21
  import { gitCancelReview, gitJev, gitPull, gitPulls, gitReview } from "./git.ts";
24
22
  import { issuesCreate, issuesGet, issuesList } from "./issues.ts";
@@ -93,7 +91,6 @@ const KnowledgeAgent = Type.Union([Type.Literal("master"), Type.Literal("designe
93
91
  const KnowledgeFile = Type.String({ minLength: 1, maxLength: 200 });
94
92
  const KnowledgeText = Type.String({ minLength: 1, maxLength: 100_000 });
95
93
  const EntryRef = Type.Object({ text: Type.String({ minLength: 1, maxLength: 100_000 }), occurrence: Type.Integer({ minimum: 0, maximum: 100000 }) }, { additionalProperties: false });
96
- const ExcalidrawAgent = Type.Union([Type.Literal("master"), Type.Literal("designer"), Type.Literal("backend"), Type.Literal("qa"), Type.Literal("scout"), Type.Literal("researcher"), Type.Literal("quickfix"), Type.Literal("planner")]);
97
94
  const PlannerSeed = Type.Object({
98
95
  issue: Type.Object({ number: Type.Integer({ minimum: 1 }), title: Type.String({ minLength: 1, maxLength: 500 }), url: Type.Optional(Type.String({ maxLength: 2000 })) }, { additionalProperties: false }),
99
96
  body: Type.String({ maxLength: 20_000 }),
@@ -133,6 +130,18 @@ function buildRoutes(): Record<string, Route> {
133
130
  schema: Type.Object({ taskId: TaskId }, { additionalProperties: false }),
134
131
  run: (body, ctx) => tasksResume(body as { taskId: string }, ctx),
135
132
  };
133
+ routes["tasks.accept"] = {
134
+ schema: Type.Object({ taskId: TaskId }, { additionalProperties: false }),
135
+ run: (body, ctx) => tasksAccept(body as { taskId: string }, ctx),
136
+ };
137
+ routes["tasks.forceClose"] = {
138
+ schema: Type.Object({ taskId: TaskId, title: Type.String({ maxLength: 20_000 }) }, { additionalProperties: false }),
139
+ run: (body, ctx) => tasksForceClose(body as { taskId: string; title: string }, ctx),
140
+ };
141
+ routes["tasks.resolveApproval"] = {
142
+ schema: Type.Object({ taskId: TaskId, approvalId: TaskId, decision: Type.Union([Type.Literal("approved"), Type.Literal("rejected")]), note: Type.Optional(NoticeText) }, { additionalProperties: false }),
143
+ run: (body, ctx) => tasksResolveApproval(body as Parameters<typeof tasksResolveApproval>[0], ctx),
144
+ };
136
145
  routes["tasks.deliveryReview"] = {
137
146
  schema: Type.Object({ taskId: TaskId }, { additionalProperties: false }),
138
147
  run: (body, ctx) => tasksDeliveryReview(body as { taskId: string }, ctx),
@@ -270,10 +279,6 @@ function buildRoutes(): Record<string, Route> {
270
279
  schema: Type.Object({ id: TaskId }, { additionalProperties: false }),
271
280
  run: (body, ctx) => quickfixMovedToTask(body as { id: string }, ctx),
272
281
  };
273
- routes["metrics.get"] = {
274
- schema: Type.Object({ groupBy: Type.Union([Type.Literal("model"), Type.Literal("model-kind")]), query: Type.Optional(Type.String({ maxLength: 500 })), from: Type.Optional(Type.String({ pattern: "^\\d{4}-\\d{2}-\\d{2}$" })), to: Type.Optional(Type.String({ pattern: "^\\d{4}-\\d{2}-\\d{2}$" })), timeZone: Type.Optional(Type.String({ maxLength: 100 })) }, { additionalProperties: false }),
275
- run: (body, ctx) => metricsGet(body as Parameters<typeof metricsGet>[0], ctx),
276
- };
277
282
  routes["knowledge.files"] = { schema: Empty, run: (_body, ctx) => knowledgeFiles(ctx) };
278
283
  routes["knowledge.ask"] = {
279
284
  schema: Type.Object({ text: NoticeText, attachments: Attachments }, { additionalProperties: false }),
@@ -307,43 +312,6 @@ function buildRoutes(): Record<string, Route> {
307
312
  schema: Type.Object({ id: TaskId }, { additionalProperties: false }),
308
313
  run: (body, ctx) => knowledgeUnnote(body as { id: string }, ctx),
309
314
  };
310
- routes["excalidraw.list"] = { schema: Empty, run: (_body, ctx) => excalidrawList(ctx) };
311
- routes["excalidraw.add"] = {
312
- schema: Type.Object({ link: Type.String({ minLength: 1, maxLength: 2000 }), name: Type.Optional(Type.String({ maxLength: 40 })) }, { additionalProperties: false }),
313
- run: (body, ctx) => excalidrawAdd(body as { link: string; name?: string }, ctx),
314
- };
315
- routes["excalidraw.create"] = {
316
- schema: Type.Object({ name: Type.Optional(Type.String({ maxLength: 40 })) }, { additionalProperties: false }),
317
- run: (body, ctx) => excalidrawCreate(body as { name?: string }, ctx),
318
- };
319
- routes["excalidraw.remove"] = {
320
- schema: Type.Object({ id: TaskId }, { additionalProperties: false }),
321
- run: (body, ctx) => excalidrawRemove(body as { id: string }, ctx),
322
- };
323
- routes["excalidraw.rename"] = {
324
- schema: Type.Object({ id: TaskId, name: Type.String({ maxLength: 40 }) }, { additionalProperties: false }),
325
- run: (body, ctx) => excalidrawRename(body as { id: string; name: string }, ctx),
326
- };
327
- routes["excalidraw.toggleAgent"] = {
328
- schema: Type.Object({ id: TaskId, agent: ExcalidrawAgent }, { additionalProperties: false }),
329
- run: (body, ctx) => excalidrawToggleAgent(body as { id: string; agent: "master" | "designer" | "backend" | "qa" | "scout" | "researcher" | "quickfix" | "planner" }, ctx),
330
- };
331
- routes["excalidraw.toggleAll"] = {
332
- schema: Type.Object({ id: TaskId }, { additionalProperties: false }),
333
- run: (body, ctx) => excalidrawToggleAll(body as { id: string }, ctx),
334
- };
335
- routes["excalidraw.toggleContribute"] = {
336
- schema: Type.Object({ id: TaskId }, { additionalProperties: false }),
337
- run: (body, ctx) => excalidrawToggleContribute(body as { id: string }, ctx),
338
- };
339
- routes["excalidraw.check"] = {
340
- schema: Type.Object({ id: TaskId }, { additionalProperties: false }),
341
- run: (body, ctx) => excalidrawCheck(body as { id: string }, ctx),
342
- };
343
- routes["excalidraw.reveal"] = {
344
- schema: Type.Object({ id: TaskId }, { additionalProperties: false }),
345
- run: (body, ctx) => excalidrawReveal(body as { id: string }, ctx),
346
- };
347
315
  routes["git.pulls"] = {
348
316
  schema: Type.Object({ refresh: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
349
317
  run: (body, ctx) => gitPulls(body as { refresh?: boolean }, ctx),
@@ -282,17 +282,52 @@ export function tasksOpen(body: { taskId: string }, ctx: ApiContext): { sessionI
282
282
  return { notice: "The task's owning session has ended or is unavailable. No replacement session was created." };
283
283
  }
284
284
 
285
+ /** Interventions only apply to an active task in this project, never an archive. */
286
+ function interventionTask(taskId: string, ctx: ApiContext): Task {
287
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,199}$/.test(taskId)) fail(400, "bad_request", "invalid task ID");
288
+ const task = ctx.service.tasks().find((entry) => entry.id === taskId);
289
+ if (!task) fail(404, "not_found", "task not found in this project");
290
+ if (TERMINAL_STATES.includes(task.state)) fail(400, "bad_request", "this task is already finished");
291
+ return task;
292
+ }
293
+
294
+ function acceptable(task: Task): boolean {
295
+ return ["implementing", "reviewing", "blocked"].includes(task.state);
296
+ }
297
+
298
+ export async function tasksAccept(body: { taskId: string }, ctx: ApiContext): Promise<{ notice: string }> {
299
+ const task = interventionTask(body.taskId, ctx);
300
+ if (!acceptable(task)) fail(400, "bad_request", "only implementing, reviewing or blocked tasks can be accepted");
301
+ if (!ctx.service.acceptTask) fail(400, "bad_request", "accepting tasks is unavailable");
302
+ return ctx.service.acceptTask(body.taskId);
303
+ }
304
+
305
+ export async function tasksForceClose(body: { taskId: string; title: string }, ctx: ApiContext): Promise<{ notice: string }> {
306
+ const task = interventionTask(body.taskId, ctx);
307
+ if (body.title !== task.title) fail(400, "bad_request", "the title must match exactly");
308
+ if (!ctx.service.forceCloseTask) fail(400, "bad_request", "force closing tasks is unavailable");
309
+ return ctx.service.forceCloseTask(body.taskId, body.title);
310
+ }
311
+
312
+ export async function tasksResolveApproval(body: { taskId: string; approvalId: string; decision: "approved" | "rejected"; note?: string }, ctx: ApiContext): Promise<{ notice: string }> {
313
+ const task = interventionTask(body.taskId, ctx);
314
+ if (!pendingApprovals(task).some((approval) => approval.id === body.approvalId)) fail(409, "conflict", "approval is no longer pending; refresh the task");
315
+ if (!ctx.service.resolveTaskApproval) fail(400, "bad_request", "resolving task approvals is unavailable");
316
+ return ctx.service.resolveTaskApproval(body.taskId, body.approvalId, body.decision, body.note);
317
+ }
318
+
285
319
  /** How many recent runs the detail lists (the terminal's count). */
286
320
  const DETAIL_RUNS = 6;
287
321
 
288
322
  /** The terminal's "Waiting on" approvals: `kind` plus `for <domain>: <detail>`. */
289
323
  function waitingOf(task: Task): TaskDetail["waiting"] {
290
- return pendingApprovals(task).map((approval) => ({ kind: approval.kind, detail: `for ${approval.domain}: ${approval.detail}` }));
324
+ return pendingApprovals(task).map((approval) => ({ id: approval.id, domain: approval.domain, kind: approval.kind, detail: `for ${approval.domain}: ${approval.detail}` }));
291
325
  }
292
326
 
293
327
  /** One task read whole: request, plan or proposal, step checklist, waits and recent runs. */
294
328
  export function tasksGet(body: { taskId: string }, ctx: ApiContext): TaskDetail {
295
- const task = [...ctx.service.tasks(), ...ctx.service.archivedTasks()].find((entry) => entry.id === body.taskId);
329
+ const active = ctx.service.tasks().find((entry) => entry.id === body.taskId);
330
+ const task = active ?? ctx.service.archivedTasks().find((entry) => entry.id === body.taskId);
296
331
  if (!task) fail(404, "not_found", `no task ${body.taskId}`);
297
332
  const found = task as Task;
298
333
  const request = taskRequest(found);
@@ -301,6 +336,8 @@ export function tasksGet(body: { taskId: string }, ctx: ApiContext): TaskDetail
301
336
  const clock = clockOf(found, work);
302
337
  const details = found.plan ? planDetails(found.plan) : undefined;
303
338
  return {
339
+ canAccept: Boolean(active && ctx.service.acceptTask) && acceptable(found),
340
+ canForceClose: Boolean(active && ctx.service.forceCloseTask) && !TERMINAL_STATES.includes(found.state) && (ctx.service.canForceCloseTask?.(found.id) ?? true),
304
341
  timing: projectPhaseTiming(found, new Date(now).toISOString()),
305
342
  ...(clock ? { work: clock } : {}),
306
343
  ...(found.delivery ? { delivery: found.delivery } : {}),
@@ -342,10 +342,6 @@ export function createFixtureService(name: string, feed = new LobbyFeed()): Lobb
342
342
  const classifierMetrics = (fixture.mockClassifierMetrics ?? []).map((record) => ({ ...record }));
343
343
  const knowledgeFiles = (fixture.mockKnowledge ?? []).map((file) => ({ ...file }));
344
344
  const knowledgeView = fixture.mockKnowledgeView ? { ...fixture.mockKnowledgeView, entries: [...fixture.mockKnowledgeView.entries], attached: [...fixture.mockKnowledgeView.attached], detached: [...fixture.mockKnowledgeView.detached] } : undefined;
345
- const xSessions = (fixture.mockExcalidraw ?? []).map((session) => ({ ...session, agents: [...session.agents] }));
346
- const xLinks = new Map<string, string>();
347
- if (xSessions[0] && fixture.mockExcalidrawLink) xLinks.set(xSessions[0].id, fixture.mockExcalidrawLink);
348
- const xLinkOf = (id: string): string => xLinks.get(id) ?? `https://whiteboard.example/#room=${id},AAAAAAAAAAAAAAAAAAAAAA`;
349
345
  const knowledge = {
350
346
  files: () => knowledgeFiles.map((file) => ({ ...file })),
351
347
  open: (agent: string, file: string) => {
@@ -359,57 +355,6 @@ export function createFixtureService(name: string, feed = new LobbyFeed()): Lobb
359
355
  comment: () => "note saved — every agent reads it under this entry",
360
356
  unnote: () => "note removed",
361
357
  };
362
- const excalidraw = {
363
- list: () => xSessions.map((session) => ({ id: session.id, name: session.name, link: xLinkOf(session.id), agents: [...session.agents], contribute: session.contribute, addedAt: session.addedAt })),
364
- add: (link: string, name?: string) => {
365
- if (!link.includes("#room=")) return { notice: "that is not an Excalidraw room link — it looks like https://excalidraw.com/#room=<id>,<key>" };
366
- const session = { id: `x${xSessions.length + 1}`, name: name || `Session ${xSessions.length + 1}`, link, agents: [] as string[], contribute: true, addedAt: new Date().toISOString() };
367
- xSessions.push({ id: session.id, name: session.name, masked: "", agents: [], contribute: true, addedAt: session.addedAt });
368
- xLinks.set(session.id, link);
369
- return { notice: `added “${session.name}” — assign it to agents with enter`, session };
370
- },
371
- create: (name?: string) => {
372
- const id = `x${xSessions.length + 1}`;
373
- const link = `https://whiteboard.example/#room=mock${xSessions.length + 1},AAAAAAAAAAAAAAAAAAAAAA`;
374
- const session = { id, name: name || `Session ${xSessions.length + 1}`, link, agents: [] as string[], contribute: true, addedAt: new Date().toISOString() };
375
- xSessions.push({ id, name: session.name, masked: "", agents: [], contribute: true, addedAt: session.addedAt });
376
- xLinks.set(id, link);
377
- return { notice: `added “${session.name}” — assign it to agents with enter`, session };
378
- },
379
- remove: (id: string) => {
380
- const index = xSessions.findIndex((session) => session.id === id);
381
- if (index < 0) return "no such session";
382
- const [gone] = xSessions.splice(index, 1);
383
- return `removed “${gone!.name}”`;
384
- },
385
- rename: (id: string, name: string) => {
386
- const session = xSessions.find((entry) => entry.id === id);
387
- if (!session) return "no such session";
388
- if (!name.trim()) return "a session needs a name";
389
- session.name = name.trim().slice(0, 40);
390
- return `renamed to “${session.name}”`;
391
- },
392
- toggleAgent: (id: string, agent: string) => {
393
- const session = xSessions.find((entry) => entry.id === id);
394
- if (!session) return "no such session";
395
- const at = session.agents.indexOf(agent);
396
- if (at >= 0) session.agents.splice(at, 1);
397
- else session.agents.push(agent);
398
- return at >= 0 ? `${agent} no longer has this session` : `${agent} has this session now`;
399
- },
400
- toggleAll: (id: string) => {
401
- const session = xSessions.find((entry) => entry.id === id);
402
- if (!session) return "no such session";
403
- session.agents = session.agents.length > 0 ? [] : ["master"];
404
- return session.agents.length > 0 ? "assigned to every agent" : "taken back from every agent";
405
- },
406
- toggleContribute: (id: string) => {
407
- const session = xSessions.find((entry) => entry.id === id);
408
- if (!session) return "no such session";
409
- session.contribute = !session.contribute;
410
- return session.contribute ? "agents may draw in this session" : "agents may only look at this session";
411
- },
412
- };
413
358
  const qfJobs = fakeQuickFixJobs(fixture.mockQuickfix ?? []);
414
359
  let qfCounter = qfJobs.length;
415
360
  const quickfix = {
@@ -485,8 +430,6 @@ export function createFixtureService(name: string, feed = new LobbyFeed()): Lobb
485
430
  metrics: () => metrics.map((record) => ({ ...record })),
486
431
  classifierMetrics: () => classifierMetrics.map((record) => ({ ...record })),
487
432
  knowledge,
488
- excalidraw,
489
- checkExcalidraw: async () => ({ ok: true, text: "reached the server; nobody has this session open yet — open the link in Excalidraw, and agents can read and draw" }),
490
433
  comments: (taskId: string) => [...(comments.get(taskId) ?? [])],
491
434
  comment: (taskId: string, text: string) => {
492
435
  const entry = { id: `C-mock-${(commentSeq += 1)}`, taskId, text, by: fixture.status.sessionId, createdAt: new Date().toISOString(), status: "open" };
@@ -101,10 +101,6 @@ export interface ScenarioFixture {
101
101
  mockKnowledge?: Array<{ agent: string; file: string; label: string; chars: number; over: boolean; notes: number }>;
102
102
  /** One open knowledge file for `knowledge.open` (absent means an empty file). */
103
103
  mockKnowledgeView?: { agent: string; file: string; label: string; chars: number; over: boolean; notes: number; content: string; entries: Array<Record<string, unknown>>; attached: Array<Record<string, unknown>>; detached: Array<Record<string, unknown>> };
104
- /** Excalidraw sessions for `excalidraw.list` (masked links only; absent means none). */
105
- mockExcalidraw?: Array<{ id: string; name: string; masked: string; agents: string[]; contribute: boolean; addedAt: string }>;
106
- /** The full link `excalidraw.reveal` returns (absent means none). */
107
- mockExcalidrawLink?: string;
108
104
  /** Open pull requests for the `git.*` calls, each with its detail fields (absent means none). */
109
105
  mockPulls?: Array<Record<string, unknown>>;
110
106
  /** Saved reviews of those pull requests, by number. */
@@ -173,8 +169,6 @@ export function loadScenario(name: string | undefined): ScenarioFixture {
173
169
  mockClassifierMetrics: Array.isArray(parsed.mockClassifierMetrics) ? (parsed.mockClassifierMetrics as ScenarioFixture["mockClassifierMetrics"]) : [],
174
170
  mockKnowledge: Array.isArray(parsed.mockKnowledge) ? (parsed.mockKnowledge as ScenarioFixture["mockKnowledge"]) : [],
175
171
  mockKnowledgeView: isRecord(parsed.mockKnowledgeView) ? (parsed.mockKnowledgeView as unknown as ScenarioFixture["mockKnowledgeView"]) : undefined,
176
- mockExcalidraw: Array.isArray(parsed.mockExcalidraw) ? (parsed.mockExcalidraw as ScenarioFixture["mockExcalidraw"]) : [],
177
- mockExcalidrawLink: typeof parsed.mockExcalidrawLink === "string" ? (parsed.mockExcalidrawLink as string) : undefined,
178
172
  mockPulls: Array.isArray(parsed.mockPulls) ? (parsed.mockPulls as ScenarioFixture["mockPulls"]) : [],
179
173
  mockReviews: Array.isArray(parsed.mockReviews) ? (parsed.mockReviews as ScenarioFixture["mockReviews"]) : [],
180
174
  mockReads: Array.isArray(parsed.mockReads) ? (parsed.mockReads as ScenarioFixture["mockReads"]) : [],
@@ -174,10 +174,11 @@ export const DEFAULT_ID = "mist"
174
174
 
175
175
  /**
176
176
  * The themes that come with the page. The first is the page's own colours and
177
- * sets nothing; the rest are T3 Code's built-in themes (MIT, Copyright (c) 2026
177
+ * sets nothing; Blossom through Iris are T3 Code's built-in themes (MIT, Copyright (c) 2026
178
178
  * T3 Tools Inc., packages/shared/src/themePalettes.ts), their roles mapped onto
179
179
  * the shadcn variables: canvas is the background, surface the card, accent the
180
- * primary, accent surface the accent, error the destructive colour.
180
+ * primary, accent surface the accent, error the destructive colour. Slate,
181
+ * Sand, Rosewood, Aurora and Midnight are original complementary palettes.
181
182
  */
182
183
  export const PRESETS: readonly Palette[] = [
183
184
  { id: DEFAULT_ID, name: "Default" },
@@ -411,6 +412,236 @@ export const PRESETS: readonly Palette[] = [
411
412
  "chart-1": "oklch(0.671712 0.169136 293.929)",
412
413
  },
413
414
  },
415
+ {
416
+ id: "slate",
417
+ name: "Slate",
418
+ light: {
419
+ "background": "#f5f7fa",
420
+ "foreground": "#202b3b",
421
+ "card": "#ffffff",
422
+ "card-foreground": "#202b3b",
423
+ "popover": "#ffffff",
424
+ "popover-foreground": "#202b3b",
425
+ "primary": "#465b78",
426
+ "primary-foreground": "#ffffff",
427
+ "secondary": "#e7ecf2",
428
+ "secondary-foreground": "#202b3b",
429
+ "muted": "#edf1f6",
430
+ "muted-foreground": "#526177",
431
+ "accent": "#e0e7ef",
432
+ "accent-foreground": "#202b3b",
433
+ "destructive": "#a52a34",
434
+ "border": "#cad3df",
435
+ "input": "#b9c5d4",
436
+ "ring": "#465b78",
437
+ "chart-1": "#465b78",
438
+ },
439
+ dark: {
440
+ "background": "#141b26",
441
+ "foreground": "#e8edf5",
442
+ "card": "#1c2533",
443
+ "card-foreground": "#e8edf5",
444
+ "popover": "#202b3b",
445
+ "popover-foreground": "#e8edf5",
446
+ "primary": "#adc0dc",
447
+ "primary-foreground": "#141b26",
448
+ "secondary": "#2a3648",
449
+ "secondary-foreground": "#e8edf5",
450
+ "muted": "#243043",
451
+ "muted-foreground": "#afbdd0",
452
+ "accent": "#33445d",
453
+ "accent-foreground": "#e8edf5",
454
+ "destructive": "#fb8990",
455
+ "border": "#43536a",
456
+ "input": "#53647d",
457
+ "ring": "#adc0dc",
458
+ "chart-1": "#adc0dc",
459
+ },
460
+ },
461
+ {
462
+ id: "sand",
463
+ name: "Sand",
464
+ light: {
465
+ "background": "#faf7f1",
466
+ "foreground": "#352b22",
467
+ "card": "#fffdf8",
468
+ "card-foreground": "#352b22",
469
+ "popover": "#fffdf8",
470
+ "popover-foreground": "#352b22",
471
+ "primary": "#765536",
472
+ "primary-foreground": "#ffffff",
473
+ "secondary": "#eee6da",
474
+ "secondary-foreground": "#352b22",
475
+ "muted": "#f2ece2",
476
+ "muted-foreground": "#6c5b49",
477
+ "accent": "#e9ddcb",
478
+ "accent-foreground": "#352b22",
479
+ "destructive": "#a52a34",
480
+ "border": "#d9cbb8",
481
+ "input": "#c9b79e",
482
+ "ring": "#765536",
483
+ "chart-1": "#765536",
484
+ },
485
+ dark: {
486
+ "background": "#211c17",
487
+ "foreground": "#f4ece0",
488
+ "card": "#2c251e",
489
+ "card-foreground": "#f4ece0",
490
+ "popover": "#352c23",
491
+ "popover-foreground": "#f4ece0",
492
+ "primary": "#dcbc92",
493
+ "primary-foreground": "#211c17",
494
+ "secondary": "#3c3227",
495
+ "secondary-foreground": "#f4ece0",
496
+ "muted": "#352d24",
497
+ "muted-foreground": "#c9b9a5",
498
+ "accent": "#4a3b2b",
499
+ "accent-foreground": "#f4ece0",
500
+ "destructive": "#fb8990",
501
+ "border": "#64503e",
502
+ "input": "#79614b",
503
+ "ring": "#dcbc92",
504
+ "chart-1": "#dcbc92",
505
+ },
506
+ },
507
+ {
508
+ id: "rosewood",
509
+ name: "Rosewood",
510
+ light: {
511
+ "background": "#fbf6f5",
512
+ "foreground": "#3b262c",
513
+ "card": "#fffafa",
514
+ "card-foreground": "#3b262c",
515
+ "popover": "#fffafa",
516
+ "popover-foreground": "#3b262c",
517
+ "primary": "#854252",
518
+ "primary-foreground": "#ffffff",
519
+ "secondary": "#f0e2e5",
520
+ "secondary-foreground": "#3b262c",
521
+ "muted": "#f5eaec",
522
+ "muted-foreground": "#76525d",
523
+ "accent": "#edd6dd",
524
+ "accent-foreground": "#3b262c",
525
+ "destructive": "#a52a34",
526
+ "border": "#dfc5cc",
527
+ "input": "#d0afb9",
528
+ "ring": "#854252",
529
+ "chart-1": "#854252",
530
+ },
531
+ dark: {
532
+ "background": "#241a20",
533
+ "foreground": "#f6e8ed",
534
+ "card": "#30222a",
535
+ "card-foreground": "#f6e8ed",
536
+ "popover": "#3a2932",
537
+ "popover-foreground": "#f6e8ed",
538
+ "primary": "#e3a5b7",
539
+ "primary-foreground": "#241a20",
540
+ "secondary": "#442e38",
541
+ "secondary-foreground": "#f6e8ed",
542
+ "muted": "#3a2932",
543
+ "muted-foreground": "#d2b4c0",
544
+ "accent": "#553441",
545
+ "accent-foreground": "#f6e8ed",
546
+ "destructive": "#fb8990",
547
+ "border": "#70505d",
548
+ "input": "#856170",
549
+ "ring": "#e3a5b7",
550
+ "chart-1": "#e3a5b7",
551
+ },
552
+ },
553
+ {
554
+ id: "aurora",
555
+ name: "Aurora",
556
+ light: {
557
+ "background": "#f2faf8",
558
+ "foreground": "#193a37",
559
+ "card": "#fafffe",
560
+ "card-foreground": "#193a37",
561
+ "popover": "#fafffe",
562
+ "popover-foreground": "#193a37",
563
+ "primary": "#216c63",
564
+ "primary-foreground": "#ffffff",
565
+ "secondary": "#def0eb",
566
+ "secondary-foreground": "#193a37",
567
+ "muted": "#e6f2ef",
568
+ "muted-foreground": "#456c66",
569
+ "accent": "#eee5f6",
570
+ "accent-foreground": "#193a37",
571
+ "destructive": "#a52a34",
572
+ "border": "#bbd7d1",
573
+ "input": "#a7c8c0",
574
+ "ring": "#216c63",
575
+ "chart-1": "#216c63",
576
+ },
577
+ dark: {
578
+ "background": "#142321",
579
+ "foreground": "#e1f5ef",
580
+ "card": "#1d302d",
581
+ "card-foreground": "#e1f5ef",
582
+ "popover": "#253d38",
583
+ "popover-foreground": "#e1f5ef",
584
+ "primary": "#87d5c1",
585
+ "primary-foreground": "#142321",
586
+ "secondary": "#29443e",
587
+ "secondary-foreground": "#e1f5ef",
588
+ "muted": "#243c36",
589
+ "muted-foreground": "#a9cfc3",
590
+ "accent": "#3d3352",
591
+ "accent-foreground": "#e1f5ef",
592
+ "destructive": "#fb8990",
593
+ "border": "#466b60",
594
+ "input": "#568172",
595
+ "ring": "#87d5c1",
596
+ "chart-1": "#87d5c1",
597
+ },
598
+ },
599
+ {
600
+ id: "midnight",
601
+ name: "Midnight",
602
+ light: {
603
+ "background": "#f5f6fb",
604
+ "foreground": "#282e48",
605
+ "card": "#fcfcff",
606
+ "card-foreground": "#282e48",
607
+ "popover": "#fcfcff",
608
+ "popover-foreground": "#282e48",
609
+ "primary": "#4f5f96",
610
+ "primary-foreground": "#ffffff",
611
+ "secondary": "#e5e8f4",
612
+ "secondary-foreground": "#282e48",
613
+ "muted": "#eceef7",
614
+ "muted-foreground": "#565f7d",
615
+ "accent": "#dfe4f6",
616
+ "accent-foreground": "#282e48",
617
+ "destructive": "#a52a34",
618
+ "border": "#c7cee5",
619
+ "input": "#b4bfdc",
620
+ "ring": "#4f5f96",
621
+ "chart-1": "#4f5f96",
622
+ },
623
+ dark: {
624
+ "background": "#151a2b",
625
+ "foreground": "#e8ecfa",
626
+ "card": "#1f263b",
627
+ "card-foreground": "#e8ecfa",
628
+ "popover": "#29314a",
629
+ "popover-foreground": "#e8ecfa",
630
+ "primary": "#acbbed",
631
+ "primary-foreground": "#151a2b",
632
+ "secondary": "#303954",
633
+ "secondary-foreground": "#e8ecfa",
634
+ "muted": "#28314b",
635
+ "muted-foreground": "#b1bedf",
636
+ "accent": "#3a466a",
637
+ "accent-foreground": "#e8ecfa",
638
+ "destructive": "#fb8990",
639
+ "border": "#48577d",
640
+ "input": "#596990",
641
+ "ring": "#acbbed",
642
+ "chart-1": "#acbbed",
643
+ },
644
+ },
414
645
  ]
415
646
 
416
647
  /** Colours for a preview swatch, per mode: the default's own values by hand, the rest from the theme. */
@@ -276,78 +276,6 @@ export interface PlannerSeedInfo {
276
276
  body: string;
277
277
  }
278
278
 
279
- /** The Metrics tab's KPI row as data: runs, success, average run, cost, tasks. */
280
- export interface MetricsTiles {
281
- runs: number;
282
- successes: number;
283
- stalls: number;
284
- avgMs: number;
285
- p90Ms: number;
286
- cost: number;
287
- completed: number;
288
- active: number;
289
- avgCompleteMs: number;
290
- }
291
-
292
- /** One per-model group as `metrics.get` carries it. */
293
- export interface MetricGroupInfo {
294
- model: string;
295
- thinking: string;
296
- kinds: string[];
297
- runs: number;
298
- successes: number;
299
- timeouts: number;
300
- avgMs: number;
301
- p50Ms: number;
302
- p90Ms: number;
303
- avgTurns: number;
304
- avgTools: number;
305
- avgTokens: number;
306
- avgCost: number;
307
- totalCost: number;
308
- tokensPerSecond: number;
309
- }
310
-
311
- /** One agent's share of all run time. */
312
- export interface MetricsAgentShare {
313
- agent: string;
314
- runs: number;
315
- ms: number;
316
- share: number;
317
- }
318
-
319
- /** Completed tasks' request-to-done time by the oracle's model and thinking. */
320
- export interface MetricsTaskTime {
321
- model: string;
322
- thinking: string;
323
- tasks: number;
324
- avgMs: number;
325
- }
326
-
327
- /** What the classifier did and spared, when it has run. */
328
- export interface MetricsClassifier {
329
- calls: number;
330
- ok: number;
331
- p50Ms: number;
332
- p90Ms: number;
333
- input: number;
334
- byPurpose: Record<string, number>;
335
- seatRunsSkipped: number;
336
- questionsAnswered: number;
337
- quickFixesHeld: number;
338
- routed: number;
339
- routedOk: number;
340
- }
341
-
342
- /** `metrics.get`: the dashboard figures for one grouping and search. */
343
- export interface MetricsData {
344
- tiles: MetricsTiles;
345
- groups: MetricGroupInfo[];
346
- daily: Array<{ date: string; runs: number; successes: number; cost: number }>;
347
- timeShare: { byAgent: MetricsAgentShare[]; taskTimes: MetricsTaskTime[] };
348
- classifier?: MetricsClassifier;
349
- }
350
-
351
279
  /** Who a knowledge file belongs to. */
352
280
  export type KnowledgeAgentName = "master" | "designer" | "backend" | "qa";
353
281
 
@@ -393,27 +321,10 @@ export interface EntryRefInfo {
393
321
  occurrence: number;
394
322
  }
395
323
 
396
- /** Who an Excalidraw session can be assigned to. */
397
- export type ExcalidrawAgentName = "master" | "designer" | "backend" | "qa" | "scout" | "researcher" | "quickfix" | "planner";
398
-
399
- /** One shared session with its key hidden (`room <id>`). */
400
- export interface ExcalidrawSessionInfo {
401
- id: string;
402
- name: string;
403
- masked: string;
404
- agents: ExcalidrawAgentName[];
405
- contribute: boolean;
406
- addedAt: string;
407
- }
408
-
409
- /** What the last check of a session found. */
410
- export interface ExcalidrawCheck {
411
- ok: boolean;
412
- text: string;
413
- }
414
-
415
324
  /** `tasks.get`: one task read whole, in the terminal's wording. */
416
325
  export interface TaskDetail {
326
+ canAccept: boolean;
327
+ canForceClose: boolean;
417
328
  timing?: TimingProjection;
418
329
  delivery?: Delivery;
419
330
  /** The full request, when it says more than the title. */
@@ -433,7 +344,7 @@ export interface TaskDetail {
433
344
  work?: WorkClock;
434
345
  amendments: string[];
435
346
  /** Approvals waiting on you: `kind` plus `for <domain>: <detail>`. */
436
- waiting: Array<{ kind: string; detail: string }>;
347
+ waiting: Array<{ id: string; domain: string; kind: string; detail: string }>;
437
348
  blockers: Array<{ reason: string; need: string }>;
438
349
  /** The last six runs, one line each (`✓ DEV worker · 3m 12s · …`). */
439
350
  runs: string[];
@@ -608,6 +519,9 @@ export interface Api {
608
519
  "tasks.get": { request: { taskId: string }; result: TaskDetail };
609
520
  "tasks.open": { request: { taskId: string }; result: { sessionId?: string; key?: string; notice?: string } };
610
521
  "tasks.resume": { request: { taskId: string }; result: { notice: string; key?: string } };
522
+ "tasks.accept": { request: { taskId: string }; result: { notice: string } };
523
+ "tasks.forceClose": { request: { taskId: string; title: string }; result: { notice: string } };
524
+ "tasks.resolveApproval": { request: { taskId: string; approvalId: string; decision: "approved" | "rejected"; note?: string }; result: { notice: string } };
611
525
  "tasks.deliveryReview": { request: { taskId: string }; result: { delivery: Delivery } };
612
526
  "tasks.deliveryDefer": { request: { taskId: string; reviewId: string }; result: { delivery: Delivery } };
613
527
  "tasks.deliver": { request: { taskId: string; reviewId: string; action: DeliveryAction; confirmMain?: boolean }; result: { delivery: Delivery } };
@@ -643,7 +557,6 @@ export interface Api {
643
557
  "quickfix.cancel": { request: { id: string }; result: { notice: string } };
644
558
  "quickfix.runAnyway": { request: { id: string }; result: { notice: string } };
645
559
  "quickfix.movedToTask": { request: { id: string }; result: { notice: string; key?: string } };
646
- "metrics.get": { request: { groupBy: "model" | "model-kind"; query?: string; from?: string; to?: string; timeZone?: string }; result: MetricsData };
647
560
  "projects.browse": { request: { path?: string }; result: FolderListing };
648
561
  "projects.open": { request: { path: string }; result: { project: ProjectInfo } };
649
562
  "knowledge.files": { request: Record<string, never>; result: { files: KnowledgeFileInfo[]; models?: Partial<Record<KnowledgeAgentName, string>> } };
@@ -655,16 +568,6 @@ export interface Api {
655
568
  "knowledge.replaceFile": { request: { agent: KnowledgeAgentName; file: string; text: string }; result: { notice: string } };
656
569
  "knowledge.comment": { request: { agent: KnowledgeAgentName; file: string; ref: EntryRefInfo; text: string }; result: { notice: string } };
657
570
  "knowledge.unnote": { request: { id: string }; result: { notice: string } };
658
- "excalidraw.list": { request: Record<string, never>; result: { sessions: ExcalidrawSessionInfo[] } };
659
- "excalidraw.add": { request: { link: string; name?: string }; result: { notice: string; id?: string } };
660
- "excalidraw.create": { request: { name?: string }; result: { notice: string; id?: string } };
661
- "excalidraw.remove": { request: { id: string }; result: { notice: string } };
662
- "excalidraw.rename": { request: { id: string; name: string }; result: { notice: string } };
663
- "excalidraw.toggleAgent": { request: { id: string; agent: ExcalidrawAgentName }; result: { notice: string } };
664
- "excalidraw.toggleAll": { request: { id: string }; result: { notice: string } };
665
- "excalidraw.toggleContribute": { request: { id: string }; result: { notice: string } };
666
- "excalidraw.check": { request: { id: string }; result: ExcalidrawCheck };
667
- "excalidraw.reveal": { request: { id: string }; result: { link: string } };
668
571
  "git.pulls": { request: { refresh?: boolean }; result: GitPulls };
669
572
  "git.pull": { request: { number: number }; result: GitPull };
670
573
  "git.review": { request: { number: number; focus?: string }; result: { notice: string } };