portable-agent-layer 0.72.0 → 0.74.0

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 (69) hide show
  1. package/package.json +10 -7
  2. package/src/cli/index.ts +21 -2
  3. package/src/cli/server.ts +33 -0
  4. package/src/hooks/handlers/agenda.ts +195 -7
  5. package/src/hooks/handlers/update-check.ts +1 -1
  6. package/src/hooks/lib/agenda-store.ts +2 -0
  7. package/src/hooks/lib/goal-links.ts +83 -0
  8. package/src/hooks/lib/models.ts +1 -1
  9. package/src/hooks/lib/projects.ts +5 -0
  10. package/src/hooks/lib/settings.ts +2 -0
  11. package/src/hooks/lib/telos-goals.ts +8 -2
  12. package/src/hooks/lib/token-usage.ts +2 -0
  13. package/src/tools/agent/project.ts +22 -20
  14. package/src/tools/control-room/attention.ts +146 -0
  15. package/src/tools/control-room/data.ts +76 -5
  16. package/src/tools/control-room/detail.ts +158 -0
  17. package/src/tools/control-room/matrix.ts +91 -19
  18. package/src/tools/control-room/prefs.ts +96 -0
  19. package/src/tools/control-room/server-config.ts +8 -0
  20. package/src/tools/control-room/server.ts +196 -11
  21. package/src/tools/control-room/snooze.ts +65 -0
  22. package/src/tools/control-room/static.ts +68 -0
  23. package/src/tools/control-room/ui/app.tsx +196 -50
  24. package/src/tools/control-room/ui/attention-bell.tsx +118 -0
  25. package/src/tools/control-room/ui/components/README.md +18 -0
  26. package/src/tools/control-room/ui/components/badge.tsx +38 -0
  27. package/src/tools/control-room/ui/components/button.tsx +43 -0
  28. package/src/tools/control-room/ui/components/card.tsx +45 -0
  29. package/src/tools/control-room/ui/components/input.tsx +24 -0
  30. package/src/tools/control-room/ui/components/label.tsx +18 -0
  31. package/src/tools/control-room/ui/components/popover.tsx +37 -0
  32. package/src/tools/control-room/ui/components/separator.tsx +25 -0
  33. package/src/tools/control-room/ui/components/skeleton.tsx +14 -0
  34. package/src/tools/control-room/ui/components/switch.tsx +27 -0
  35. package/src/tools/control-room/ui/components/table.tsx +60 -0
  36. package/src/tools/control-room/ui/components/toggle-group.tsx +38 -0
  37. package/src/tools/control-room/ui/dist/assets/index-BoQjFpzV.js +49 -0
  38. package/src/tools/control-room/ui/dist/assets/index-Dncp2bYg.css +2 -0
  39. package/src/tools/control-room/ui/dist/index.html +19 -0
  40. package/src/tools/control-room/ui/format.ts +0 -12
  41. package/src/tools/control-room/ui/frame.tsx +125 -0
  42. package/src/tools/control-room/ui/index.html +2 -2
  43. package/src/tools/control-room/ui/lib/api.ts +27 -0
  44. package/src/tools/control-room/ui/lib/cn.ts +6 -0
  45. package/src/tools/control-room/ui/lib/write.ts +43 -0
  46. package/src/tools/control-room/ui/package.json +28 -0
  47. package/src/tools/control-room/ui/parts.tsx +235 -0
  48. package/src/tools/control-room/ui/screens/log.tsx +274 -0
  49. package/src/tools/control-room/ui/screens/project-detail.tsx +302 -0
  50. package/src/tools/control-room/ui/screens/projects.tsx +128 -0
  51. package/src/tools/control-room/ui/screens/settings.tsx +205 -0
  52. package/src/tools/control-room/ui/screens/today.tsx +391 -0
  53. package/src/tools/control-room/ui/theme.css +127 -0
  54. package/src/tools/control-room/ui/vite.config.ts +36 -0
  55. package/src/tools/control-room/writes.ts +106 -0
  56. package/src/tools/ledger/outcomes.ts +9 -0
  57. package/src/tools/ledger/query.ts +2 -0
  58. package/src/tools/ledger/view.ts +31 -10
  59. package/src/tools/lib/handoff-note.ts +14 -0
  60. package/src/tools/lib/project-isc.ts +61 -0
  61. package/src/tools/control-room/ui/agenda.tsx +0 -43
  62. package/src/tools/control-room/ui/agents.tsx +0 -67
  63. package/src/tools/control-room/ui/app.css +0 -857
  64. package/src/tools/control-room/ui/board.tsx +0 -82
  65. package/src/tools/control-room/ui/handoffs.tsx +0 -37
  66. package/src/tools/control-room/ui/ledger.tsx +0 -137
  67. package/src/tools/control-room/ui/matrix.tsx +0 -117
  68. package/src/tools/control-room/ui/panel.tsx +0 -60
  69. package/src/tools/control-room/ui/signal.tsx +0 -161
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Industry, as Tailwind sees it. Source file — `bun run build:ui` compiles this
3
+ * to tailwind.css, which is what index.html actually links.
4
+ */
5
+
6
+ @layer theme, base, components, utilities;
7
+ @import "tailwindcss/theme.css" layer(theme);
8
+ @import "tailwindcss/preflight.css" layer(base);
9
+ @import "tailwindcss/utilities.css" layer(utilities) source(none);
10
+
11
+ @source "./*.tsx";
12
+ @source "./**/*.tsx";
13
+ @source "./index.html";
14
+
15
+ @theme static {
16
+ --color-bg: #f2f2f3;
17
+ --color-surface: #e9e9ea;
18
+ --color-ink: #1d1f20;
19
+ --color-divider: color-mix(in srgb, #1d1f20 16%, transparent);
20
+
21
+ --color-neutral-100: #f5f5f8;
22
+ --color-neutral-200: #e7e7ea;
23
+ --color-neutral-300: #d4d4d7;
24
+ --color-neutral-400: #b7b7ba;
25
+ --color-neutral-500: #98989b;
26
+ --color-neutral-600: #7a7a7d;
27
+ --color-neutral-700: #5d5d60;
28
+ --color-neutral-800: #424244;
29
+ --color-neutral-900: #2b2b2d;
30
+
31
+ --color-accent: #5980a6;
32
+ --color-accent-100: #eef6ff;
33
+ --color-accent-200: #d6ebff;
34
+ --color-accent-300: #b5d9fd;
35
+ --color-accent-400: #94bce3;
36
+ --color-accent-500: #749dc4;
37
+ --color-accent-600: #597ea3;
38
+ --color-accent-700: #416180;
39
+ --color-accent-800: #2c455d;
40
+ --color-accent-900: #1d2d3d;
41
+
42
+ --color-alarm: oklch(0.55 0.17 27);
43
+
44
+ --font-heading: "Barlow Condensed", system-ui, sans-serif;
45
+ --font-body: "Barlow", system-ui, sans-serif;
46
+ --font-mono: ui-monospace, Menlo, monospace;
47
+
48
+ /* Industry's --space-N is exactly N x 3.4px, which is Tailwind's own scale
49
+ with a retuned base — so p-4 here is Industry's --space-4 to the pixel. */
50
+ --spacing: 0.2125rem;
51
+
52
+ --radius-sm: 2px;
53
+ --radius-md: 4px;
54
+ --radius-lg: 7px;
55
+
56
+ --shadow-sm: 0 1px 2px color-mix(in srgb, #2b2b2d 14%, transparent);
57
+ --shadow-md: 0 3px 10px color-mix(in srgb, #2b2b2d 16%, transparent);
58
+ --shadow-lg: 0 12px 32px color-mix(in srgb, #2b2b2d 22%, transparent);
59
+
60
+ /* Industry draws an 11px crosshair in 55% ink whose centre sits on the
61
+ border line — .corner is 11px at top/left -6px over a 1px border. */
62
+ --registration-size: 11px;
63
+ --registration-inset: -6px;
64
+ --registration-mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='11'%3E%3Cpath d='M5.5 0v11M0 5.5h11' stroke='%231d1f20' stroke-opacity='.55' stroke-width='1'/%3E%3C/svg%3E");
65
+ }
66
+
67
+ @layer base {
68
+ body {
69
+ background: var(--color-bg);
70
+ color: var(--color-ink);
71
+ font-family: var(--font-body);
72
+ font-size: 13px;
73
+ line-height: 1.45;
74
+ }
75
+
76
+ h1,
77
+ h2,
78
+ h3,
79
+ h4,
80
+ h5,
81
+ h6 {
82
+ font-family: var(--font-heading);
83
+ font-weight: 600;
84
+ letter-spacing: 0.02em;
85
+ }
86
+
87
+ :focus-visible {
88
+ outline: 2px solid var(--color-accent);
89
+ outline-offset: 2px;
90
+ }
91
+
92
+ ::selection {
93
+ background: var(--color-accent-200);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * The wireframe frame every card, figure and primary button wears: square
99
+ * corners, a hairline border and four "+" registration marks drawn outside the
100
+ * box. Industry ships it as a class plus four <i> children; here the marks are
101
+ * one pseudo-element, so the markup stays clean.
102
+ */
103
+ @utility blueprint {
104
+ position: relative;
105
+ border: 1px solid var(--color-divider);
106
+ border-radius: 0;
107
+
108
+ &::after {
109
+ content: "";
110
+ position: absolute;
111
+ inset: var(--registration-inset);
112
+ pointer-events: none;
113
+ background-image: var(--registration-mark), var(--registration-mark),
114
+ var(--registration-mark), var(--registration-mark);
115
+ background-repeat: no-repeat;
116
+ background-size: var(--registration-size) var(--registration-size);
117
+ background-position: left top, right top, left bottom, right bottom;
118
+ }
119
+ }
120
+
121
+ /** Industry's uppercase eyebrow, used for every panel heading and column label. */
122
+ @utility eyebrow {
123
+ font-size: 10px;
124
+ letter-spacing: 0.12em;
125
+ text-transform: uppercase;
126
+ color: var(--color-neutral-700);
127
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The control room's build. Bun serves the page and owns every API route, but
3
+ * Bun's CSS bundler passes Tailwind's directives through as unknown at-rules —
4
+ * so the page is built here, by the first-party plugin that moves in lockstep
5
+ * with Tailwind itself, and Bun serves the result out of dist/.
6
+ *
7
+ * `server.proxy` is what makes `bun run dev:ui` usable: the page comes from Vite
8
+ * with hot reload while every /api call goes to the real control room.
9
+ */
10
+
11
+ import tailwindcss from "@tailwindcss/vite";
12
+ import react from "@vitejs/plugin-react";
13
+ import { defineConfig } from "vite";
14
+
15
+ /**
16
+ * Mirrors DEFAULT_PORT in ../server-config.ts. A config the loader reads before
17
+ * TypeScript resolution cannot import from the source tree, so a test asserts
18
+ * the two agree.
19
+ */
20
+ const API_ORIGIN = "http://127.0.0.1:7250";
21
+
22
+ export default defineConfig({
23
+ root: import.meta.dirname,
24
+ base: "/",
25
+ plugins: [react(), tailwindcss()],
26
+ build: {
27
+ outDir: "dist",
28
+ emptyOutDir: true,
29
+ sourcemap: false,
30
+ },
31
+ server: {
32
+ proxy: {
33
+ "/api": API_ORIGIN,
34
+ },
35
+ },
36
+ });
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The three things the page may change without an agent.
3
+ *
4
+ * Each one is a single named field on a single record, reached through the same
5
+ * rule the CLI uses — the page is a second hand on the same instrument, not a
6
+ * write surface over ~/.pal.
7
+ */
8
+
9
+ import { readProject, writeProject } from "../../hooks/lib/projects";
10
+ import {
11
+ raw as rawSettings,
12
+ reload,
13
+ write as writeSettings,
14
+ } from "../../hooks/lib/settings";
15
+ import { completeIsc, type IscSections, reopenIsc } from "../lib/project-isc";
16
+
17
+ export type WriteOutcome =
18
+ | { ok: true; changed: boolean }
19
+ | { ok: false; status: number; error: string };
20
+
21
+ const missing = (project: string): WriteOutcome => ({
22
+ ok: false,
23
+ status: 404,
24
+ error: `no such project: ${project}`,
25
+ });
26
+
27
+ export function setIscStatus(project: string, id: number, close: boolean): WriteOutcome {
28
+ const p = readProject(project);
29
+ if (!p) return missing(project);
30
+ const sections: IscSections = {
31
+ criteria: p.criteria ?? "",
32
+ changelog: p.changelog ?? "",
33
+ };
34
+ const move = close ? completeIsc(sections, id) : reopenIsc(sections, id);
35
+ if (!move.ok) return { ok: false, status: 404, error: move.reason };
36
+ if (move.already) return { ok: true, changed: false };
37
+ p.criteria = move.criteria;
38
+ p.changelog = move.changelog;
39
+ p.updated = new Date().toISOString();
40
+ writeProject(p);
41
+ return { ok: true, changed: true };
42
+ }
43
+
44
+ export const QUADRANTS = ["now", "plan", "noise", "later"] as const;
45
+ export type Quadrant = (typeof QUADRANTS)[number];
46
+
47
+ export function isQuadrant(value: unknown): value is Quadrant {
48
+ return typeof value === "string" && (QUADRANTS as readonly string[]).includes(value);
49
+ }
50
+
51
+ /**
52
+ * Importance already has a home in `serves`; urgency is read off the files.
53
+ * A placement overrules the pair, and says who decided, so the next guess can
54
+ * run freely without overwriting the answer.
55
+ */
56
+ export function setPlacement(project: string, quadrant: Quadrant | null): WriteOutcome {
57
+ const p = readProject(project);
58
+ if (!p) return missing(project);
59
+ if (quadrant) {
60
+ p.placed = quadrant;
61
+ p.placed_by = "user";
62
+ } else {
63
+ p.placed = undefined;
64
+ p.placed_by = undefined;
65
+ }
66
+ p.updated = new Date().toISOString();
67
+ writeProject(p);
68
+ return { ok: true, changed: true };
69
+ }
70
+
71
+ export interface InstallSettings {
72
+ actor: string;
73
+ timezone: string;
74
+ }
75
+
76
+ export function readInstallSettings(): InstallSettings {
77
+ const identity = rawSettings().identity ?? {};
78
+ return {
79
+ actor: identity.principal?.name ?? "",
80
+ timezone: identity.principal?.timezone ?? "",
81
+ };
82
+ }
83
+
84
+ /** Only the two fields the Settings panel offers; everything else is left alone. */
85
+ export function writeInstallSettings(update: Partial<InstallSettings>): WriteOutcome {
86
+ if (update.timezone && !isTimezone(update.timezone)) {
87
+ return { ok: false, status: 400, error: `not a timezone: ${update.timezone}` };
88
+ }
89
+ const data = rawSettings();
90
+ const identity = data.identity ?? {};
91
+ const principal = { ...identity.principal };
92
+ if (update.actor !== undefined) principal.name = update.actor;
93
+ if (update.timezone !== undefined) principal.timezone = update.timezone;
94
+ writeSettings({ ...data, identity: { ...identity, principal } });
95
+ reload();
96
+ return { ok: true, changed: true };
97
+ }
98
+
99
+ function isTimezone(value: string): boolean {
100
+ try {
101
+ new Intl.DateTimeFormat("en-US", { timeZone: value });
102
+ return true;
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The outcomes a page groups by. Kept apart from view.ts because the browser
3
+ * needs this list and nothing else from that module — importing it there would
4
+ * drag the actor and machine registries, and node:fs behind them, into the
5
+ * page bundle.
6
+ */
7
+
8
+ export const PAGE_OUTCOMES = ["applied", "failed", "denied", "blocked"] as const;
9
+ export type PageOutcome = (typeof PAGE_OUTCOMES)[number];
@@ -34,6 +34,7 @@ export interface LedgerFilter {
34
34
  actor?: string;
35
35
  machine?: string;
36
36
  runtime?: string;
37
+ authority?: string;
37
38
  outcome?: string;
38
39
  tool?: string;
39
40
  target?: string;
@@ -100,6 +101,7 @@ function matchesFilter(entry: LedgerEntry, filter: LedgerFilter): boolean {
100
101
  if (filter.actor && entry.actor !== filter.actor) return false;
101
102
  if (filter.machine && entry.machine !== filter.machine) return false;
102
103
  if (filter.runtime && entry.runtime !== filter.runtime) return false;
104
+ if (filter.authority && entry.authority !== filter.authority) return false;
103
105
  if (filter.outcome && entry.outcome !== filter.outcome) return false;
104
106
  if (filter.tool && entry.tool.toLowerCase() !== filter.tool.toLowerCase()) return false;
105
107
  if (filter.target && !entry.target.includes(filter.target)) return false;
@@ -12,10 +12,16 @@ import {
12
12
  readActorRegistry,
13
13
  } from "../../hooks/lib/actor";
14
14
  import type { LedgerEntry } from "../../hooks/lib/ledger";
15
+ import {
16
+ loadMachine,
17
+ displayName as machineDisplayName,
18
+ type RegistryEntry,
19
+ readRegistry as readMachineRegistry,
20
+ } from "../../hooks/lib/machine";
21
+ import { PAGE_OUTCOMES, type PageOutcome } from "./outcomes";
15
22
  import { anchorSlugOf, changedLines, type LedgerFilter, queryLedger } from "./query";
16
23
 
17
- export const PAGE_OUTCOMES = ["applied", "failed", "denied", "blocked"] as const;
18
- export type PageOutcome = (typeof PAGE_OUTCOMES)[number];
24
+ export { PAGE_OUTCOMES, type PageOutcome };
19
25
 
20
26
  export interface OutcomeCount {
21
27
  total: number;
@@ -32,6 +38,8 @@ export interface LedgerViewStats {
32
38
  export interface LedgerViewRow {
33
39
  id: string;
34
40
  ts: string;
41
+ /** Where the record was written. Local-only in the record; the page names it. */
42
+ machine: string;
35
43
  actor: string;
36
44
  authority: string;
37
45
  runtime: string;
@@ -89,11 +97,17 @@ export function displayTarget(target: string): string {
89
97
  return rest ? `${slug} ${rest}` : slug;
90
98
  }
91
99
 
92
- function toRow(entry: LedgerEntry, registry: ActorRegistryEntry[]): LedgerViewRow {
100
+ interface Names {
101
+ actors: ActorRegistryEntry[];
102
+ machines: RegistryEntry[];
103
+ }
104
+
105
+ function toRow(entry: LedgerEntry, { actors, machines }: Names): LedgerViewRow {
93
106
  const row: LedgerViewRow = {
94
107
  id: entry.id,
95
108
  ts: entry.ts,
96
- actor: actorDisplayName(entry.actor, registry),
109
+ machine: machineDisplayName(entry.machine, machines),
110
+ actor: actorDisplayName(entry.actor, actors),
97
111
  authority: entry.authority,
98
112
  runtime: entry.runtime,
99
113
  tool: entry.tool,
@@ -106,18 +120,25 @@ function toRow(entry: LedgerEntry, registry: ActorRegistryEntry[]): LedgerViewRo
106
120
  return row;
107
121
  }
108
122
 
109
- /** The local actor first: on a fresh install the registry may not list them yet. */
110
- function knownActors(): ActorRegistryEntry[] {
111
- const self = loadActor();
112
- return [{ id: self.id, label: self.label }, ...readActorRegistry()];
123
+ /** This install first: on a fresh machine the registries may not list it yet. */
124
+ function knownNames(): Names {
125
+ const actor = loadActor();
126
+ const machine = loadMachine();
127
+ return {
128
+ actors: [{ id: actor.id, label: actor.label }, ...readActorRegistry()],
129
+ machines: [
130
+ { id: machine.id, label: machine.label, os: machine.os },
131
+ ...readMachineRegistry(),
132
+ ],
133
+ };
113
134
  }
114
135
 
115
136
  export function viewRows(entries: LedgerEntry[]): LedgerViewRow[] {
116
- const registry = knownActors();
137
+ const names = knownNames();
117
138
  return entries
118
139
  .slice()
119
140
  .reverse()
120
- .map((entry) => toRow(entry, registry));
141
+ .map((entry) => toRow(entry, names));
121
142
  }
122
143
 
123
144
  export function ledgerView(filter: LedgerFilter = {}): LedgerView {
@@ -86,3 +86,17 @@ export function recordNote(
86
86
  ): HandoffStore {
87
87
  return trimHandoffs({ ...store, [note.cwd]: entryFor(note, now) });
88
88
  }
89
+
90
+ /**
91
+ * Fills in the one field an auto handoff cannot write for itself. Never
92
+ * overwrites an answer already on the entry, and never invents an entry.
93
+ */
94
+ export function withWaitingOn(
95
+ store: HandoffStore,
96
+ cwd: string,
97
+ question: string
98
+ ): HandoffStore {
99
+ const entry = store[cwd];
100
+ if (!entry || entry.waitingOn) return store;
101
+ return { ...store, [cwd]: { ...entry, waitingOn: question } };
102
+ }
@@ -140,6 +140,67 @@ export function selectIscs(
140
140
  return open;
141
141
  }
142
142
 
143
+ export interface IscSections {
144
+ criteria: string;
145
+ changelog: string;
146
+ }
147
+
148
+ export type IscMove =
149
+ | ({ ok: true; already: boolean } & IscSections)
150
+ | { ok: false; reason: string };
151
+
152
+ /**
153
+ * Closing an ISC moves its line out of Criteria and files it under a dated
154
+ * archive heading in the Changelog. Reopening walks it back. Both are pure so
155
+ * the CLI and the control room reach the same record through one rule.
156
+ */
157
+ export function completeIsc(
158
+ sections: IscSections,
159
+ id: number,
160
+ now: Date = new Date()
161
+ ): IscMove {
162
+ const kept = { criteria: sections.criteria, changelog: sections.changelog };
163
+ if (parseIscs(sections.changelog).some((i) => i.id === id)) {
164
+ return { ok: true, already: true, ...kept };
165
+ }
166
+ const { line, rest } = removeIscLine(sections.criteria, id);
167
+ if (!line) return { ok: false, reason: `ISC-${id} not found` };
168
+ return {
169
+ ok: true,
170
+ already: false,
171
+ criteria: rest,
172
+ changelog: archiveLine(
173
+ sections.changelog,
174
+ line.replace("[ ]", "[x]"),
175
+ "Archived",
176
+ now
177
+ ),
178
+ };
179
+ }
180
+
181
+ export function reopenIsc(sections: IscSections, id: number): IscMove {
182
+ const kept = { criteria: sections.criteria, changelog: sections.changelog };
183
+ if (parseIscs(sections.criteria).some((i) => i.id === id && i.status === "open")) {
184
+ return { ok: true, already: true, ...kept };
185
+ }
186
+ let changelog = sections.changelog;
187
+ let criteria = sections.criteria;
188
+ let removed = removeIscLine(changelog, id);
189
+ if (removed.line) changelog = dropEmptyArchiveHeadings(removed.rest);
190
+ else {
191
+ removed = removeIscLine(criteria, id);
192
+ if (removed.line) criteria = removed.rest;
193
+ }
194
+ if (!removed.line) return { ok: false, reason: `ISC-${id} not found` };
195
+ const openLine = removed.line.replace(/\[[x~]\]/i, "[ ]");
196
+ return {
197
+ ok: true,
198
+ already: false,
199
+ criteria: criteria ? `${criteria.trimEnd()}\n${openLine}` : openLine,
200
+ changelog,
201
+ };
202
+ }
203
+
143
204
  /** A filesystem-safe stem for a task's own ISA, kept unique by the clock. */
144
205
  export function taskSlug(title: string, now: number = Date.now()): string {
145
206
  const sanitized = title
@@ -1,43 +0,0 @@
1
- import type { AgendaView } from "../data";
2
- import { Pending, useLoaded } from "./panel";
3
-
4
- function Age({ view }: { view: AgendaView }) {
5
- if (!view.generatedAt) return <span className="agenda-age">never written</span>;
6
- const hours = view.ageHours ?? 0;
7
- const text = hours < 1 ? "written just now" : `written ${hours}h ago`;
8
- return (
9
- <span className={view.stale ? "agenda-age stale" : "agenda-age"}>
10
- {view.stale ? `${text} — out of date` : text}
11
- </span>
12
- );
13
- }
14
-
15
- export function Agenda() {
16
- const loaded = useLoaded<AgendaView>("/api/agenda");
17
-
18
- return (
19
- <section className="agenda">
20
- <header>
21
- <h2>Today</h2>
22
- {loaded.state === "ready" && <Age view={loaded.data} />}
23
- </header>
24
- <Pending value={loaded} />
25
- {loaded.state === "ready" &&
26
- (loaded.data.moves.length === 0 ? (
27
- <p className="agenda-empty">
28
- No moves yet. They are written when a session ends — finish one and come back.
29
- </p>
30
- ) : (
31
- <ol className="moves">
32
- {loaded.data.moves.map((move, i) => (
33
- <li key={move.move}>
34
- <span className="rank">{i + 1}</span>
35
- <span className="move">{move.move}</span>
36
- <span className="because">{move.because}</span>
37
- </li>
38
- ))}
39
- </ol>
40
- ))}
41
- </section>
42
- );
43
- }
@@ -1,67 +0,0 @@
1
- import type { AgentsRow, AgentsView } from "../data";
2
- import { Panel, Pending, useLoaded } from "./panel";
3
-
4
- function Row({ r, max }: { r: AgentsRow; max: number }) {
5
- return (
6
- <tr>
7
- <td>
8
- {r.slug}
9
- <span className="bar">
10
- <i style={{ width: `${(r.actions / max) * 100}%` }} />
11
- </span>
12
- </td>
13
- <td className="num">{r.actions}</td>
14
- <td>
15
- <span className="runtimes">
16
- {Object.entries(r.runtimes).map(([runtime, n]) => (
17
- <span key={runtime} className="tag">
18
- {runtime} {n}
19
- </span>
20
- ))}
21
- </span>
22
- </td>
23
- <td className="num">{r.machines}</td>
24
- <td className="num">{r.actors}</td>
25
- <td className="num">{r.sessions}</td>
26
- </tr>
27
- );
28
- }
29
-
30
- export function Agents() {
31
- const view = useLoaded<AgentsView>("/api/agents");
32
- const max =
33
- view.state === "ready" ? Math.max(1, ...view.data.projects.map((p) => p.actions)) : 1;
34
- return (
35
- <Panel
36
- index="04 · activity"
37
- title="Agents at work"
38
- span={7}
39
- order={3}
40
- aside={view.state === "ready" ? `since ${view.data.since.slice(0, 10)}` : ""}
41
- >
42
- <Pending value={view} />
43
- {view.state === "ready" && view.data.projects.length === 0 && (
44
- <div className="empty">No recorded actions in the window.</div>
45
- )}
46
- {view.state === "ready" && view.data.projects.length > 0 && (
47
- <table>
48
- <thead>
49
- <tr>
50
- <th>project</th>
51
- <th className="num">actions</th>
52
- <th>runtimes</th>
53
- <th className="num">machines</th>
54
- <th className="num">actors</th>
55
- <th className="num">sessions</th>
56
- </tr>
57
- </thead>
58
- <tbody>
59
- {view.data.projects.map((r) => (
60
- <Row key={r.slug} r={r} max={max} />
61
- ))}
62
- </tbody>
63
- </table>
64
- )}
65
- </Panel>
66
- );
67
- }