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.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/client/agent-tree.ts +308 -0
- package/client/answer-state.ts +62 -0
- package/client/bead-chips.tsx +147 -0
- package/client/beads-header-button.ts +108 -0
- package/client/beads-model.ts +581 -0
- package/client/beads-screen.tsx +516 -0
- package/client/beads-tab.tsx +58 -0
- package/client/chat-card.tsx +636 -0
- package/client/chat-cards.ts +1038 -0
- package/client/dashboard-actions.tsx +255 -0
- package/client/dashboard-model.ts +947 -0
- package/client/dashboard-view.ts +215 -0
- package/client/dashboard.tsx +318 -0
- package/client/launch-manager.ts +323 -0
- package/client/launcher.tsx +516 -0
- package/client/markdown-view.tsx +112 -0
- package/client/markdown.ts +145 -0
- package/client/settings.tsx +104 -0
- package/client/setup-model.ts +552 -0
- package/client/setup-screen.tsx +913 -0
- package/client/slot.ts +47 -0
- package/client/tree.tsx +204 -0
- package/client/ui.tsx +262 -0
- package/client/waiting-pills-model.ts +156 -0
- package/client/waiting-pills.tsx +201 -0
- package/index.client.tsx +232 -0
- package/index.server.ts +168 -0
- package/package.json +35 -0
- package/paseo-plugin.json +6 -0
- package/roles/manager.md +181 -0
- package/roles/reviewer.md +160 -0
- package/roles/worker.md +407 -0
- package/server/agent-labels.ts +194 -0
- package/server/agent-role.ts +102 -0
- package/server/answer-marks.ts +120 -0
- package/server/bead-actions.ts +88 -0
- package/server/bead-work.ts +80 -0
- package/server/beads-store.ts +342 -0
- package/server/bm-report.ts +433 -0
- package/server/chat-peers.ts +65 -0
- package/server/chat-rpc.ts +122 -0
- package/server/chat-waiting.ts +182 -0
- package/server/collector.ts +629 -0
- package/server/config-writer.ts +222 -0
- package/server/cost.ts +88 -0
- package/server/dashboard-rpc.ts +662 -0
- package/server/fallback-detect.ts +183 -0
- package/server/fallback-handover.ts +365 -0
- package/server/fallback-manager.ts +170 -0
- package/server/fallback-reviewer.ts +198 -0
- package/server/fallback-rpc.ts +306 -0
- package/server/fallback-settings.ts +322 -0
- package/server/fallback-state.ts +518 -0
- package/server/fallback-switch.ts +191 -0
- package/server/fallback-wait.ts +188 -0
- package/server/format-check.ts +352 -0
- package/server/install-home.ts +187 -0
- package/server/live-timeline.ts +129 -0
- package/server/manager-instructions.ts +9 -0
- package/server/manager.ts +647 -0
- package/server/model-costs.ts +238 -0
- package/server/notice-queue.ts +315 -0
- package/server/notices.ts +81 -0
- package/server/paseo-cli.ts +115 -0
- package/server/provider-id.ts +12 -0
- package/server/review-budget.ts +208 -0
- package/server/reviewer-instructions.ts +9 -0
- package/server/role-choices.ts +161 -0
- package/server/role-extras.ts +270 -0
- package/server/role-hook.ts +347 -0
- package/server/role-mode.ts +397 -0
- package/server/role-settings-rpc.ts +325 -0
- package/server/roles.ts +96 -0
- package/server/settings-notices.ts +112 -0
- package/server/setup-rpc.ts +70 -0
- package/server/setup-skills.ts +121 -0
- package/server/setup-tools.ts +162 -0
- package/server/shell.ts +68 -0
- package/server/stop-propagation.ts +365 -0
- package/server/tools-check.ts +118 -0
- package/server/trace-store.ts +1137 -0
- package/server/traces.ts +1356 -0
- package/server/worker-instructions.ts +9 -0
- package/server/workflow-steps.ts +422 -0
- package/shared/bead-ids.ts +25 -0
- package/shared/bm-fallback.ts +91 -0
- package/shared/bm-format.ts +424 -0
- package/shared/bm-questions.ts +213 -0
- package/shared/bm-report.ts +433 -0
- package/shared/contracts.ts +1371 -0
- package/shared/fallback-patterns.ts +201 -0
- package/shared/fallback.ts +46 -0
- package/shared/new-request.ts +20 -0
- package/shared/order.ts +22 -0
- package/shared/prices.ts +65 -0
- package/shared/settings.ts +57 -0
- package/shared/sole-worker.ts +20 -0
- package/shared/version.ts +6 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Beads Manager" surface (WP-113): the screen behind the sidebar item and the
|
|
3
|
+
* Command Center item. It only renders; the launch logic, texts and styles live
|
|
4
|
+
* in `launch-manager.ts`, which is tested without a renderer.
|
|
5
|
+
*
|
|
6
|
+
* Client rules: React Native primitives only, every color from theme.colors,
|
|
7
|
+
* no Node builtin imports.
|
|
8
|
+
*/
|
|
9
|
+
import type { PluginTheme } from "@getpaseo/plugin";
|
|
10
|
+
import { type PluginSurfaceProps, usePaseo, useRpc } from "@getpaseo/plugin/client";
|
|
11
|
+
import { useQuery } from "@tanstack/react-query";
|
|
12
|
+
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
13
|
+
import {
|
|
14
|
+
AccessibilityInfo,
|
|
15
|
+
ActivityIndicator,
|
|
16
|
+
Animated,
|
|
17
|
+
Pressable,
|
|
18
|
+
ScrollView,
|
|
19
|
+
Text,
|
|
20
|
+
View,
|
|
21
|
+
} from "react-native";
|
|
22
|
+
import { BeadsScreen } from "./beads-screen";
|
|
23
|
+
import { SetupScreen } from "./setup-screen";
|
|
24
|
+
import { DashboardPanel } from "./dashboard";
|
|
25
|
+
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
26
|
+
import {
|
|
27
|
+
SURFACE_HOME_VIEW,
|
|
28
|
+
WORKSPACE_ACTIONS,
|
|
29
|
+
backLabelOf,
|
|
30
|
+
backOf,
|
|
31
|
+
overviewPolling,
|
|
32
|
+
closedWorkspaces,
|
|
33
|
+
dashboardRequests,
|
|
34
|
+
launcherNotices,
|
|
35
|
+
screenTitleOf,
|
|
36
|
+
workspaceStats,
|
|
37
|
+
type DashboardViewName,
|
|
38
|
+
} from "./dashboard-view";
|
|
39
|
+
import { toneColor, type Tone } from "./dashboard-model";
|
|
40
|
+
import { managerEnsureRpc, tracesWorkspacesRpc, workspacesOverviewRpc } from "../shared/contracts";
|
|
41
|
+
import { PLUGIN_VERSION } from "../shared/version";
|
|
42
|
+
import {
|
|
43
|
+
errorMessageOf,
|
|
44
|
+
launcherStatusLines,
|
|
45
|
+
launcherStyles,
|
|
46
|
+
launchRequests,
|
|
47
|
+
managerLauncher,
|
|
48
|
+
runPendingRequest,
|
|
49
|
+
type StatusLine,
|
|
50
|
+
} from "./launch-manager";
|
|
51
|
+
|
|
52
|
+
interface WorkspaceRow {
|
|
53
|
+
id: string;
|
|
54
|
+
label: string;
|
|
55
|
+
detail: string;
|
|
56
|
+
/** Title of the Metric and Beads screens: the workspace and its project. */
|
|
57
|
+
screenTitle: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** One breath of the pulse: slow enough to read as "alive", not as an alarm. */
|
|
61
|
+
const PULSE_MS = 900;
|
|
62
|
+
/** The floor of the pulse, and the resting opacity of a dot with nothing to say. */
|
|
63
|
+
const DIM_OPACITY = 0.3;
|
|
64
|
+
/** Roles in the order they appear in the label, with the word the user reads. */
|
|
65
|
+
const DOT_ROLES = [
|
|
66
|
+
["manager", "Manager"],
|
|
67
|
+
["worker", "Worker"],
|
|
68
|
+
["reviewer", "Reviewer"],
|
|
69
|
+
] as const;
|
|
70
|
+
|
|
71
|
+
/** Running bm agents of one workspace, per role (`workspaces.overview`). */
|
|
72
|
+
export interface RunningAgentCounts {
|
|
73
|
+
manager: number;
|
|
74
|
+
worker: number;
|
|
75
|
+
reviewer: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Everything the dot beside a project name shows; see `runningDotState`. */
|
|
79
|
+
export interface RunningDotState {
|
|
80
|
+
total: number;
|
|
81
|
+
/** True only when the dot pulses: an idle row and reduced motion both say false. */
|
|
82
|
+
animate: boolean;
|
|
83
|
+
/** Resting opacity, and the value the pulse returns to. */
|
|
84
|
+
opacity: number;
|
|
85
|
+
/** "1 Worker, 1 Reviewer" — read aloud even when the text is not drawn. */
|
|
86
|
+
label: string;
|
|
87
|
+
tone: Tone;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The dot that says a bm agent is working in this workspace right now
|
|
92
|
+
* (delta 20260917e §4.2): it pulses while any of the three roles runs, and
|
|
93
|
+
* stands still and dim when none does.
|
|
94
|
+
*/
|
|
95
|
+
export function runningDotState(
|
|
96
|
+
counts: RunningAgentCounts | undefined,
|
|
97
|
+
reduceMotion: boolean,
|
|
98
|
+
): RunningDotState | null {
|
|
99
|
+
// The overview has not answered for this row yet. Unknown is not idle, and a
|
|
100
|
+
// dim dot would claim "nothing is running" before anything was counted.
|
|
101
|
+
if (counts === undefined) return null;
|
|
102
|
+
const total = counts.manager + counts.worker + counts.reviewer;
|
|
103
|
+
if (total === 0) {
|
|
104
|
+
return { total, animate: false, opacity: DIM_OPACITY, label: "No Beads agent running", tone: "muted" };
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
total,
|
|
108
|
+
// Reduced motion keeps the signal and drops the motion: the dot goes solid
|
|
109
|
+
// and no loop is started at all.
|
|
110
|
+
animate: !reduceMotion,
|
|
111
|
+
opacity: 1,
|
|
112
|
+
label: DOT_ROLES.filter(([key]) => counts[key] > 0)
|
|
113
|
+
.map(([key, name]) => `${counts[key]} ${name}${counts[key] === 1 ? "" : "s"}`)
|
|
114
|
+
.join(", "),
|
|
115
|
+
tone: "success",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Puts a state on an `Animated.Value`: the resting opacity always, the pulse
|
|
121
|
+
* only when the state asks for one. Returns the stop function, or `undefined`
|
|
122
|
+
* when there is nothing running to stop — that is the whole reduced-motion
|
|
123
|
+
* promise, so it is a function the test can call rather than a rendered effect.
|
|
124
|
+
*/
|
|
125
|
+
export function applyDotPulse(value: Animated.Value, state: RunningDotState): (() => void) | undefined {
|
|
126
|
+
// Set on every change so a dot that stops pulsing lands on its resting
|
|
127
|
+
// opacity instead of wherever the interrupted loop left it.
|
|
128
|
+
value.setValue(state.opacity);
|
|
129
|
+
if (!state.animate) return undefined;
|
|
130
|
+
const loop = Animated.loop(
|
|
131
|
+
Animated.sequence([
|
|
132
|
+
// `useNativeDriver` must be false on react-native-web, which is what
|
|
133
|
+
// Paseo's renderer is (delta 20260917e P3).
|
|
134
|
+
Animated.timing(value, { toValue: DIM_OPACITY, duration: PULSE_MS, useNativeDriver: false }),
|
|
135
|
+
Animated.timing(value, { toValue: 1, duration: PULSE_MS, useNativeDriver: false }),
|
|
136
|
+
]),
|
|
137
|
+
);
|
|
138
|
+
loop.start();
|
|
139
|
+
return () => loop.stop();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The operating system's "reduce motion" preference, false when the host cannot
|
|
144
|
+
* answer. A host that fails the query has not asked for less motion; reading
|
|
145
|
+
* that absence as a preference would quietly kill the pulse everywhere.
|
|
146
|
+
*/
|
|
147
|
+
export async function readReduceMotion(): Promise<boolean> {
|
|
148
|
+
try {
|
|
149
|
+
return await AccessibilityInfo.isReduceMotionEnabled();
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function useReduceMotion(): boolean {
|
|
156
|
+
const [reduceMotion, setReduceMotion] = useState(false);
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
let live = true;
|
|
159
|
+
void readReduceMotion().then((value) => {
|
|
160
|
+
if (live) setReduceMotion(value);
|
|
161
|
+
});
|
|
162
|
+
return () => {
|
|
163
|
+
live = false;
|
|
164
|
+
};
|
|
165
|
+
}, []);
|
|
166
|
+
return reduceMotion;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function RunningDot(props: {
|
|
170
|
+
counts: RunningAgentCounts | undefined;
|
|
171
|
+
theme: PluginTheme;
|
|
172
|
+
styles: ReturnType<typeof launcherStyles>;
|
|
173
|
+
}) {
|
|
174
|
+
const { counts, theme, styles } = props;
|
|
175
|
+
const reduceMotion = useReduceMotion();
|
|
176
|
+
const state = runningDotState(counts, reduceMotion);
|
|
177
|
+
const opacity = useRef(new Animated.Value(DIM_OPACITY)).current;
|
|
178
|
+
// Restarting on `animate` and `opacity` alone keeps the loop through a poll
|
|
179
|
+
// that only changed the counts, so the dot does not blink back to full.
|
|
180
|
+
useEffect(() => (state === null ? undefined : applyDotPulse(opacity, state)), [
|
|
181
|
+
opacity,
|
|
182
|
+
state?.animate,
|
|
183
|
+
state?.opacity,
|
|
184
|
+
]);
|
|
185
|
+
if (state === null) return null;
|
|
186
|
+
return (
|
|
187
|
+
<View style={styles.stat} accessibilityLabel={state.label}>
|
|
188
|
+
<Animated.View
|
|
189
|
+
style={{ width: 8, height: 8, borderRadius: 4, opacity, backgroundColor: toneColor(theme, state.tone) }}
|
|
190
|
+
/>
|
|
191
|
+
{/* An idle row says it with the dim dot alone; spelling out "nothing is
|
|
192
|
+
running" on every quiet project is noise. The accessibility label
|
|
193
|
+
above carries the words in both states. */}
|
|
194
|
+
{state.total === 0 ? null : (
|
|
195
|
+
<Text style={[styles.statText, { color: toneColor(theme, state.tone) }]} numberOfLines={1}>
|
|
196
|
+
{state.label}
|
|
197
|
+
</Text>
|
|
198
|
+
)}
|
|
199
|
+
</View>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The surface's status strip: a slash command's notice (tap to dismiss), a host
|
|
205
|
+
* too old to open agents, and the state of the last Manager launch. Built once
|
|
206
|
+
* by the surface and drawn on both the main screen (Setup) and the workspace
|
|
207
|
+
* list (delta 20260918e §4.2); the lines come from `launcherStatusLines`.
|
|
208
|
+
*/
|
|
209
|
+
function LauncherStatus({
|
|
210
|
+
lines,
|
|
211
|
+
onDismiss,
|
|
212
|
+
theme,
|
|
213
|
+
styles,
|
|
214
|
+
}: {
|
|
215
|
+
lines: readonly StatusLine[];
|
|
216
|
+
onDismiss: () => void;
|
|
217
|
+
theme: PluginTheme;
|
|
218
|
+
styles: ReturnType<typeof launcherStyles>;
|
|
219
|
+
}) {
|
|
220
|
+
return (
|
|
221
|
+
<>
|
|
222
|
+
{lines.map((line) =>
|
|
223
|
+
line.dismissable ? (
|
|
224
|
+
<Pressable
|
|
225
|
+
key={line.key}
|
|
226
|
+
accessibilityRole="button"
|
|
227
|
+
accessibilityLabel={`${line.text}. Dismiss.`}
|
|
228
|
+
onPress={onDismiss}
|
|
229
|
+
style={styles.row}
|
|
230
|
+
>
|
|
231
|
+
<Text style={styles.rowSubtitle}>{line.text}</Text>
|
|
232
|
+
</Pressable>
|
|
233
|
+
) : (
|
|
234
|
+
<Text
|
|
235
|
+
key={line.key}
|
|
236
|
+
accessibilityLiveRegion="polite"
|
|
237
|
+
style={[styles.notice, { color: toneColor(theme, line.tone) }]}
|
|
238
|
+
>
|
|
239
|
+
{line.text}
|
|
240
|
+
</Text>
|
|
241
|
+
),
|
|
242
|
+
)}
|
|
243
|
+
</>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function ManagerLauncherSurface(props: PluginSurfaceProps) {
|
|
248
|
+
const { theme, layout, navigation } = props;
|
|
249
|
+
const paseo = usePaseo();
|
|
250
|
+
// The Dashboard shares this surface (Q-036), so the view is state here and the
|
|
251
|
+
// Command Center hand-off is a queue, exactly like the launch request below.
|
|
252
|
+
// It opens on Setup, the main screen (delta 20260918e §4.2).
|
|
253
|
+
const [view, setView] = useState<DashboardViewName>(SURFACE_HOME_VIEW);
|
|
254
|
+
const [dashboardWorkspace, setDashboardWorkspace] = useState<{ id: string; label: string } | null>(null);
|
|
255
|
+
const ensure = useRpc(managerEnsureRpc);
|
|
256
|
+
const listStored = useRpc(tracesWorkspacesRpc);
|
|
257
|
+
const stored = useQuery({
|
|
258
|
+
queryKey: ["paseo-bm", "launcher", "stored-workspaces"],
|
|
259
|
+
queryFn: () => listStored({}),
|
|
260
|
+
});
|
|
261
|
+
const listOverview = useRpc(workspacesOverviewRpc);
|
|
262
|
+
// Bead counts and running Workers; read and refreshed only while the
|
|
263
|
+
// workspace list shows them (delta 20260918f F5).
|
|
264
|
+
const overview = useQuery({
|
|
265
|
+
queryKey: ["paseo-bm", "launcher", "overview"],
|
|
266
|
+
queryFn: () => listOverview({}),
|
|
267
|
+
...overviewPolling(view),
|
|
268
|
+
});
|
|
269
|
+
const overviewById = useMemo(
|
|
270
|
+
() => new Map((overview.data?.workspaces ?? []).map((entry) => [entry.workspaceId, entry])),
|
|
271
|
+
[overview.data],
|
|
272
|
+
);
|
|
273
|
+
const openAgent = navigation?.openAgent;
|
|
274
|
+
const state = useSyncExternalStore(
|
|
275
|
+
managerLauncher.subscribe,
|
|
276
|
+
managerLauncher.getState,
|
|
277
|
+
managerLauncher.getState,
|
|
278
|
+
);
|
|
279
|
+
// What a slash command had to say. It has no channel of its own except an
|
|
280
|
+
// error toast, which would paint a success red (dashboard-view.ts).
|
|
281
|
+
const commandNotice = useSyncExternalStore(
|
|
282
|
+
launcherNotices.subscribe,
|
|
283
|
+
launcherNotices.peek,
|
|
284
|
+
launcherNotices.peek,
|
|
285
|
+
);
|
|
286
|
+
const styles = useMemo(() => launcherStyles(theme, layout.compact), [theme, layout.compact]);
|
|
287
|
+
|
|
288
|
+
const workspaces = useQuery({
|
|
289
|
+
queryKey: ["paseo-bm", "launcher", "workspaces"],
|
|
290
|
+
queryFn: async (): Promise<WorkspaceRow[]> => {
|
|
291
|
+
const result = await paseo.workspaces.list({
|
|
292
|
+
sort: [{ key: "activity_at", direction: "desc" }],
|
|
293
|
+
});
|
|
294
|
+
return result.entries
|
|
295
|
+
.filter((workspace) => !workspace.archivingAt)
|
|
296
|
+
.map((workspace) => ({
|
|
297
|
+
id: workspace.id,
|
|
298
|
+
label: workspace.title || workspace.name,
|
|
299
|
+
detail: workspace.projectDisplayName,
|
|
300
|
+
screenTitle: screenTitleOf(workspace.title || workspace.name, workspace.projectDisplayName),
|
|
301
|
+
}));
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Open the Dashboard when the Command Center queued one.
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
const run = () => {
|
|
308
|
+
const workspaceId = dashboardRequests.take();
|
|
309
|
+
if (workspaceId === null) return;
|
|
310
|
+
const known = workspaces.data?.find((workspace) => workspace.id === workspaceId);
|
|
311
|
+
setDashboardWorkspace({ id: workspaceId, label: known?.screenTitle ?? workspaceId });
|
|
312
|
+
setView("dashboard");
|
|
313
|
+
};
|
|
314
|
+
run();
|
|
315
|
+
return dashboardRequests.subscribe(run);
|
|
316
|
+
}, [workspaces.data]);
|
|
317
|
+
|
|
318
|
+
// Run a launch queued by the Command Center item, now, whenever one arrives,
|
|
319
|
+
// and when a pending launch ends (a request that came meanwhile waited).
|
|
320
|
+
useEffect(() => {
|
|
321
|
+
const run = () => {
|
|
322
|
+
void runPendingRequest(launchRequests, managerLauncher, { ensure, openAgent });
|
|
323
|
+
};
|
|
324
|
+
run();
|
|
325
|
+
const stopRequests = launchRequests.subscribe(run);
|
|
326
|
+
const stopLauncher = managerLauncher.subscribe(run);
|
|
327
|
+
return () => {
|
|
328
|
+
stopRequests();
|
|
329
|
+
stopLauncher();
|
|
330
|
+
};
|
|
331
|
+
}, [ensure, openAgent]);
|
|
332
|
+
|
|
333
|
+
const pending = state.status === "pending";
|
|
334
|
+
// Every ← goes where `backOf` says: Metric and Beads to the workspace list,
|
|
335
|
+
// the list to Setup. Setup is the main screen and has no ←.
|
|
336
|
+
const goBack = () => setView(backOf(view) ?? SURFACE_HOME_VIEW);
|
|
337
|
+
|
|
338
|
+
// Built ONCE and placed on every view of the surface (Setup, the workspace
|
|
339
|
+
// list, Metric, Beads), so a slash command's notice and a launch error are
|
|
340
|
+
// seen wherever the surface is.
|
|
341
|
+
const status = (
|
|
342
|
+
<LauncherStatus
|
|
343
|
+
lines={launcherStatusLines({ commandNotice, canOpenAgents: openAgent !== undefined, state })}
|
|
344
|
+
onDismiss={() => launcherNotices.take()}
|
|
345
|
+
theme={theme}
|
|
346
|
+
styles={styles}
|
|
347
|
+
/>
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
if (view === "setup") {
|
|
351
|
+
return <SetupScreen {...props} onOpenWorkspaces={() => setView("workspaces")} status={status} />;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (view === "beads" && dashboardWorkspace !== null) {
|
|
355
|
+
return (
|
|
356
|
+
<BeadsScreen
|
|
357
|
+
{...props}
|
|
358
|
+
workspaceId={dashboardWorkspace.id}
|
|
359
|
+
workspaceLabel={dashboardWorkspace.label}
|
|
360
|
+
onBack={goBack}
|
|
361
|
+
backLabel={backLabelOf(view) ?? undefined}
|
|
362
|
+
status={status}
|
|
363
|
+
/>
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (view === "dashboard" && dashboardWorkspace !== null) {
|
|
368
|
+
return (
|
|
369
|
+
<DashboardPanel
|
|
370
|
+
{...props}
|
|
371
|
+
workspaceId={dashboardWorkspace.id}
|
|
372
|
+
workspaceLabel={dashboardWorkspace.label}
|
|
373
|
+
onBack={goBack}
|
|
374
|
+
backLabel={backLabelOf(view) ?? undefined}
|
|
375
|
+
status={status}
|
|
376
|
+
/>
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
<ScrollView style={styles.screen} contentContainerStyle={styles.content}>
|
|
382
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
383
|
+
<Pressable
|
|
384
|
+
accessibilityRole="button"
|
|
385
|
+
accessibilityLabel={backLabelOf(view) ?? undefined}
|
|
386
|
+
onPress={goBack}
|
|
387
|
+
style={styles.iconButton}
|
|
388
|
+
>
|
|
389
|
+
<Text style={styles.rowTitle}>←</Text>
|
|
390
|
+
</Pressable>
|
|
391
|
+
<Text style={[styles.title, { flex: 1 }]}>Workspaces</Text>
|
|
392
|
+
</View>
|
|
393
|
+
<Text style={styles.body}>Chat with a workspace's Beads Manager, or open its metrics or beads.</Text>
|
|
394
|
+
|
|
395
|
+
{status}
|
|
396
|
+
|
|
397
|
+
{workspaces.isPending ? <ActivityIndicator color={styles.spinner.color} /> : null}
|
|
398
|
+
{workspaces.isError ? (
|
|
399
|
+
<Text style={[styles.notice, { color: toneColor(theme, "danger") }]}>
|
|
400
|
+
{`Could not load workspaces. ${errorMessageOf(workspaces.error)}`}
|
|
401
|
+
</Text>
|
|
402
|
+
) : null}
|
|
403
|
+
{workspaces.data?.length === 0 ? (
|
|
404
|
+
<Text style={styles.body}>No workspaces on this host yet.</Text>
|
|
405
|
+
) : null}
|
|
406
|
+
|
|
407
|
+
{/* The rows render even without `navigation.openAgent`: only the Manager
|
|
408
|
+
button needs it, while the Dashboard is read-only and must stay
|
|
409
|
+
reachable on an older host (REQ-040d). They keep the order
|
|
410
|
+
`workspaces.list` returns: most recent activity first, no pinning
|
|
411
|
+
(delta 20260918f §4.5). */}
|
|
412
|
+
{(workspaces.data ?? []).map((workspace) => {
|
|
413
|
+
const busyHere = pending && state.workspaceId === workspace.id;
|
|
414
|
+
const open = (view: "dashboard" | "beads") => {
|
|
415
|
+
setDashboardWorkspace({ id: workspace.id, label: workspace.screenTitle });
|
|
416
|
+
setView(view);
|
|
417
|
+
};
|
|
418
|
+
const stats = workspaceStats(overviewById.get(workspace.id));
|
|
419
|
+
return (
|
|
420
|
+
<View key={workspace.id} style={styles.row}>
|
|
421
|
+
<View style={{ gap: 4, flexShrink: 1 }}>
|
|
422
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
423
|
+
<Text style={[styles.rowTitle, { flexShrink: 1 }]} numberOfLines={1}>
|
|
424
|
+
{workspace.label}
|
|
425
|
+
</Text>
|
|
426
|
+
<RunningDot counts={overviewById.get(workspace.id)?.runningAgents} theme={theme} styles={styles} />
|
|
427
|
+
</View>
|
|
428
|
+
<Text style={styles.rowSubtitle} numberOfLines={1}>
|
|
429
|
+
{workspace.detail}
|
|
430
|
+
</Text>
|
|
431
|
+
{stats.length === 0 ? null : (
|
|
432
|
+
<View style={styles.stats} accessibilityLabel={stats.map((stat) => stat.label).join(", ")}>
|
|
433
|
+
{stats.map((stat) => (
|
|
434
|
+
<View key={stat.key} style={styles.stat}>
|
|
435
|
+
<Icon name={stat.icon} size={13} color={toneColor(theme, stat.tone)} />
|
|
436
|
+
<Text style={[styles.statText, { color: toneColor(theme, stat.tone) }]}>{stat.value}</Text>
|
|
437
|
+
</View>
|
|
438
|
+
))}
|
|
439
|
+
</View>
|
|
440
|
+
)}
|
|
441
|
+
</View>
|
|
442
|
+
{/* The rows render even without `navigation.openAgent`: only the
|
|
443
|
+
Manager action needs it, while Metric and Beads are read-only
|
|
444
|
+
and must stay reachable on an older host (REQ-040d). */}
|
|
445
|
+
<View style={styles.actions}>
|
|
446
|
+
{WORKSPACE_ACTIONS.map((action) => {
|
|
447
|
+
if (action.key === "manager" && !openAgent) return null;
|
|
448
|
+
const primary = action.key === "manager";
|
|
449
|
+
const disabled = primary && pending;
|
|
450
|
+
const textStyle = primary ? styles.actionPrimaryText : styles.actionText;
|
|
451
|
+
return (
|
|
452
|
+
<Pressable
|
|
453
|
+
key={action.key}
|
|
454
|
+
accessibilityRole="button"
|
|
455
|
+
accessibilityLabel={
|
|
456
|
+
action.key === "manager"
|
|
457
|
+
? `Go to the Beads Manager of ${workspace.label}`
|
|
458
|
+
: action.key === "metric"
|
|
459
|
+
? `Open the Beads metrics for ${workspace.label}`
|
|
460
|
+
: `Open the beads of ${workspace.label}`
|
|
461
|
+
}
|
|
462
|
+
accessibilityState={primary ? { disabled, busy: busyHere } : undefined}
|
|
463
|
+
disabled={disabled}
|
|
464
|
+
onPress={() => {
|
|
465
|
+
if (action.key === "manager") void managerLauncher.launch(workspace.id, { ensure, openAgent: openAgent! });
|
|
466
|
+
else open(action.key === "metric" ? "dashboard" : "beads");
|
|
467
|
+
}}
|
|
468
|
+
style={[styles.action, primary ? styles.actionPrimary : null, disabled ? styles.buttonDisabled : null]}
|
|
469
|
+
>
|
|
470
|
+
{primary && busyHere ? (
|
|
471
|
+
<ActivityIndicator color={textStyle.color} />
|
|
472
|
+
) : (
|
|
473
|
+
<>
|
|
474
|
+
<Icon name={action.icon} size={14} color={textStyle.color} />
|
|
475
|
+
<Text style={textStyle}>{action.label}</Text>
|
|
476
|
+
</>
|
|
477
|
+
)}
|
|
478
|
+
</Pressable>
|
|
479
|
+
);
|
|
480
|
+
})}
|
|
481
|
+
</View>
|
|
482
|
+
</View>
|
|
483
|
+
);
|
|
484
|
+
})}
|
|
485
|
+
|
|
486
|
+
{/* History of workspaces Paseo no longer lists: still readable, and
|
|
487
|
+
movable onto the reopened workspace from its Metric screen. */}
|
|
488
|
+
{closedWorkspaces(stored.data?.workspaces ?? [], workspaces.data?.map((workspace) => workspace.id) ?? []).map(
|
|
489
|
+
(closed, index) => (
|
|
490
|
+
<View key={closed.workspaceId} style={{ gap: 6 }}>
|
|
491
|
+
{index === 0 ? <Text style={styles.title}>Closed workspaces with history</Text> : null}
|
|
492
|
+
<View style={styles.row}>
|
|
493
|
+
<View style={{ flex: 1 }}>
|
|
494
|
+
<Text style={styles.rowTitle}>{closed.label}</Text>
|
|
495
|
+
<Text style={styles.rowSubtitle}>{closed.detail}</Text>
|
|
496
|
+
</View>
|
|
497
|
+
<Pressable
|
|
498
|
+
accessibilityRole="button"
|
|
499
|
+
accessibilityLabel={`Open the Beads metrics of the closed workspace ${closed.label}`}
|
|
500
|
+
onPress={() => {
|
|
501
|
+
setDashboardWorkspace({ id: closed.workspaceId, label: closed.label });
|
|
502
|
+
setView("dashboard");
|
|
503
|
+
}}
|
|
504
|
+
style={styles.button}
|
|
505
|
+
>
|
|
506
|
+
<Text style={styles.buttonText}>Metric</Text>
|
|
507
|
+
</Pressable>
|
|
508
|
+
</View>
|
|
509
|
+
</View>
|
|
510
|
+
),
|
|
511
|
+
)}
|
|
512
|
+
|
|
513
|
+
<Text style={styles.footer}>{`paseo-bm ${PLUGIN_VERSION}`}</Text>
|
|
514
|
+
</ScrollView>
|
|
515
|
+
);
|
|
516
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Draws the blocks from `markdown.ts` with React Native primitives.
|
|
3
|
+
* Colours come from the theme; sizes from the Dashboard styles.
|
|
4
|
+
*/
|
|
5
|
+
import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
|
|
6
|
+
import { Text, View } from "react-native";
|
|
7
|
+
import { parseMarkdown, type Inline, type MarkdownBlock } from "./markdown";
|
|
8
|
+
|
|
9
|
+
type Theme = PluginSurfaceProps["theme"];
|
|
10
|
+
|
|
11
|
+
const MONO = "Menlo";
|
|
12
|
+
|
|
13
|
+
function Spans({ spans, theme, size, weight }: { spans: Inline[]; theme: Theme; size: number; weight?: "600" | "700" }) {
|
|
14
|
+
return (
|
|
15
|
+
<Text style={{ color: theme.colors.foreground, fontSize: size, lineHeight: size * 1.45, fontWeight: weight }} selectable>
|
|
16
|
+
{spans.map((span, index) => (
|
|
17
|
+
<Text
|
|
18
|
+
key={index}
|
|
19
|
+
style={
|
|
20
|
+
span.code
|
|
21
|
+
? { fontFamily: MONO, fontSize: size - 1, backgroundColor: theme.colors.surface2, color: theme.colors.foreground }
|
|
22
|
+
: { fontWeight: span.bold ? "700" : weight, fontStyle: span.italic ? "italic" : "normal" }
|
|
23
|
+
}
|
|
24
|
+
>
|
|
25
|
+
{span.text}
|
|
26
|
+
</Text>
|
|
27
|
+
))}
|
|
28
|
+
</Text>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function Block({ block, theme, size }: { block: MarkdownBlock; theme: Theme; size: number }) {
|
|
33
|
+
switch (block.kind) {
|
|
34
|
+
case "heading":
|
|
35
|
+
return (
|
|
36
|
+
<View style={{ marginTop: block.level <= 2 ? 10 : 6 }}>
|
|
37
|
+
<Spans spans={block.spans} theme={theme} size={size + Math.max(0, 6 - block.level * 1.5)} weight="700" />
|
|
38
|
+
</View>
|
|
39
|
+
);
|
|
40
|
+
case "paragraph":
|
|
41
|
+
return <Spans spans={block.spans} theme={theme} size={size} />;
|
|
42
|
+
case "bullet":
|
|
43
|
+
case "numbered": {
|
|
44
|
+
const marker =
|
|
45
|
+
block.kind === "numbered" ? `${block.number}.` : block.checked === undefined ? "•" : block.checked ? "☑" : "☐";
|
|
46
|
+
return (
|
|
47
|
+
<View style={{ flexDirection: "row", gap: 6, paddingLeft: block.depth * 14 }}>
|
|
48
|
+
<Text style={{ color: theme.colors.foregroundMuted, fontSize: size, lineHeight: size * 1.45, minWidth: 14 }}>
|
|
49
|
+
{marker}
|
|
50
|
+
</Text>
|
|
51
|
+
<View style={{ flex: 1 }}>
|
|
52
|
+
<Spans spans={block.spans} theme={theme} size={size} />
|
|
53
|
+
</View>
|
|
54
|
+
</View>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
case "quote":
|
|
58
|
+
return (
|
|
59
|
+
<View style={{ borderLeftWidth: 3, borderLeftColor: theme.colors.border, paddingLeft: 8 }}>
|
|
60
|
+
<Spans spans={block.spans} theme={theme} size={size} />
|
|
61
|
+
</View>
|
|
62
|
+
);
|
|
63
|
+
case "code":
|
|
64
|
+
return (
|
|
65
|
+
<View style={{ backgroundColor: theme.colors.surface2, borderRadius: 6, padding: 8 }}>
|
|
66
|
+
<Text style={{ fontFamily: MONO, fontSize: size - 1, color: theme.colors.foreground }} selectable>
|
|
67
|
+
{block.text}
|
|
68
|
+
</Text>
|
|
69
|
+
</View>
|
|
70
|
+
);
|
|
71
|
+
case "table":
|
|
72
|
+
return (
|
|
73
|
+
<View style={{ borderWidth: 1, borderColor: theme.colors.border, borderRadius: 6 }}>
|
|
74
|
+
{[block.header, ...block.rows].map((row, rowIndex) => (
|
|
75
|
+
<View
|
|
76
|
+
key={rowIndex}
|
|
77
|
+
style={{
|
|
78
|
+
flexDirection: "row",
|
|
79
|
+
borderTopWidth: rowIndex === 0 ? 0 : 1,
|
|
80
|
+
borderTopColor: theme.colors.border,
|
|
81
|
+
backgroundColor: rowIndex === 0 ? theme.colors.surface2 : "transparent",
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{row.map((cell, cellIndex) => (
|
|
85
|
+
<View key={cellIndex} style={{ flex: 1, padding: 6 }}>
|
|
86
|
+
<Text
|
|
87
|
+
style={{ color: theme.colors.foreground, fontSize: size - 1, fontWeight: rowIndex === 0 ? "600" : "400" }}
|
|
88
|
+
selectable
|
|
89
|
+
>
|
|
90
|
+
{cell}
|
|
91
|
+
</Text>
|
|
92
|
+
</View>
|
|
93
|
+
))}
|
|
94
|
+
</View>
|
|
95
|
+
))}
|
|
96
|
+
</View>
|
|
97
|
+
);
|
|
98
|
+
case "rule":
|
|
99
|
+
return <View style={{ height: 1, backgroundColor: theme.colors.border, marginVertical: 4 }} />;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function MarkdownView({ source, theme, compact }: { source: string; theme: Theme; compact: boolean }) {
|
|
104
|
+
const size = compact ? 12 : 14;
|
|
105
|
+
return (
|
|
106
|
+
<View style={{ gap: 6 }}>
|
|
107
|
+
{parseMarkdown(source).map((block, index) => (
|
|
108
|
+
<Block key={index} block={block} theme={theme} size={size} />
|
|
109
|
+
))}
|
|
110
|
+
</View>
|
|
111
|
+
);
|
|
112
|
+
}
|