paseo-bm-plugin 0.0.0-placeholder.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 (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/client/agent-tree.ts +308 -0
  4. package/client/answer-state.ts +62 -0
  5. package/client/bead-chips.tsx +147 -0
  6. package/client/beads-header-button.ts +108 -0
  7. package/client/beads-model.ts +581 -0
  8. package/client/beads-screen.tsx +516 -0
  9. package/client/beads-tab.tsx +58 -0
  10. package/client/chat-card.tsx +636 -0
  11. package/client/chat-cards.ts +1038 -0
  12. package/client/dashboard-actions.tsx +255 -0
  13. package/client/dashboard-model.ts +947 -0
  14. package/client/dashboard-view.ts +215 -0
  15. package/client/dashboard.tsx +318 -0
  16. package/client/launch-manager.ts +323 -0
  17. package/client/launcher.tsx +516 -0
  18. package/client/markdown-view.tsx +112 -0
  19. package/client/markdown.ts +145 -0
  20. package/client/settings.tsx +104 -0
  21. package/client/setup-model.ts +552 -0
  22. package/client/setup-screen.tsx +913 -0
  23. package/client/slot.ts +47 -0
  24. package/client/tree.tsx +204 -0
  25. package/client/ui.tsx +262 -0
  26. package/client/waiting-pills-model.ts +156 -0
  27. package/client/waiting-pills.tsx +201 -0
  28. package/index.client.tsx +232 -0
  29. package/index.server.ts +168 -0
  30. package/package.json +35 -0
  31. package/paseo-plugin.json +6 -0
  32. package/roles/manager.md +181 -0
  33. package/roles/reviewer.md +160 -0
  34. package/roles/worker.md +407 -0
  35. package/server/agent-labels.ts +194 -0
  36. package/server/agent-role.ts +102 -0
  37. package/server/answer-marks.ts +120 -0
  38. package/server/bead-actions.ts +88 -0
  39. package/server/bead-work.ts +80 -0
  40. package/server/beads-store.ts +342 -0
  41. package/server/bm-report.ts +433 -0
  42. package/server/chat-peers.ts +65 -0
  43. package/server/chat-rpc.ts +122 -0
  44. package/server/chat-waiting.ts +182 -0
  45. package/server/collector.ts +629 -0
  46. package/server/config-writer.ts +222 -0
  47. package/server/cost.ts +88 -0
  48. package/server/dashboard-rpc.ts +662 -0
  49. package/server/fallback-detect.ts +183 -0
  50. package/server/fallback-handover.ts +365 -0
  51. package/server/fallback-manager.ts +170 -0
  52. package/server/fallback-reviewer.ts +198 -0
  53. package/server/fallback-rpc.ts +306 -0
  54. package/server/fallback-settings.ts +322 -0
  55. package/server/fallback-state.ts +518 -0
  56. package/server/fallback-switch.ts +191 -0
  57. package/server/fallback-wait.ts +188 -0
  58. package/server/format-check.ts +352 -0
  59. package/server/install-home.ts +187 -0
  60. package/server/live-timeline.ts +129 -0
  61. package/server/manager-instructions.ts +9 -0
  62. package/server/manager.ts +647 -0
  63. package/server/model-costs.ts +238 -0
  64. package/server/notice-queue.ts +315 -0
  65. package/server/notices.ts +81 -0
  66. package/server/paseo-cli.ts +115 -0
  67. package/server/provider-id.ts +12 -0
  68. package/server/review-budget.ts +208 -0
  69. package/server/reviewer-instructions.ts +9 -0
  70. package/server/role-choices.ts +161 -0
  71. package/server/role-extras.ts +270 -0
  72. package/server/role-hook.ts +347 -0
  73. package/server/role-mode.ts +397 -0
  74. package/server/role-settings-rpc.ts +325 -0
  75. package/server/roles.ts +96 -0
  76. package/server/settings-notices.ts +112 -0
  77. package/server/setup-rpc.ts +70 -0
  78. package/server/setup-skills.ts +121 -0
  79. package/server/setup-tools.ts +162 -0
  80. package/server/shell.ts +68 -0
  81. package/server/stop-propagation.ts +365 -0
  82. package/server/tools-check.ts +118 -0
  83. package/server/trace-store.ts +1137 -0
  84. package/server/traces.ts +1356 -0
  85. package/server/worker-instructions.ts +9 -0
  86. package/server/workflow-steps.ts +422 -0
  87. package/shared/bead-ids.ts +25 -0
  88. package/shared/bm-fallback.ts +91 -0
  89. package/shared/bm-format.ts +424 -0
  90. package/shared/bm-questions.ts +213 -0
  91. package/shared/bm-report.ts +433 -0
  92. package/shared/contracts.ts +1371 -0
  93. package/shared/fallback-patterns.ts +201 -0
  94. package/shared/fallback.ts +46 -0
  95. package/shared/new-request.ts +20 -0
  96. package/shared/order.ts +22 -0
  97. package/shared/prices.ts +65 -0
  98. package/shared/settings.ts +57 -0
  99. package/shared/sole-worker.ts +20 -0
  100. package/shared/version.ts +6 -0
  101. package/tsconfig.json +16 -0
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Which view the Beads Manager surface shows, and the hand-off from the
3
+ * Command Center (WP-211.2.1; REQ-040a, REQ-040b).
4
+ *
5
+ * The Dashboard lives on the same surface as the launcher (Q-036), so the
6
+ * surface needs one bit of state and one queue — the same shape
7
+ * `launch-manager.ts` already uses for its own Command Center item, because a
8
+ * workspace Command Center context can open a surface but cannot pass it
9
+ * anything.
10
+ *
11
+ * Pure: no React, no React Native, no JSX.
12
+ */
13
+
14
+ import type { WorkspaceOverview } from "../shared/contracts";
15
+ import { STATUS_TONE } from "./beads-model";
16
+ import type { Tone } from "./dashboard-model";
17
+ import { createSlot, type Slot } from "./slot";
18
+
19
+ /**
20
+ * The views of the Beads Manager surface. Setup is the main screen and the
21
+ * workspace list is one tap away (owner decision Q1, delta 20260918e §4.2).
22
+ */
23
+ export type DashboardViewName = "setup" | "workspaces" | "dashboard" | "beads";
24
+
25
+ /** Where the surface opens. */
26
+ export const SURFACE_HOME_VIEW: DashboardViewName = "setup";
27
+
28
+ /** Where ← leads from a view; `null` on the main screen, which has no ←. */
29
+ export function backOf(view: DashboardViewName): DashboardViewName | null {
30
+ switch (view) {
31
+ case "dashboard":
32
+ case "beads":
33
+ return "workspaces";
34
+ case "workspaces":
35
+ return "setup";
36
+ case "setup":
37
+ return null;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * What each ← says to a screen reader: where it leads (delta 20260918f F4,
43
+ * owner decision Q6 a). `null` on the main screen, which has no ←.
44
+ */
45
+ export function backLabelOf(view: DashboardViewName): string | null {
46
+ switch (backOf(view)) {
47
+ case "setup":
48
+ return "Back to Beads Manager setup";
49
+ case "workspaces":
50
+ return "Back to workspaces";
51
+ default:
52
+ return null;
53
+ }
54
+ }
55
+
56
+ /** How often the per-workspace figures are refreshed while the workspace list shows. */
57
+ export const OVERVIEW_POLL_MS = 10_000;
58
+
59
+ /**
60
+ * Whether the surface reads the per-workspace figures (`workspaces.overview`,
61
+ * which touches every workspace's bead store) and how often. Only the
62
+ * workspace list shows them (delta 20260918f F5).
63
+ */
64
+ export function overviewPolling(view: DashboardViewName): { enabled: boolean; refetchInterval: number | false } {
65
+ return view === "workspaces" ? { enabled: true, refetchInterval: OVERVIEW_POLL_MS } : { enabled: false, refetchInterval: false };
66
+ }
67
+
68
+ /** The workspace whose Dashboard the Command Center asked for; one per loaded client bundle, like `launchRequests`. */
69
+ export const dashboardRequests: Slot<string> = createSlot<string>();
70
+
71
+ /**
72
+ * How a slash command says something back to the user.
73
+ *
74
+ * Paseo 0.8 gives a plugin slash command NO report channel: its context carries
75
+ * only `paseo`, `rpc`, `openSurface`, `openSettings` and `openPanel`, and the
76
+ * composer shows nothing unless `onSubmit` REJECTS — in which case the message
77
+ * lands in an error toast. Reporting a success by throwing would paint
78
+ * "Asked 2 Workers to stop" in the colour of a failure, which is a lie about
79
+ * what happened.
80
+ *
81
+ * So a command puts its line here and opens the Beads Manager surface, which
82
+ * draws it as an ordinary notice; tapping it takes it, which hides it.
83
+ */
84
+ export const launcherNotices: Slot<string> = createSlot<string>();
85
+
86
+ /** Body of the workspace Command Center item "Open Beads Metric". */
87
+ export function selectDashboardFromCommandCenter(
88
+ context: { workspace: { id: string }; openSurface(id: string): void },
89
+ surfaceId: string,
90
+ requests: Slot<string> = dashboardRequests,
91
+ ): void {
92
+ requests.put(context.workspace.id);
93
+ context.openSurface(surfaceId);
94
+ }
95
+
96
+ /**
97
+ * Title of a workspace's Metric and Beads screens. A workspace is usually named
98
+ * after its first chat ("Build bva terminal UI…"), which does not say which
99
+ * repository the beads come from, so the project is named too.
100
+ */
101
+ export function screenTitleOf(label: string, project: string): string {
102
+ return project === "" || project === label ? label : `${label} · ${project}`;
103
+ }
104
+
105
+ export interface StoredWorkspace {
106
+ workspaceId: string;
107
+ state: "live" | "archived" | "orphaned" | "unknown";
108
+ lastKnownName: string | null;
109
+ lastKnownDirectory: string | null;
110
+ lastSeenAt: string | null;
111
+ bytes: number;
112
+ }
113
+
114
+ /**
115
+ * Workspaces with trace history that the launcher does not list any more —
116
+ * archived or removed from Paseo — newest activity first. Their history stays
117
+ * on disk, so this is how it is reached again; after the project is reopened
118
+ * (a new workspace id), its Metric screen offers to move the history onto it.
119
+ */
120
+ export function closedWorkspaces(
121
+ stored: readonly StoredWorkspace[],
122
+ listedIds: readonly string[],
123
+ ): Array<{ workspaceId: string; label: string; detail: string }> {
124
+ const listed = new Set(listedIds);
125
+ return stored
126
+ .filter((entry) => !listed.has(entry.workspaceId) && entry.state !== "unknown")
127
+ .sort((a, b) => ((a.lastSeenAt ?? "") < (b.lastSeenAt ?? "") ? 1 : -1))
128
+ .map((entry) => ({
129
+ workspaceId: entry.workspaceId,
130
+ label: entry.lastKnownName ?? entry.workspaceId,
131
+ detail: [
132
+ entry.state === "archived" ? "archived" : "no longer in Paseo",
133
+ entry.lastKnownDirectory,
134
+ entry.lastSeenAt === null ? null : `last active ${entry.lastSeenAt.slice(0, 10)}`,
135
+ `${Math.max(1, Math.round(entry.bytes / 1024))} KB of history`,
136
+ ]
137
+ .filter((part) => part !== null)
138
+ .join(" · "),
139
+ }));
140
+ }
141
+
142
+ /** One figure under a workspace name: a small icon, a number, and what it means. */
143
+ export interface WorkspaceStat {
144
+ key: "total" | "inProgress" | "blocked" | "running";
145
+ icon: string;
146
+ value: string;
147
+ label: string;
148
+ tone: Tone;
149
+ }
150
+
151
+ /**
152
+ * The four figures of a workspace row: beads in total, in progress, blocked,
153
+ * and Workers running now. A figure that is zero stays grey so the ones that
154
+ * need attention stand out; a workspace without a bead store says so once.
155
+ * In progress and blocked take the bead status colours (`STATUS_TONE`), so a
156
+ * row reads like the Beads screen it opens (delta 20260918e, REQ-060 n).
157
+ */
158
+ export function workspaceStats(overview: WorkspaceOverview | undefined): WorkspaceStat[] {
159
+ if (overview === undefined) return [];
160
+ const running: WorkspaceStat = {
161
+ key: "running",
162
+ icon: "Hammer",
163
+ value: String(overview.runningWorkers),
164
+ label: `${overview.runningWorkers} Worker(s) running`,
165
+ tone: overview.runningWorkers > 0 ? "success" : "muted",
166
+ };
167
+ const beads = overview.beads;
168
+ if (beads === null) {
169
+ return [{ key: "total", icon: "Layers", value: "–", label: "no beads in this workspace", tone: "muted" }, running];
170
+ }
171
+ return [
172
+ { key: "total", icon: "Layers", value: String(beads.total), label: `${beads.total} beads in total`, tone: "muted" },
173
+ {
174
+ key: "inProgress",
175
+ icon: "CircleDot",
176
+ value: String(beads.inProgress),
177
+ label: `${beads.inProgress} in progress`,
178
+ tone: beads.inProgress > 0 ? STATUS_TONE.in_progress : "muted",
179
+ },
180
+ {
181
+ key: "blocked",
182
+ icon: "Ban",
183
+ value: String(beads.blocked),
184
+ label: `${beads.blocked} blocked`,
185
+ tone: beads.blocked > 0 ? STATUS_TONE.blocked : "muted",
186
+ },
187
+ running,
188
+ ];
189
+ }
190
+
191
+ /** The three actions of a workspace row, in order, with their icons. */
192
+ export const WORKSPACE_ACTIONS = [
193
+ { key: "manager", label: "Go to", icon: "Bot" },
194
+ { key: "metric", label: "Metric", icon: "ChartColumn" },
195
+ { key: "beads", label: "Beads", icon: "ListChecks" },
196
+ ] as const;
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // The "Beads" tab of a workspace (delta 20260918e §4.1): one entry in the "+"
200
+ // menu of the tab bar, with the Beads and Metric screens as two sub-tabs.
201
+ // ---------------------------------------------------------------------------
202
+
203
+ /** Workspace panel id; Paseo lists every workspace panel in the "+" menu. */
204
+ export const BEADS_TAB_PANEL_ID = "bm-beads";
205
+
206
+ /** The two sub-tabs, in the order they are drawn. */
207
+ export const BEADS_TAB_VIEWS = [
208
+ { key: "beads", label: "Beads" },
209
+ { key: "metric", label: "Metric" },
210
+ ] as const;
211
+
212
+ export type BeadsTabView = (typeof BEADS_TAB_VIEWS)[number]["key"];
213
+
214
+ /** The sub-tab a new "Beads" tab opens on (owner decision Q2). */
215
+ export const DEFAULT_BEADS_TAB_VIEW: BeadsTabView = "beads";
@@ -0,0 +1,318 @@
1
+ /**
2
+ * The Beads Dashboard (WP-211.2.1; REQ-040 → REQ-046, REQ-048d, REQ-055).
3
+ *
4
+ * Layout, top to bottom:
5
+ * 1. overview cards — requests, beads, agents, messages, tokens, cost;
6
+ * 2. three small bar charts — requests per day, the five heaviest Workers
7
+ * (press one to open it), tokens by model × role;
8
+ * 3. requests as graphs — Manager → Workers → Reviewers, each marked with a
9
+ * small role icon; tap a request to open its graph, tap a node to expand
10
+ * what it sent, got back and cost;
11
+ * 4. storage and the privacy note.
12
+ *
13
+ * The destructive flows live in `dashboard-actions.tsx`. All wording and
14
+ * numbers come from `dashboard-model.ts`, tested without a renderer.
15
+ *
16
+ * Client rules: React Native primitives only, colours from `theme.colors`, no
17
+ * Node import, no `server/` import.
18
+ */
19
+ import { type PluginSurfaceProps, useRpc, useSettings } from "@getpaseo/plugin/client";
20
+ import { useQuery } from "@tanstack/react-query";
21
+ import { useMemo, useState } from "react";
22
+ import { ActivityIndicator, Pressable, ScrollView, Text, View } from "react-native";
23
+ import { beadsStatsRpc, tracesGetRpc, tracesListRpc, type TraceSummary } from "../shared/contracts";
24
+ import { DEFAULT_WARN_ABOVE_BYTES, dashboardSettings } from "../shared/settings";
25
+ import { TraceActions } from "./dashboard-actions";
26
+ import {
27
+ PRIVACY_NOTICE,
28
+ dashboardStyles,
29
+ groupTraces,
30
+ heaviestWorkers,
31
+ overviewCards,
32
+ tokensByModelRole,
33
+ requestGraph,
34
+ requestsPerDay,
35
+ storageView,
36
+ toneColor,
37
+ type Badge,
38
+ type ChipGroup,
39
+ type GraphNode,
40
+ } from "./dashboard-model";
41
+ import { errorMessageOf } from "./launch-manager";
42
+ import { BarChart, Chip, RoleLegend, RoleMark, StatCards, WorkspaceScreenHeader, type Styles, type Theme, type WorkspaceScreenProps } from "./ui";
43
+
44
+
45
+ function BadgeText({ badge, styles, theme }: { badge: Badge; styles: Styles; theme: Theme }) {
46
+ return <Text style={[styles.badge, { color: toneColor(theme, badge.tone) }]}>{badge.text}</Text>;
47
+ }
48
+
49
+ function Chips({ group, styles, theme }: { group: ChipGroup; styles: Styles; theme: Theme }) {
50
+ return (
51
+ <View style={{ gap: 4 }}>
52
+ <Text style={styles.body}>{group.label}</Text>
53
+ <View style={styles.chipRow}>
54
+ {group.chips.map((chip) => (
55
+ <Chip key={chip.text} badge={chip} styles={styles} theme={theme} />
56
+ ))}
57
+ </View>
58
+ {group.note === undefined ? null : <Text style={[styles.body, { fontSize: 11 }]}>{group.note}</Text>}
59
+ </View>
60
+ );
61
+ }
62
+
63
+ interface NodeViewProps {
64
+ node: GraphNode;
65
+ open: boolean;
66
+ onToggle: (id: string) => void;
67
+ styles: Styles;
68
+ theme: Theme;
69
+ navigation: PluginSurfaceProps["navigation"];
70
+ }
71
+
72
+ function NodeView({ node, open, onToggle, styles, theme, navigation }: NodeViewProps) {
73
+ const label = node.kind === "request" ? "Manager · request details" : `${node.title} · ${node.subtitle}`;
74
+ return (
75
+ <View style={styles.node}>
76
+ <Pressable
77
+ accessibilityRole="button"
78
+ accessibilityLabel={label}
79
+ accessibilityState={{ expanded: open }}
80
+ onPress={() => onToggle(node.id)}
81
+ >
82
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
83
+ <Text style={styles.body}>{open ? "▾" : "▸"}</Text>
84
+ <RoleMark kind={node.kind} theme={theme} />
85
+ <Text style={[styles.body, { flex: 1 }]}>{label}</Text>
86
+ {node.kind === "request" ? null : <BadgeText badge={node.badge} styles={styles} theme={theme} />}
87
+ </View>
88
+ </Pressable>
89
+ {open ? (
90
+ <View style={{ gap: 2 }}>
91
+ {node.details.length === 0 && node.chipGroups.length === 0 ? <Text style={styles.body}>Loading…</Text> : null}
92
+ {node.chipGroups.map((group) => (
93
+ <Chips key={group.label} group={group} styles={styles} theme={theme} />
94
+ ))}
95
+ {node.details.map((line, index) => (
96
+ <Text key={`${node.id}-${index}`} style={styles.body} selectable>
97
+ {line}
98
+ </Text>
99
+ ))}
100
+ {node.agentId !== null && navigation?.openAgent !== undefined ? (
101
+ <Pressable
102
+ accessibilityRole="button"
103
+ accessibilityLabel={`Open ${node.title}`}
104
+ onPress={() => navigation.openAgent({ agentId: node.agentId! })}
105
+ style={[styles.secondaryButton, { alignSelf: "flex-start" }]}
106
+ >
107
+ <Text style={styles.secondaryButtonText}>Open agent</Text>
108
+ </Pressable>
109
+ ) : null}
110
+ </View>
111
+ ) : null}
112
+ </View>
113
+ );
114
+ }
115
+
116
+ interface RequestCardProps {
117
+ trace: TraceSummary;
118
+ workspaceId: string;
119
+ styles: Styles;
120
+ theme: Theme;
121
+ layout: PluginSurfaceProps["layout"];
122
+ navigation: PluginSurfaceProps["navigation"];
123
+ onChanged: () => void;
124
+ }
125
+
126
+ function RequestCard({ trace, workspaceId, styles, theme, layout, navigation, onChanged }: RequestCardProps) {
127
+ const getTrace = useRpc(tracesGetRpc);
128
+ const [open, setOpen] = useState(false);
129
+ const [expanded, setExpanded] = useState<ReadonlySet<string>>(new Set());
130
+
131
+ // The detail is fetched only once the request is opened.
132
+ const detail = useQuery({
133
+ queryKey: ["paseo-bm", "trace", workspaceId, trace.traceId],
134
+ queryFn: () => getTrace({ workspaceId, traceId: trace.traceId }),
135
+ enabled: open,
136
+ });
137
+
138
+ const [root, ...children] = requestGraph(trace, detail.data?.trace ?? null, new Date());
139
+ const toggle = (id: string) =>
140
+ setExpanded((current) => {
141
+ const next = new Set(current);
142
+ if (next.has(id)) next.delete(id);
143
+ else next.add(id);
144
+ return next;
145
+ });
146
+
147
+ return (
148
+ <View style={styles.card}>
149
+ <Pressable
150
+ accessibilityRole="button"
151
+ accessibilityLabel={`Request: ${root!.title}`}
152
+ accessibilityState={{ expanded: open }}
153
+ onPress={() => setOpen(!open)}
154
+ >
155
+ <View style={{ flexDirection: "row", justifyContent: "space-between", gap: 8 }}>
156
+ <View style={{ flex: 1, gap: 2 }}>
157
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
158
+ <RoleMark kind="request" theme={theme} />
159
+ <Text style={[styles.mono, { flex: 1 }]} numberOfLines={open ? undefined : 1}>
160
+ {`${open ? "▾" : "▸"} ${root!.title}`}
161
+ </Text>
162
+ </View>
163
+ <Text style={styles.body}>{root!.subtitle}</Text>
164
+ </View>
165
+ <BadgeText badge={root!.badge} styles={styles} theme={theme} />
166
+ </View>
167
+ </Pressable>
168
+
169
+ {open ? (
170
+ <View style={{ gap: 2 }}>
171
+ {detail.isPending ? <ActivityIndicator color={styles.spinner.color} /> : null}
172
+ {detail.isError ? (
173
+ <Text style={[styles.body, { color: toneColor(theme, "danger") }]}>{errorMessageOf(detail.error)}</Text>
174
+ ) : null}
175
+ <NodeView node={root!} open={expanded.has(root!.id)} onToggle={toggle} styles={styles} theme={theme} navigation={navigation} />
176
+ {children.map((node) => (
177
+ <View key={node.id} style={{ marginLeft: node.depth * (layout.compact ? 12 : 20) }}>
178
+ <NodeView node={node} open={expanded.has(node.id)} onToggle={toggle} styles={styles} theme={theme} navigation={navigation} />
179
+ </View>
180
+ ))}
181
+ {expanded.has(root!.id) ? (
182
+ <TraceActions
183
+ theme={theme}
184
+ layout={layout}
185
+ styles={styles}
186
+ workspaceId={workspaceId}
187
+ scope="trace"
188
+ traceId={trace.traceId}
189
+ onDone={onChanged}
190
+ />
191
+ ) : null}
192
+ </View>
193
+ ) : null}
194
+ </View>
195
+ );
196
+ }
197
+
198
+ export function DashboardPanel({ theme, layout, navigation, workspaceId, workspaceLabel, onBack, backLabel, status }: WorkspaceScreenProps) {
199
+ const styles = useMemo(() => dashboardStyles(theme, layout.compact), [theme, layout.compact]);
200
+ const listTraces = useRpc(tracesListRpc);
201
+ const beadStats = useRpc(beadsStatsRpc);
202
+ const settings = useSettings(dashboardSettings);
203
+ const [showStorage, setShowStorage] = useState(false);
204
+
205
+ const traces = useQuery({
206
+ queryKey: ["paseo-bm", "traces", workspaceId],
207
+ queryFn: () => listTraces({ workspaceId }),
208
+ });
209
+ const beads = useQuery({
210
+ queryKey: ["paseo-bm", "beads", workspaceId],
211
+ queryFn: () => beadStats({ workspaceId }),
212
+ });
213
+
214
+ const rows = traces.data?.traces ?? [];
215
+ const warnAboveBytes =
216
+ settings.status === "ready" ? settings.values.warnAboveBytes : DEFAULT_WARN_ABOVE_BYTES;
217
+ const storage = traces.data === undefined ? null : storageView(traces.data.store, warnAboveBytes);
218
+ const refresh = () => {
219
+ void traces.refetch();
220
+ void beads.refetch();
221
+ };
222
+
223
+ return (
224
+ <ScrollView style={styles.screen} contentContainerStyle={styles.content}>
225
+ <WorkspaceScreenHeader
226
+ title={workspaceLabel === undefined ? null : `Metric · ${workspaceLabel}`}
227
+ onBack={onBack}
228
+ backLabel={backLabel ?? "Back to Beads Manager"}
229
+ status={status}
230
+ styles={styles}
231
+ right={
232
+ <Pressable accessibilityRole="button" accessibilityLabel="Refresh" onPress={refresh} style={styles.secondaryButton}>
233
+ <Text style={styles.secondaryButtonText}>Refresh</Text>
234
+ </Pressable>
235
+ }
236
+ />
237
+
238
+ {/* 1. Overview */}
239
+ <StatCards cards={overviewCards(rows, beads.data?.stats ?? null)} styles={styles} />
240
+ {storage?.warning != null ? (
241
+ <Text style={[styles.body, { color: toneColor(theme, storage.warning.tone) }]}>{storage.warning.text}</Text>
242
+ ) : null}
243
+
244
+ {/* 2. Charts */}
245
+ {rows.length > 0 ? (
246
+ <View style={styles.cards}>
247
+ {/* The list has one row per question; this chart counts requests (Q27). */}
248
+ <BarChart
249
+ title="Requests, last 7 days — each request counted once"
250
+ bars={requestsPerDay(rows, new Date())}
251
+ styles={styles}
252
+ />
253
+ <BarChart
254
+ title="Top 5 heaviest Workers (tokens)"
255
+ bars={heaviestWorkers(rows)}
256
+ styles={styles}
257
+ onOpen={navigation?.openAgent === undefined ? undefined : (agentId) => navigation.openAgent({ agentId })}
258
+ />
259
+ {/* Which model each role actually ran on (delta 20260918, REQ-058e). */}
260
+ <BarChart title="Tokens by model × role" bars={tokensByModelRole(rows)} styles={styles} labelWidth={180} />
261
+ </View>
262
+ ) : null}
263
+
264
+ {/* 3. Requests as graphs */}
265
+ {rows.length > 0 ? <RoleLegend styles={styles} theme={theme} /> : null}
266
+ {traces.isPending ? <ActivityIndicator color={styles.spinner.color} /> : null}
267
+ {traces.isError ? (
268
+ <Text style={[styles.body, { color: toneColor(theme, "danger") }]}>{errorMessageOf(traces.error)}</Text>
269
+ ) : null}
270
+ {traces.data !== undefined && rows.length === 0 ? (
271
+ <Text style={styles.body}>No requests recorded yet. They appear once you ask the Beads Manager for something.</Text>
272
+ ) : null}
273
+ {traces.data?.notices.map((notice) => (
274
+ <Text key={notice} style={[styles.body, { color: toneColor(theme, "warning") }]}>
275
+ {notice}
276
+ </Text>
277
+ ))}
278
+ {groupTraces(rows).map((group) => (
279
+ <View key={group.key} style={{ gap: layout.compact ? 6 : 8 }}>
280
+ <Text style={styles.sectionTitle}>{group.title}</Text>
281
+ {group.hint === null ? null : <Text style={styles.body}>{group.hint}</Text>}
282
+ {group.traces.map((trace) => (
283
+ <RequestCard
284
+ key={`${trace.traceId}#${trace.turn?.index ?? 1}`}
285
+ trace={trace}
286
+ workspaceId={workspaceId}
287
+ styles={styles}
288
+ theme={theme}
289
+ layout={layout}
290
+ navigation={navigation}
291
+ onChanged={refresh}
292
+ />
293
+ ))}
294
+ </View>
295
+ ))}
296
+ {traces.data?.truncated === true ? <Text style={styles.body}>Older requests are not shown.</Text> : null}
297
+
298
+ {/* 4. Storage, folded away */}
299
+ <Pressable accessibilityRole="button" accessibilityState={{ expanded: showStorage }} onPress={() => setShowStorage(!showStorage)}>
300
+ <Text style={styles.body}>{`${showStorage ? "▾" : "▸"} Storage${storage === null ? "" : ` · ${storage.summary}`}`}</Text>
301
+ </Pressable>
302
+ {showStorage ? (
303
+ <View style={styles.card}>
304
+ <TraceActions
305
+ theme={theme}
306
+ layout={layout}
307
+ styles={styles}
308
+ workspaceId={workspaceId}
309
+ scope="workspace"
310
+ workspaceState={rows[0]?.workspaceState}
311
+ onDone={refresh}
312
+ />
313
+ </View>
314
+ ) : null}
315
+ <Text style={[styles.body, { color: toneColor(theme, "muted") }]}>{PRIVACY_NOTICE}</Text>
316
+ </ScrollView>
317
+ );
318
+ }