portable-agent-layer 0.71.0 → 0.73.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 (104) hide show
  1. package/package.json +10 -7
  2. package/src/cli/migrate.ts +1 -1
  3. package/src/cli/server.ts +7 -0
  4. package/src/cli/skill.ts +1 -1
  5. package/src/hooks/CompactRecover.ts +28 -86
  6. package/src/hooks/LedgerUnapplied.ts +3 -28
  7. package/src/hooks/LoadContext.ts +33 -60
  8. package/src/hooks/SecurityValidator.ts +16 -109
  9. package/src/hooks/handlers/agenda.ts +195 -7
  10. package/src/hooks/handlers/failure-principle.ts +19 -44
  11. package/src/hooks/handlers/session-intelligence.ts +13 -70
  12. package/src/hooks/lib/agenda-store.ts +2 -0
  13. package/src/hooks/lib/capture-store.ts +103 -0
  14. package/src/hooks/lib/compact-recall.ts +89 -0
  15. package/src/hooks/lib/failure-principle.ts +98 -0
  16. package/src/hooks/lib/goal-links.ts +83 -0
  17. package/src/hooks/lib/ledger-hook.ts +35 -0
  18. package/src/hooks/lib/ledger.ts +48 -1
  19. package/src/hooks/lib/models.ts +1 -1
  20. package/src/hooks/lib/projects.ts +5 -0
  21. package/src/hooks/lib/security-gate.ts +159 -0
  22. package/src/hooks/lib/session-context.ts +74 -0
  23. package/src/hooks/lib/settings.ts +2 -0
  24. package/src/hooks/lib/telos-goals.ts +8 -2
  25. package/src/hooks/lib/token-usage.ts +2 -0
  26. package/src/tools/agent/algorithm-reflect.ts +28 -97
  27. package/src/tools/agent/analyze.ts +19 -120
  28. package/src/tools/agent/handoff-note.ts +29 -77
  29. package/src/tools/agent/project.ts +34 -153
  30. package/src/tools/agent/relationship-note.ts +27 -46
  31. package/src/tools/agent/synthesize.ts +1 -1
  32. package/src/tools/agent/thread.ts +43 -123
  33. package/src/tools/control-room/attention.ts +146 -0
  34. package/src/tools/control-room/data.ts +78 -7
  35. package/src/tools/control-room/detail.ts +158 -0
  36. package/src/tools/control-room/matrix.ts +92 -20
  37. package/src/tools/control-room/prefs.ts +96 -0
  38. package/src/tools/control-room/server-config.ts +8 -0
  39. package/src/tools/control-room/server.ts +196 -11
  40. package/src/tools/control-room/snooze.ts +65 -0
  41. package/src/tools/control-room/static.ts +68 -0
  42. package/src/tools/control-room/ui/app.tsx +196 -50
  43. package/src/tools/control-room/ui/attention-bell.tsx +118 -0
  44. package/src/tools/control-room/ui/components/README.md +18 -0
  45. package/src/tools/control-room/ui/components/badge.tsx +38 -0
  46. package/src/tools/control-room/ui/components/button.tsx +43 -0
  47. package/src/tools/control-room/ui/components/card.tsx +45 -0
  48. package/src/tools/control-room/ui/components/input.tsx +24 -0
  49. package/src/tools/control-room/ui/components/label.tsx +18 -0
  50. package/src/tools/control-room/ui/components/popover.tsx +37 -0
  51. package/src/tools/control-room/ui/components/separator.tsx +25 -0
  52. package/src/tools/control-room/ui/components/skeleton.tsx +14 -0
  53. package/src/tools/control-room/ui/components/switch.tsx +27 -0
  54. package/src/tools/control-room/ui/components/table.tsx +60 -0
  55. package/src/tools/control-room/ui/components/toggle-group.tsx +38 -0
  56. package/src/tools/control-room/ui/dist/assets/index-BoQjFpzV.js +49 -0
  57. package/src/tools/control-room/ui/dist/assets/index-Dncp2bYg.css +2 -0
  58. package/src/tools/control-room/ui/dist/index.html +19 -0
  59. package/src/tools/control-room/ui/format.ts +0 -12
  60. package/src/tools/control-room/ui/frame.tsx +125 -0
  61. package/src/tools/control-room/ui/index.html +2 -2
  62. package/src/tools/control-room/ui/lib/api.ts +27 -0
  63. package/src/tools/control-room/ui/lib/cn.ts +6 -0
  64. package/src/tools/control-room/ui/lib/write.ts +43 -0
  65. package/src/tools/control-room/ui/package.json +28 -0
  66. package/src/tools/control-room/ui/parts.tsx +235 -0
  67. package/src/tools/control-room/ui/screens/log.tsx +274 -0
  68. package/src/tools/control-room/ui/screens/project-detail.tsx +302 -0
  69. package/src/tools/control-room/ui/screens/projects.tsx +128 -0
  70. package/src/tools/control-room/ui/screens/settings.tsx +205 -0
  71. package/src/tools/control-room/ui/screens/today.tsx +391 -0
  72. package/src/tools/control-room/ui/theme.css +127 -0
  73. package/src/tools/control-room/ui/vite.config.ts +36 -0
  74. package/src/tools/control-room/writes.ts +106 -0
  75. package/src/tools/ledger/outcomes.ts +9 -0
  76. package/src/tools/ledger/query.ts +2 -0
  77. package/src/tools/ledger/view.ts +34 -10
  78. package/src/tools/lib/algorithm-reflect.ts +84 -0
  79. package/src/tools/lib/analyze-report.ts +120 -0
  80. package/src/tools/lib/handoff-note.ts +102 -0
  81. package/src/tools/lib/note-flags.ts +59 -0
  82. package/src/tools/lib/project-isc.ts +212 -0
  83. package/src/tools/lib/relationship-reflect.ts +402 -0
  84. package/src/tools/lib/self-model.ts +499 -0
  85. package/src/tools/lib/session-usage.ts +216 -0
  86. package/src/tools/lib/skill-doctor.ts +457 -0
  87. package/src/tools/lib/thread.ts +119 -0
  88. package/src/tools/lib/token-report.ts +173 -0
  89. package/src/tools/lib/transcript-usage.ts +42 -0
  90. package/src/tools/lib/usage-buckets.ts +329 -0
  91. package/src/tools/relationship-reflect.ts +48 -412
  92. package/src/tools/self-model.ts +76 -558
  93. package/src/tools/session-summary.ts +8 -215
  94. package/src/tools/skill-doctor.ts +9 -444
  95. package/src/tools/token-cost.ts +18 -428
  96. package/src/tools/control-room/ui/agenda.tsx +0 -43
  97. package/src/tools/control-room/ui/agents.tsx +0 -67
  98. package/src/tools/control-room/ui/app.css +0 -857
  99. package/src/tools/control-room/ui/board.tsx +0 -82
  100. package/src/tools/control-room/ui/handoffs.tsx +0 -37
  101. package/src/tools/control-room/ui/ledger.tsx +0 -136
  102. package/src/tools/control-room/ui/matrix.tsx +0 -117
  103. package/src/tools/control-room/ui/panel.tsx +0 -60
  104. package/src/tools/control-room/ui/signal.tsx +0 -161
@@ -9,389 +9,29 @@
9
9
  */
10
10
 
11
11
  import { spawnSync } from "node:child_process";
12
- import { existsSync, readdirSync, readFileSync } from "node:fs";
13
12
  import { homedir } from "node:os";
14
13
  import { resolve } from "node:path";
15
14
  import { parseArgs } from "node:util";
16
- import { costOfUsage } from "../hooks/lib/models";
17
15
  import { palHome } from "../hooks/lib/paths";
18
16
  import { findBinaryOnPath } from "../hooks/lib/which";
17
+ import { parseRtkSummary, type RtkGain, usageLines } from "./lib/token-report";
18
+ import { readClaudeCode, readPalInference } from "./lib/usage-buckets";
19
19
 
20
- // ── Types ──
21
-
22
- interface Bucket {
23
- input: number;
24
- output: number;
25
- cacheWrite5m: number;
26
- cacheWrite1h: number;
27
- cacheRead: number;
28
- cost: number;
29
- calls: number;
30
- }
31
-
32
- function emptyBucket(): Bucket {
33
- return {
34
- input: 0,
35
- output: 0,
36
- cacheWrite5m: 0,
37
- cacheWrite1h: 0,
38
- cacheRead: 0,
39
- cost: 0,
40
- calls: 0,
41
- };
42
- }
43
-
44
- interface TimeBuckets {
45
- today: Bucket;
46
- week: Bucket;
47
- month: Bucket;
48
- total: Bucket;
49
- }
50
-
51
- function emptyTimeBuckets(): TimeBuckets {
52
- return {
53
- today: emptyBucket(),
54
- week: emptyBucket(),
55
- month: emptyBucket(),
56
- total: emptyBucket(),
57
- };
58
- }
59
-
60
- // ── Helpers ──
61
-
62
- function addToBucket(
63
- bucket: Bucket,
64
- model: string,
65
- input: number,
66
- output: number,
67
- cacheWrite5m: number,
68
- cacheWrite1h: number,
69
- cacheRead: number
70
- ): void {
71
- bucket.input += input;
72
- bucket.output += output;
73
- bucket.cacheWrite5m += cacheWrite5m;
74
- bucket.cacheWrite1h += cacheWrite1h;
75
- bucket.cacheRead += cacheRead;
76
- bucket.cost += costOfUsage(model, {
77
- input,
78
- output,
79
- cacheWrite5m,
80
- cacheWrite1h,
81
- cacheRead,
82
- });
83
- bucket.calls++;
84
- }
85
-
86
- // ── Formatting ──
87
-
88
- function fmt(n: number): string {
89
- if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
90
- if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
91
- return n.toLocaleString("en-US");
92
- }
93
-
94
- function fmtCost(n: number): string {
95
- if (n >= 1) return `$${n.toFixed(2)}`;
96
- return `$${n.toFixed(4)}`;
97
- }
98
-
99
- function printRow(label: string, b: Bucket, labelWidth = 14): void {
100
- const tokens = b.input + b.output + b.cacheWrite5m + b.cacheWrite1h + b.cacheRead;
101
- console.log(
102
- ` ${label.padEnd(labelWidth)} ${fmt(tokens).padStart(8)} tok ${fmt(b.calls).padStart(5)} calls ${fmtCost(b.cost).padStart(8)}`
103
- );
104
- }
105
-
106
- function printDetailed(label: string, b: Bucket, labelWidth = 14): void {
107
- console.log(
108
- ` ${label.padEnd(labelWidth)} ${fmt(b.input).padStart(8)} in ${fmt(b.output).padStart(8)} out ${fmt(b.cacheWrite5m).padStart(7)} cw5m ${fmt(b.cacheWrite1h).padStart(7)} cw1h ${fmt(b.cacheRead).padStart(8)} cr ${fmtCost(b.cost).padStart(8)}`
109
- );
110
- }
111
-
112
- // ── Claude Code transcripts ──
113
-
114
- function addToTimeBuckets(
115
- tb: TimeBuckets,
116
- ts: string,
117
- model: string,
118
- input: number,
119
- output: number,
120
- cacheWrite5m: number,
121
- cacheWrite1h: number,
122
- cacheRead: number,
123
- todayPrefix: string,
124
- weekAgo: string,
125
- monthAgo: string
126
- ): void {
127
- addToBucket(tb.total, model, input, output, cacheWrite5m, cacheWrite1h, cacheRead);
128
- if (ts >= monthAgo)
129
- addToBucket(tb.month, model, input, output, cacheWrite5m, cacheWrite1h, cacheRead);
130
- if (ts >= weekAgo)
131
- addToBucket(tb.week, model, input, output, cacheWrite5m, cacheWrite1h, cacheRead);
132
- if (ts.startsWith(todayPrefix))
133
- addToBucket(tb.today, model, input, output, cacheWrite5m, cacheWrite1h, cacheRead);
134
- }
135
-
136
- function readClaudeCode(projectFilter?: string): {
137
- buckets: TimeBuckets;
138
- byModel: Record<string, Bucket>;
139
- byProject: Record<string, TimeBuckets>;
140
- } {
141
- const now = new Date();
142
- const todayPrefix = now.toISOString().slice(0, 10);
143
- const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
144
- const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
145
-
146
- const buckets = emptyTimeBuckets();
147
- const byModel: Record<string, Bucket> = {};
148
- const byProject: Record<string, TimeBuckets> = {};
149
-
150
- const claudeDir = resolve(homedir(), ".claude", "projects");
151
- if (!existsSync(claudeDir)) return { buckets, byModel, byProject };
152
-
153
- const projectDirs = readdirSync(claudeDir, { withFileTypes: true })
154
- .filter((d) => d.isDirectory())
155
- .map((d) => d.name);
156
-
157
- for (const projDir of projectDirs) {
158
- const projPath = resolve(claudeDir, projDir);
159
- const segments = projDir.replace(/^-/, "").split("-");
160
- const projName = segments.length > 1 ? segments.slice(-1)[0] : projDir;
161
-
162
- if (typeof projectFilter === "string" && !projName.includes(projectFilter)) continue;
163
-
164
- const jsonlFiles: string[] = [];
165
-
166
- for (const entry of readdirSync(projPath, { withFileTypes: true })) {
167
- if (entry.isFile() && entry.name.endsWith(".jsonl")) {
168
- jsonlFiles.push(resolve(projPath, entry.name));
169
- } else if (entry.isDirectory()) {
170
- const subagentsDir = resolve(projPath, entry.name, "subagents");
171
- try {
172
- for (const sub of readdirSync(subagentsDir)) {
173
- if (sub.endsWith(".jsonl")) {
174
- jsonlFiles.push(resolve(subagentsDir, sub));
175
- }
176
- }
177
- } catch {
178
- /* no subagents dir */
179
- }
180
- }
181
- }
182
-
183
- for (const filepath of jsonlFiles) {
184
- let content: string;
185
- try {
186
- content = readFileSync(filepath, "utf-8");
187
- } catch {
188
- continue;
189
- }
190
-
191
- for (const line of content.split("\n")) {
192
- if (!line.includes('"usage"')) continue;
193
- try {
194
- const d = JSON.parse(line) as {
195
- type?: string;
196
- timestamp?: string;
197
- message?: {
198
- model?: string;
199
- usage?: {
200
- input_tokens?: number;
201
- output_tokens?: number;
202
- cache_creation_input_tokens?: number;
203
- cache_read_input_tokens?: number;
204
- cache_creation?: {
205
- ephemeral_5m_input_tokens?: number;
206
- ephemeral_1h_input_tokens?: number;
207
- };
208
- };
209
- };
210
- };
211
- if (d.type !== "assistant") continue;
212
- const usage = d.message?.usage;
213
- const model = d.message?.model;
214
- const ts = d.timestamp;
215
- if (!usage || !model || !ts) continue;
216
-
217
- const input = usage.input_tokens ?? 0;
218
- const output = usage.output_tokens ?? 0;
219
- const cr = usage.cache_read_input_tokens ?? 0;
220
- const cw5m = usage.cache_creation?.ephemeral_5m_input_tokens;
221
- const cw1h = usage.cache_creation?.ephemeral_1h_input_tokens;
222
- // Older transcripts only have the summed cache_creation_input_tokens — bill as 5m.
223
- const hasBreakdown = cw5m !== undefined || cw1h !== undefined;
224
- const cacheWrite5m = hasBreakdown
225
- ? (cw5m ?? 0)
226
- : (usage.cache_creation_input_tokens ?? 0);
227
- const cacheWrite1h = cw1h ?? 0;
228
-
229
- addToTimeBuckets(
230
- buckets,
231
- ts,
232
- model,
233
- input,
234
- output,
235
- cacheWrite5m,
236
- cacheWrite1h,
237
- cr,
238
- todayPrefix,
239
- weekAgo,
240
- monthAgo
241
- );
242
-
243
- byModel[model] ??= emptyBucket();
244
- addToBucket(
245
- byModel[model],
246
- model,
247
- input,
248
- output,
249
- cacheWrite5m,
250
- cacheWrite1h,
251
- cr
252
- );
253
-
254
- byProject[projName] ??= emptyTimeBuckets();
255
- addToTimeBuckets(
256
- byProject[projName],
257
- ts,
258
- model,
259
- input,
260
- output,
261
- cacheWrite5m,
262
- cacheWrite1h,
263
- cr,
264
- todayPrefix,
265
- weekAgo,
266
- monthAgo
267
- );
268
- } catch {
269
- /* skip */
270
- }
271
- }
272
- }
273
- }
274
-
275
- return { buckets, byModel, byProject };
276
- }
277
-
278
- // ── PAL inference ──
279
-
280
- function readPalInference(): {
281
- buckets: TimeBuckets;
282
- byModel: Record<string, TimeBuckets>;
283
- byCaller: Record<string, Bucket>;
284
- } {
285
- const now = new Date();
286
- const todayPrefix = now.toISOString().slice(0, 10);
287
- const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
288
- const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
289
-
290
- const buckets = emptyTimeBuckets();
291
- const byModel: Record<string, TimeBuckets> = {};
292
- const byCaller: Record<string, Bucket> = {};
293
-
294
- const filepath = resolve(palHome(), "memory", "signals", "token-usage.jsonl");
295
- if (!existsSync(filepath)) return { buckets, byModel, byCaller };
296
-
297
- const content = readFileSync(filepath, "utf-8").trim();
298
- if (!content) return { buckets, byModel, byCaller };
299
-
300
- for (const line of content.split("\n")) {
301
- try {
302
- const e = JSON.parse(line) as {
303
- ts: string;
304
- caller: string;
305
- model: string;
306
- inputTokens: number;
307
- outputTokens: number;
308
- };
309
- addToTimeBuckets(
310
- buckets,
311
- e.ts,
312
- e.model,
313
- e.inputTokens,
314
- e.outputTokens,
315
- 0,
316
- 0,
317
- 0,
318
- todayPrefix,
319
- weekAgo,
320
- monthAgo
321
- );
322
- byModel[e.model] ??= emptyTimeBuckets();
323
- addToTimeBuckets(
324
- byModel[e.model],
325
- e.ts,
326
- e.model,
327
- e.inputTokens,
328
- e.outputTokens,
329
- 0,
330
- 0,
331
- 0,
332
- todayPrefix,
333
- weekAgo,
334
- monthAgo
335
- );
336
- byCaller[e.caller] ??= emptyBucket();
337
- addToBucket(byCaller[e.caller], e.model, e.inputTokens, e.outputTokens, 0, 0, 0);
338
- } catch {
339
- /* skip */
340
- }
341
- }
342
-
343
- return { buckets, byModel, byCaller };
344
- }
345
-
346
- // ── rtk compression savings ──
347
-
348
- interface RtkSummary {
349
- total_commands: number;
350
- total_saved: number;
351
- avg_savings_pct: number;
352
- }
353
-
354
- /**
355
- * Query rtk's own savings ledger. `installed: false` means rtk isn't on PATH;
356
- * `summary: null` with `installed: true` means rtk is present but has no data
357
- * (or errored) — the two cases print differently in the usage report.
358
- */
359
- function readRtkGain(): { installed: boolean; summary: RtkSummary | null } {
20
+ function rtkGain(): RtkGain {
360
21
  const rtk = findBinaryOnPath("rtk");
361
22
  if (!rtk) return { installed: false, summary: null };
362
- try {
363
- const r = spawnSync(rtk, ["gain", "--format", "json"], {
364
- encoding: "utf8",
365
- stdio: ["ignore", "pipe", "ignore"],
366
- });
367
- if (r.status !== 0 || !r.stdout) return { installed: true, summary: null };
368
- const parsed = JSON.parse(r.stdout) as { summary?: RtkSummary };
369
- return { installed: true, summary: parsed.summary ?? null };
370
- } catch {
371
- return { installed: true, summary: null };
372
- }
373
- }
374
-
375
- function printRtkGain(): void {
376
- const { installed, summary } = readRtkGain();
377
- console.log("\n rtk Compression\n");
378
- if (!installed) {
379
- console.log(" rtk not installed");
380
- return;
381
- }
382
- if (!summary || summary.total_commands === 0) {
383
- console.log(" rtk installed — no savings recorded yet");
384
- return;
385
- }
386
- console.log(
387
- ` Tokens saved ${fmt(summary.total_saved).padStart(8)} tok ${summary.avg_savings_pct.toFixed(1)}% avg across ${fmt(summary.total_commands)} commands`
388
- );
23
+ const result = spawnSync(rtk, ["gain", "--format", "json"], {
24
+ encoding: "utf8",
25
+ stdio: ["ignore", "pipe", "ignore"],
26
+ });
27
+ return {
28
+ installed: true,
29
+ summary: parseRtkSummary(result.status, result.stdout ?? ""),
30
+ };
389
31
  }
390
32
 
391
- // ── CLI ──
392
-
393
33
  export function usage() {
394
- parseArgs({
34
+ const { values } = parseArgs({
395
35
  options: {
396
36
  today: { type: "boolean", default: false },
397
37
  week: { type: "boolean", default: false },
@@ -402,62 +42,12 @@ export function usage() {
402
42
  strict: false,
403
43
  });
404
44
 
405
- const cc = readClaudeCode();
406
- const pal = readPalInference();
407
-
408
- console.log("\n Claude Code Usage\n");
409
- printRow("Today", cc.buckets.today);
410
- printRow("7d", cc.buckets.week);
411
- printRow("30d", cc.buckets.month);
412
- printRow("Total", cc.buckets.total);
413
-
414
- if (Object.keys(cc.byModel).length > 0) {
415
- console.log("\n By Model (all time)\n");
416
- const sorted = Object.entries(cc.byModel).sort((a, b) => b[1].cost - a[1].cost);
417
- const modelNames = sorted.map(([m]) => m.replace("claude-", ""));
418
- const modelWidth = Math.max(14, ...modelNames.map((n) => n.length + 2));
419
- for (let i = 0; i < sorted.length; i++) {
420
- printDetailed(modelNames[i], sorted[i][1], modelWidth);
421
- }
422
- }
423
-
424
- if (Object.keys(cc.byProject).length > 1) {
425
- console.log("\n By Project (all time)\n");
426
- const sorted = Object.entries(cc.byProject).sort(
427
- (a, b) => b[1].total.cost - a[1].total.cost
428
- );
429
- for (const [proj, tb] of sorted) {
430
- printRow(proj, tb.total);
431
- }
432
- }
433
-
434
- for (const [model, tb] of Object.entries(pal.byModel)) {
435
- if (tb.total.calls === 0) continue;
436
- let label: string;
437
- if (model.includes("haiku")) label = "Haiku";
438
- else if (model.includes("sonnet")) label = "Sonnet";
439
- else label = model.replace("claude-", "");
440
- console.log(`\n PAL Inference (${label})\n`);
441
- printRow("Today", tb.today);
442
- printRow("7d", tb.week);
443
- printRow("30d", tb.month);
444
- printRow("Total", tb.total);
445
- }
446
-
447
- printRtkGain();
448
-
449
- const grand = emptyBucket();
450
- for (const b of [cc.buckets.total, pal.buckets.total]) {
451
- grand.input += b.input;
452
- grand.output += b.output;
453
- grand.cacheWrite5m += b.cacheWrite5m;
454
- grand.cacheWrite1h += b.cacheWrite1h;
455
- grand.cacheRead += b.cacheRead;
456
- grand.cost += b.cost;
457
- grand.calls += b.calls;
458
- }
459
-
460
- console.log(`\n Grand Total: ${fmtCost(grand.cost)}\n`);
45
+ const lines = usageLines(
46
+ readClaudeCode(resolve(homedir(), ".claude", "projects"), values.project as string),
47
+ readPalInference(resolve(palHome(), "memory", "signals", "token-usage.jsonl")),
48
+ rtkGain()
49
+ );
50
+ for (const line of lines) console.log(line);
461
51
  }
462
52
 
463
53
  if (import.meta.main) usage();
@@ -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
- }