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
|
+
* The Beads screen (design delta 20260916-beads-screen): five overview
|
|
3
|
+
* sections, a filterable bead list, and a bead detail with three hand-off
|
|
4
|
+
* actions. Every action is confirmed first and goes to the workspace's Beads
|
|
5
|
+
* Manager; this screen never writes the bead store.
|
|
6
|
+
*
|
|
7
|
+
* Client rules: React Native primitives only, colours from `theme.colors`, no
|
|
8
|
+
* Node import, no `server/` import.
|
|
9
|
+
*/
|
|
10
|
+
import { type PluginSurfaceProps, useRpc } from "@getpaseo/plugin/client";
|
|
11
|
+
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
12
|
+
import { useQuery } from "@tanstack/react-query";
|
|
13
|
+
import { useMemo, useState, useSyncExternalStore } from "react";
|
|
14
|
+
import { ActivityIndicator, Pressable, ScrollView, Text, TextInput, View } from "react-native";
|
|
15
|
+
import { beadsActionRpc, beadsGetRpc, beadsListRpc, type BeadAction, type BeadRow } from "../shared/contracts";
|
|
16
|
+
import { MarkdownView } from "./markdown-view";
|
|
17
|
+
import {
|
|
18
|
+
EMPTY_FILTER,
|
|
19
|
+
LABEL_PREVIEW,
|
|
20
|
+
SORT_OPTIONS,
|
|
21
|
+
activeFilters,
|
|
22
|
+
previewValues,
|
|
23
|
+
sortBeads,
|
|
24
|
+
type FacetValue,
|
|
25
|
+
type SortKey,
|
|
26
|
+
actionSpec,
|
|
27
|
+
actionsFor,
|
|
28
|
+
beadsOverview,
|
|
29
|
+
closedBeadsVisibility,
|
|
30
|
+
doneText,
|
|
31
|
+
beadListItems,
|
|
32
|
+
groupBeads,
|
|
33
|
+
facetsOf,
|
|
34
|
+
filterBeads,
|
|
35
|
+
priorityLabel,
|
|
36
|
+
statusBadge,
|
|
37
|
+
toggle,
|
|
38
|
+
workSummary,
|
|
39
|
+
type BeadFilter,
|
|
40
|
+
} from "./beads-model";
|
|
41
|
+
import { dashboardStyles, toneColor, type Badge } from "./dashboard-model";
|
|
42
|
+
import { errorMessageOf } from "./launch-manager";
|
|
43
|
+
import { BarChart, BeadRowCard, Chip, RoleMark, StatCards, WorkspaceScreenHeader, type Styles, type Theme, type WorkspaceScreenProps } from "./ui";
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
const LIST_LIMIT = 200;
|
|
47
|
+
|
|
48
|
+
const facetText = (value: string) => value.replace("in_progress", "in progress");
|
|
49
|
+
|
|
50
|
+
function FacetRow({
|
|
51
|
+
title,
|
|
52
|
+
values,
|
|
53
|
+
selected,
|
|
54
|
+
onToggle,
|
|
55
|
+
styles,
|
|
56
|
+
theme,
|
|
57
|
+
expanded,
|
|
58
|
+
onExpand,
|
|
59
|
+
}: {
|
|
60
|
+
title: string;
|
|
61
|
+
values: readonly FacetValue[];
|
|
62
|
+
selected: ReadonlySet<string>;
|
|
63
|
+
onToggle: (value: string) => void;
|
|
64
|
+
styles: Styles;
|
|
65
|
+
theme: Theme;
|
|
66
|
+
/** Label groups preview a few values; the others show everything. */
|
|
67
|
+
expanded?: boolean;
|
|
68
|
+
onExpand?: () => void;
|
|
69
|
+
}) {
|
|
70
|
+
if (values.length === 0) return null;
|
|
71
|
+
const { shown, hidden } =
|
|
72
|
+
expanded === undefined ? { shown: values, hidden: 0 } : previewValues(values, selected, expanded);
|
|
73
|
+
return (
|
|
74
|
+
<View style={{ gap: 4 }}>
|
|
75
|
+
<Text style={styles.body}>{title}</Text>
|
|
76
|
+
<View style={styles.chipRow}>
|
|
77
|
+
{shown.map((entry) => (
|
|
78
|
+
<Chip
|
|
79
|
+
key={entry.value}
|
|
80
|
+
badge={{ text: `${facetText(entry.value)} ${entry.count}`, tone: selected.has(entry.value) ? "info" : "muted" }}
|
|
81
|
+
selected={selected.has(entry.value)}
|
|
82
|
+
onPress={() => onToggle(entry.value)}
|
|
83
|
+
styles={styles}
|
|
84
|
+
theme={theme}
|
|
85
|
+
/>
|
|
86
|
+
))}
|
|
87
|
+
{hidden > 0 || (expanded === true && values.length > LABEL_PREVIEW) ? (
|
|
88
|
+
<Pressable accessibilityRole="button" onPress={onExpand}>
|
|
89
|
+
<Text style={[styles.badge, { color: toneColor(theme, "info"), paddingVertical: 2 }]}>
|
|
90
|
+
{hidden > 0 ? `+${hidden} more` : "show less"}
|
|
91
|
+
</Text>
|
|
92
|
+
</Pressable>
|
|
93
|
+
) : null}
|
|
94
|
+
</View>
|
|
95
|
+
</View>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A bead's detail and actions; also opened from a chat card or the chat panel. */
|
|
100
|
+
export function BeadDetailPanel({
|
|
101
|
+
workspaceId,
|
|
102
|
+
bead,
|
|
103
|
+
styles,
|
|
104
|
+
theme,
|
|
105
|
+
navigation,
|
|
106
|
+
}: {
|
|
107
|
+
workspaceId: string;
|
|
108
|
+
bead: BeadRow;
|
|
109
|
+
styles: Styles;
|
|
110
|
+
theme: Theme;
|
|
111
|
+
navigation?: PluginSurfaceProps["navigation"];
|
|
112
|
+
}) {
|
|
113
|
+
const getBead = useRpc(beadsGetRpc);
|
|
114
|
+
const runAction = useRpc(beadsActionRpc);
|
|
115
|
+
const [pending, setPending] = useState<BeadAction | null>(null);
|
|
116
|
+
const [busy, setBusy] = useState(false);
|
|
117
|
+
const [result, setResult] = useState<{ text: string; managerId: string | null; tone: Badge["tone"] } | null>(null);
|
|
118
|
+
const detail = useQuery({
|
|
119
|
+
queryKey: ["paseo-bm", "bead", workspaceId, bead.id],
|
|
120
|
+
queryFn: () => getBead({ workspaceId, id: bead.id }),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const confirm = async (action: BeadAction) => {
|
|
124
|
+
setBusy(true);
|
|
125
|
+
try {
|
|
126
|
+
const sent = await runAction({ workspaceId, id: bead.id, action });
|
|
127
|
+
setResult({
|
|
128
|
+
text: `Sent to the Beads Manager${sent.created ? " (created for this workspace)" : ""}. It will hand the bead to a Worker.`,
|
|
129
|
+
managerId: sent.managerId,
|
|
130
|
+
tone: "success",
|
|
131
|
+
});
|
|
132
|
+
} catch (failure) {
|
|
133
|
+
setResult({ text: errorMessageOf(failure), managerId: null, tone: "danger" });
|
|
134
|
+
} finally {
|
|
135
|
+
setBusy(false);
|
|
136
|
+
setPending(null);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const full = detail.data?.bead;
|
|
141
|
+
const work = workSummary(bead, new Date());
|
|
142
|
+
const spec = pending === null ? null : actionSpec(pending, bead);
|
|
143
|
+
return (
|
|
144
|
+
<View style={{ gap: 6, paddingTop: 6 }}>
|
|
145
|
+
{detail.isPending ? <ActivityIndicator color={styles.spinner.color} /> : null}
|
|
146
|
+
{detail.isError ? (
|
|
147
|
+
<Text style={[styles.body, { color: toneColor(theme, "danger") }]}>{errorMessageOf(detail.error)}</Text>
|
|
148
|
+
) : null}
|
|
149
|
+
{full === undefined ? null : (
|
|
150
|
+
<>
|
|
151
|
+
<View style={styles.chipRow}>
|
|
152
|
+
{full.labels.map((label) => (
|
|
153
|
+
<Chip key={label} badge={{ text: label, tone: "info" }} styles={styles} theme={theme} />
|
|
154
|
+
))}
|
|
155
|
+
</View>
|
|
156
|
+
<Text style={styles.body}>
|
|
157
|
+
{[
|
|
158
|
+
full.createdAt === null ? null : `created ${full.createdAt.slice(0, 16).replace("T", " ")}`,
|
|
159
|
+
full.updatedAt === null ? null : `updated ${full.updatedAt.slice(0, 16).replace("T", " ")}`,
|
|
160
|
+
full.closedAt === null ? null : `closed ${full.closedAt.slice(0, 16).replace("T", " ")}`,
|
|
161
|
+
full.parentId === null ? null : `parent ${full.parentId}`,
|
|
162
|
+
full.blockedBy.length === 0 ? null : `blocked by ${full.blockedBy.join(", ")}`,
|
|
163
|
+
full.children.length === 0 ? null : `${full.children.length} child bead(s)`,
|
|
164
|
+
]
|
|
165
|
+
.filter((part) => part !== null)
|
|
166
|
+
.join(" · ")}
|
|
167
|
+
</Text>
|
|
168
|
+
{work === null ? null : (
|
|
169
|
+
<View style={[styles.card, { gap: 4 }]}>
|
|
170
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
|
171
|
+
<RoleMark kind="worker" theme={theme} />
|
|
172
|
+
<Text style={styles.sectionTitle}>Being worked on</Text>
|
|
173
|
+
</View>
|
|
174
|
+
{work.lines.map((line) => (
|
|
175
|
+
<Text key={line} style={styles.body}>
|
|
176
|
+
{line}
|
|
177
|
+
</Text>
|
|
178
|
+
))}
|
|
179
|
+
{work.agentId !== null && navigation?.openAgent !== undefined ? (
|
|
180
|
+
<Pressable
|
|
181
|
+
accessibilityRole="button"
|
|
182
|
+
onPress={() => navigation.openAgent({ agentId: work.agentId! })}
|
|
183
|
+
style={[styles.secondaryButton, { alignSelf: "flex-start" }]}
|
|
184
|
+
>
|
|
185
|
+
<Text style={styles.secondaryButtonText}>Open the Worker</Text>
|
|
186
|
+
</Pressable>
|
|
187
|
+
) : null}
|
|
188
|
+
</View>
|
|
189
|
+
)}
|
|
190
|
+
{full.closeReason === null ? null : <Text style={styles.body} selectable>{`Close reason: ${full.closeReason}`}</Text>}
|
|
191
|
+
{full.description === null ? (
|
|
192
|
+
<Text style={styles.body}>(no description)</Text>
|
|
193
|
+
) : (
|
|
194
|
+
<View style={[styles.card, { backgroundColor: theme.colors.surface0 }]}>
|
|
195
|
+
<MarkdownView source={full.description} theme={theme} compact={false} />
|
|
196
|
+
</View>
|
|
197
|
+
)}
|
|
198
|
+
</>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
{result === null ? null : (
|
|
202
|
+
<View style={{ gap: 4 }}>
|
|
203
|
+
<Text style={[styles.body, { color: toneColor(theme, result.tone) }]}>{result.text}</Text>
|
|
204
|
+
{result.managerId !== null && navigation?.openAgent !== undefined ? (
|
|
205
|
+
<Pressable
|
|
206
|
+
accessibilityRole="button"
|
|
207
|
+
onPress={() => navigation.openAgent({ agentId: result.managerId! })}
|
|
208
|
+
style={[styles.secondaryButton, { alignSelf: "flex-start" }]}
|
|
209
|
+
>
|
|
210
|
+
<Text style={styles.secondaryButtonText}>Open the Beads Manager</Text>
|
|
211
|
+
</Pressable>
|
|
212
|
+
) : null}
|
|
213
|
+
</View>
|
|
214
|
+
)}
|
|
215
|
+
|
|
216
|
+
{spec === null ? (
|
|
217
|
+
<View style={styles.chipRow}>
|
|
218
|
+
{actionsFor(bead).map((action) => {
|
|
219
|
+
const label = actionSpec(action, bead);
|
|
220
|
+
return (
|
|
221
|
+
<Pressable
|
|
222
|
+
key={action}
|
|
223
|
+
accessibilityRole="button"
|
|
224
|
+
disabled={busy}
|
|
225
|
+
onPress={() => {
|
|
226
|
+
setResult(null);
|
|
227
|
+
setPending(action);
|
|
228
|
+
}}
|
|
229
|
+
style={label.danger ? styles.dangerButton : styles.button}
|
|
230
|
+
>
|
|
231
|
+
<Text style={label.danger ? styles.dangerButtonText : styles.buttonText}>{label.label}</Text>
|
|
232
|
+
</Pressable>
|
|
233
|
+
);
|
|
234
|
+
})}
|
|
235
|
+
</View>
|
|
236
|
+
) : (
|
|
237
|
+
<View style={styles.card}>
|
|
238
|
+
<Text style={styles.sectionTitle}>{spec.title}</Text>
|
|
239
|
+
<Text style={styles.body}>{spec.body}</Text>
|
|
240
|
+
<View style={styles.chipRow}>
|
|
241
|
+
{/* Cancel first: the safe choice is the one under the thumb. */}
|
|
242
|
+
<Pressable accessibilityRole="button" onPress={() => setPending(null)} style={styles.secondaryButton}>
|
|
243
|
+
<Text style={styles.secondaryButtonText}>No, cancel</Text>
|
|
244
|
+
</Pressable>
|
|
245
|
+
<Pressable
|
|
246
|
+
accessibilityRole="button"
|
|
247
|
+
disabled={busy}
|
|
248
|
+
onPress={() => {
|
|
249
|
+
void confirm(pending!);
|
|
250
|
+
}}
|
|
251
|
+
style={spec.danger ? styles.dangerButton : styles.button}
|
|
252
|
+
>
|
|
253
|
+
<Text style={spec.danger ? styles.dangerButtonText : styles.buttonText}>
|
|
254
|
+
{busy ? "Sending…" : spec.confirmLabel}
|
|
255
|
+
</Text>
|
|
256
|
+
</Pressable>
|
|
257
|
+
</View>
|
|
258
|
+
</View>
|
|
259
|
+
)}
|
|
260
|
+
</View>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function BeadsScreen({ theme, layout, navigation, workspaceId, workspaceLabel, onBack, backLabel, status }: WorkspaceScreenProps) {
|
|
265
|
+
const styles = useMemo(() => dashboardStyles(theme, layout.compact), [theme, layout.compact]);
|
|
266
|
+
const listBeads = useRpc(beadsListRpc);
|
|
267
|
+
const [filter, setFilter] = useState<BeadFilter>(EMPTY_FILTER);
|
|
268
|
+
const [openId, setOpenId] = useState<string | null>(null);
|
|
269
|
+
const [sortKey, setSortKey] = useState<SortKey>("updated");
|
|
270
|
+
const [descending, setDescending] = useState(true);
|
|
271
|
+
const [showLabels, setShowLabels] = useState(false);
|
|
272
|
+
const [expandedGroups, setExpandedGroups] = useState<ReadonlySet<string>>(new Set());
|
|
273
|
+
const beads = useQuery({
|
|
274
|
+
queryKey: ["paseo-bm", "beads-list", workspaceId],
|
|
275
|
+
queryFn: () => listBeads({ workspaceId }),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const rows = beads.data?.beads ?? [];
|
|
279
|
+
const facets = useMemo(() => facetsOf(rows, filter), [rows, filter]);
|
|
280
|
+
const shown = useMemo(() => sortBeads(filterBeads(rows, filter), sortKey, descending), [rows, filter, sortKey, descending]);
|
|
281
|
+
// Closed beads are hidden until asked for, for the rest of the app session (Q9).
|
|
282
|
+
const showClosed = useSyncExternalStore(closedBeadsVisibility.subscribe, closedBeadsVisibility.get, closedBeadsVisibility.get);
|
|
283
|
+
const grouped = useMemo(() => groupBeads(shown, { showClosed, limit: LIST_LIMIT }), [shown, showClosed]);
|
|
284
|
+
const active = activeFilters(filter);
|
|
285
|
+
const overview = beads.data === undefined ? null : beadsOverview(rows, beads.data.stats, new Date());
|
|
286
|
+
// Done / total at a glance, counted like the Progress card (Q11).
|
|
287
|
+
const done = overview === null ? null : doneText(overview.progress);
|
|
288
|
+
const now = new Date();
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<ScrollView style={styles.screen} contentContainerStyle={styles.content}>
|
|
292
|
+
<WorkspaceScreenHeader
|
|
293
|
+
title={workspaceLabel === undefined ? null : `Beads · ${workspaceLabel}`}
|
|
294
|
+
onBack={onBack}
|
|
295
|
+
backLabel={backLabel ?? "Back"}
|
|
296
|
+
status={status}
|
|
297
|
+
styles={styles}
|
|
298
|
+
right={
|
|
299
|
+
<>
|
|
300
|
+
{done === null ? null : (
|
|
301
|
+
<Text style={styles.body} accessibilityLabel={done.label}>
|
|
302
|
+
{done.text}
|
|
303
|
+
</Text>
|
|
304
|
+
)}
|
|
305
|
+
<Pressable accessibilityRole="button" onPress={() => void beads.refetch()} style={styles.secondaryButton}>
|
|
306
|
+
<Text style={styles.secondaryButtonText}>Refresh</Text>
|
|
307
|
+
</Pressable>
|
|
308
|
+
</>
|
|
309
|
+
}
|
|
310
|
+
/>
|
|
311
|
+
|
|
312
|
+
{beads.data === undefined ? null : (
|
|
313
|
+
<Text style={styles.body} selectable>{`Read from ${beads.data.stats.source}`}</Text>
|
|
314
|
+
)}
|
|
315
|
+
{beads.isPending ? <ActivityIndicator color={styles.spinner.color} /> : null}
|
|
316
|
+
{beads.isError ? (
|
|
317
|
+
<Text style={[styles.body, { color: toneColor(theme, "danger") }]}>{errorMessageOf(beads.error)}</Text>
|
|
318
|
+
) : null}
|
|
319
|
+
|
|
320
|
+
{overview === null ? null : (
|
|
321
|
+
<>
|
|
322
|
+
{/* 1. Status */}
|
|
323
|
+
<StatCards cards={overview.status} styles={styles} />
|
|
324
|
+
|
|
325
|
+
{/* 2. Progress */}
|
|
326
|
+
<View style={styles.card}>
|
|
327
|
+
<Text style={styles.sectionTitle}>Progress</Text>
|
|
328
|
+
<View style={styles.barTrack}>
|
|
329
|
+
<View style={[styles.barFill, { width: `${Math.round(overview.progress.share * 100)}%` }]} />
|
|
330
|
+
</View>
|
|
331
|
+
<Text style={styles.body}>{overview.progress.label}</Text>
|
|
332
|
+
</View>
|
|
333
|
+
|
|
334
|
+
<View style={styles.cards}>
|
|
335
|
+
<BarChart title="By type" bars={overview.byType} styles={styles} labelWidth={70} />
|
|
336
|
+
<BarChart title="By priority" bars={overview.byPriority} styles={styles} labelWidth={70} />
|
|
337
|
+
</View>
|
|
338
|
+
|
|
339
|
+
{/* 5. Time */}
|
|
340
|
+
<StatCards cards={overview.timing} styles={styles} />
|
|
341
|
+
</>
|
|
342
|
+
)}
|
|
343
|
+
|
|
344
|
+
{/* Filters */}
|
|
345
|
+
<View style={[styles.card, { gap: 8 }]}>
|
|
346
|
+
<TextInput
|
|
347
|
+
value={filter.text}
|
|
348
|
+
onChangeText={(text) => setFilter({ ...filter, text })}
|
|
349
|
+
placeholder="Search id or title"
|
|
350
|
+
placeholderTextColor={theme.colors.foregroundMuted}
|
|
351
|
+
style={[styles.body, { borderWidth: 1, borderColor: theme.colors.border, borderRadius: 8, padding: 8 }]}
|
|
352
|
+
/>
|
|
353
|
+
{active.length > 0 ? (
|
|
354
|
+
<View style={styles.chipRow}>
|
|
355
|
+
{active.map((entry) => (
|
|
356
|
+
<Chip
|
|
357
|
+
key={`${entry.facet}:${entry.value}`}
|
|
358
|
+
badge={{ text: `${facetText(entry.value)} ✕`, tone: "info" }}
|
|
359
|
+
selected
|
|
360
|
+
onPress={() => setFilter({ ...filter, [entry.facet]: toggle(filter[entry.facet], entry.value) })}
|
|
361
|
+
styles={styles}
|
|
362
|
+
theme={theme}
|
|
363
|
+
/>
|
|
364
|
+
))}
|
|
365
|
+
<Pressable accessibilityRole="button" onPress={() => setFilter(EMPTY_FILTER)}>
|
|
366
|
+
<Text style={[styles.badge, { color: toneColor(theme, "danger"), paddingVertical: 2 }]}>Clear all</Text>
|
|
367
|
+
</Pressable>
|
|
368
|
+
</View>
|
|
369
|
+
) : null}
|
|
370
|
+
<FacetRow
|
|
371
|
+
title="Status"
|
|
372
|
+
values={facets.statuses}
|
|
373
|
+
selected={filter.statuses}
|
|
374
|
+
onToggle={(value) => setFilter({ ...filter, statuses: toggle(filter.statuses, value) })}
|
|
375
|
+
styles={styles}
|
|
376
|
+
theme={theme}
|
|
377
|
+
/>
|
|
378
|
+
<FacetRow
|
|
379
|
+
title="Type"
|
|
380
|
+
values={facets.types}
|
|
381
|
+
selected={filter.types}
|
|
382
|
+
onToggle={(value) => setFilter({ ...filter, types: toggle(filter.types, value) })}
|
|
383
|
+
styles={styles}
|
|
384
|
+
theme={theme}
|
|
385
|
+
/>
|
|
386
|
+
<FacetRow
|
|
387
|
+
title="Priority"
|
|
388
|
+
values={facets.priorities}
|
|
389
|
+
selected={filter.priorities}
|
|
390
|
+
onToggle={(value) => setFilter({ ...filter, priorities: toggle(filter.priorities, value) })}
|
|
391
|
+
styles={styles}
|
|
392
|
+
theme={theme}
|
|
393
|
+
/>
|
|
394
|
+
{facets.labelGroups.length === 0 ? null : (
|
|
395
|
+
<Pressable accessibilityRole="button" accessibilityState={{ expanded: showLabels }} onPress={() => setShowLabels(!showLabels)}>
|
|
396
|
+
<Text style={styles.body}>
|
|
397
|
+
{`${showLabels ? "▾" : "▸"} Labels · ${facets.labelGroups.map((group) => `${group.category}${group.selected > 0 ? ` (${group.selected})` : ""}`).join(", ")}`}
|
|
398
|
+
</Text>
|
|
399
|
+
</Pressable>
|
|
400
|
+
)}
|
|
401
|
+
{showLabels
|
|
402
|
+
? facets.labelGroups.map((group) => (
|
|
403
|
+
<FacetRow
|
|
404
|
+
key={group.category}
|
|
405
|
+
title={group.category}
|
|
406
|
+
values={group.values}
|
|
407
|
+
selected={filter.labels}
|
|
408
|
+
onToggle={(value) => setFilter({ ...filter, labels: toggle(filter.labels, value) })}
|
|
409
|
+
styles={styles}
|
|
410
|
+
theme={theme}
|
|
411
|
+
expanded={expandedGroups.has(group.category)}
|
|
412
|
+
onExpand={() => setExpandedGroups(toggle(expandedGroups, group.category))}
|
|
413
|
+
/>
|
|
414
|
+
))
|
|
415
|
+
: null}
|
|
416
|
+
</View>
|
|
417
|
+
|
|
418
|
+
{/* Sort */}
|
|
419
|
+
<View style={[styles.chipRow, { alignItems: "center" }]}>
|
|
420
|
+
<Text style={styles.body}>Sort</Text>
|
|
421
|
+
{SORT_OPTIONS.map((option) => {
|
|
422
|
+
const on = option.key === sortKey;
|
|
423
|
+
return (
|
|
424
|
+
<Chip
|
|
425
|
+
key={option.key}
|
|
426
|
+
badge={{ text: on ? `${option.label} ${descending ? "↓" : "↑"}` : option.label, tone: on ? "info" : "muted" }}
|
|
427
|
+
selected={on}
|
|
428
|
+
onPress={() => {
|
|
429
|
+
if (on) setDescending(!descending);
|
|
430
|
+
else {
|
|
431
|
+
setSortKey(option.key);
|
|
432
|
+
setDescending(true);
|
|
433
|
+
}
|
|
434
|
+
}}
|
|
435
|
+
styles={styles}
|
|
436
|
+
theme={theme}
|
|
437
|
+
/>
|
|
438
|
+
);
|
|
439
|
+
})}
|
|
440
|
+
</View>
|
|
441
|
+
|
|
442
|
+
{/* List: status groups, closed beads behind the eye (delta 20260918e §4.4) */}
|
|
443
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
444
|
+
<Text style={[styles.sectionTitle, { flex: 1 }]}>{`${grouped.visible} of ${rows.length} beads`}</Text>
|
|
445
|
+
<Pressable
|
|
446
|
+
accessibilityRole="button"
|
|
447
|
+
accessibilityState={{ selected: showClosed }}
|
|
448
|
+
accessibilityLabel={`${showClosed ? "Hide" : "Show"} closed beads (${grouped.closed})`}
|
|
449
|
+
onPress={() => closedBeadsVisibility.set(!showClosed)}
|
|
450
|
+
style={[styles.secondaryButton, { flexDirection: "row", alignItems: "center", gap: 6 }]}
|
|
451
|
+
>
|
|
452
|
+
<Icon name={showClosed ? "Eye" : "EyeOff"} size={16} color={theme.colors.foreground} />
|
|
453
|
+
<Text style={styles.secondaryButtonText}>{`Closed ${grouped.closed}`}</Text>
|
|
454
|
+
</Pressable>
|
|
455
|
+
</View>
|
|
456
|
+
{beads.data !== undefined && rows.length === 0 ? (
|
|
457
|
+
<Text style={styles.body}>This workspace has no beads yet.</Text>
|
|
458
|
+
) : null}
|
|
459
|
+
{grouped.visible === 0 && grouped.closed > 0 ? (
|
|
460
|
+
<Text style={styles.body}>{`All ${grouped.closed} matching beads are closed. Show them with the eye button.`}</Text>
|
|
461
|
+
) : null}
|
|
462
|
+
{/* One flat run of siblings keyed by bead id: a bead that changes group
|
|
463
|
+
keeps its row and its open detail (delta 20260918f F9). */}
|
|
464
|
+
{beadListItems(grouped).map((item) =>
|
|
465
|
+
item.kind === "group" ? (
|
|
466
|
+
<View key={item.key} style={{ gap: layout.compact ? 6 : 8 }}>
|
|
467
|
+
{/* A line of its own between groups, then the group's name and size. */}
|
|
468
|
+
<View style={{ height: 1, backgroundColor: theme.colors.border, marginTop: 4 }} />
|
|
469
|
+
<Text style={[styles.sectionTitle, { color: toneColor(theme, item.tone) }]}>{`${item.label} · ${item.total}`}</Text>
|
|
470
|
+
</View>
|
|
471
|
+
) : (
|
|
472
|
+
renderRow(item.bead)
|
|
473
|
+
),
|
|
474
|
+
)}
|
|
475
|
+
{grouped.truncated > 0 ? (
|
|
476
|
+
<Text style={styles.body}>{`Showing the first ${LIST_LIMIT}. Narrow the filters to see the rest.`}</Text>
|
|
477
|
+
) : null}
|
|
478
|
+
</ScrollView>
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
function renderRow(bead: BeadRow) {
|
|
482
|
+
const open = openId === bead.id;
|
|
483
|
+
const work = workSummary(bead, now);
|
|
484
|
+
return (
|
|
485
|
+
<BeadRowCard
|
|
486
|
+
key={bead.id}
|
|
487
|
+
bead={bead}
|
|
488
|
+
open={open}
|
|
489
|
+
onToggle={() => setOpenId(open ? null : bead.id)}
|
|
490
|
+
styles={styles}
|
|
491
|
+
theme={theme}
|
|
492
|
+
meta={
|
|
493
|
+
<>
|
|
494
|
+
<View style={{ flexDirection: "row", gap: 8, alignItems: "center", flexWrap: "wrap", marginTop: 2 }}>
|
|
495
|
+
<Text style={[styles.body, { fontSize: 12 }]} selectable numberOfLines={1}>
|
|
496
|
+
{bead.id}
|
|
497
|
+
</Text>
|
|
498
|
+
<View style={{ flex: 1 }} />
|
|
499
|
+
<Chip badge={{ text: `${priorityLabel(bead.priority)} · ${bead.issueType}`, tone: "muted" }} styles={styles} theme={theme} />
|
|
500
|
+
<Chip badge={statusBadge(bead)} styles={styles} theme={theme} />
|
|
501
|
+
</View>
|
|
502
|
+
{work === null ? null : (
|
|
503
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 6, marginTop: 4 }}>
|
|
504
|
+
<RoleMark kind="worker" theme={theme} size={16} />
|
|
505
|
+
<Text style={[styles.body, { flex: 1, color: toneColor(theme, work.tone) }]} numberOfLines={1}>
|
|
506
|
+
{work.headline}
|
|
507
|
+
</Text>
|
|
508
|
+
</View>
|
|
509
|
+
)}
|
|
510
|
+
</>
|
|
511
|
+
}
|
|
512
|
+
detail={<BeadDetailPanel workspaceId={workspaceId} bead={bead} styles={styles} theme={theme} navigation={navigation} />}
|
|
513
|
+
/>
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "Beads" tab of a workspace (delta 20260918e §4.1, REQ-060 a–c): the
|
|
3
|
+
* workspace panel Paseo lists in the "+" menu of the tab bar — the dropdown on
|
|
4
|
+
* desktop and the "New tab" screen on mobile — with the workspace's Beads and
|
|
5
|
+
* Metric screens as two sub-tabs.
|
|
6
|
+
*
|
|
7
|
+
* The two screens are the ones the Beads Manager surface shows, rendered
|
|
8
|
+
* without a back button or a title: the tab already lives in its workspace.
|
|
9
|
+
* Their data stays in the React Query cache under their usual keys, so
|
|
10
|
+
* switching sub-tabs does not wait for a reload.
|
|
11
|
+
*
|
|
12
|
+
* Client rules: React Native primitives only, colours from `theme.colors`, no
|
|
13
|
+
* Node import, no `server/` import.
|
|
14
|
+
*/
|
|
15
|
+
import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
|
|
16
|
+
import { useMemo, useState } from "react";
|
|
17
|
+
import { Pressable, Text, View } from "react-native";
|
|
18
|
+
import { BeadsScreen } from "./beads-screen";
|
|
19
|
+
import { DashboardPanel } from "./dashboard";
|
|
20
|
+
import { dashboardStyles } from "./dashboard-model";
|
|
21
|
+
import { BEADS_TAB_VIEWS, DEFAULT_BEADS_TAB_VIEW, type BeadsTabView } from "./dashboard-view";
|
|
22
|
+
|
|
23
|
+
export function BeadsTabPanel(props: PluginWorkspacePanelProps) {
|
|
24
|
+
const { theme, layout } = props;
|
|
25
|
+
const styles = useMemo(() => dashboardStyles(theme, layout.compact), [theme, layout.compact]);
|
|
26
|
+
// Lives as long as this tab: a reload opens on Beads again (PRD delta §6).
|
|
27
|
+
const [view, setView] = useState<BeadsTabView>(DEFAULT_BEADS_TAB_VIEW);
|
|
28
|
+
// The same outer padding as the screens below, so the row lines up with them.
|
|
29
|
+
const padding = styles.content.padding;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<View style={{ flex: 1, backgroundColor: theme.colors.surface0 }}>
|
|
33
|
+
<View accessibilityRole="tablist" style={{ flexDirection: "row", gap: 8, paddingHorizontal: padding, paddingTop: padding }}>
|
|
34
|
+
{BEADS_TAB_VIEWS.map((tab) => {
|
|
35
|
+
const selected = tab.key === view;
|
|
36
|
+
return (
|
|
37
|
+
<Pressable
|
|
38
|
+
key={tab.key}
|
|
39
|
+
accessibilityRole="tab"
|
|
40
|
+
accessibilityState={{ selected }}
|
|
41
|
+
onPress={() => setView(tab.key)}
|
|
42
|
+
style={selected ? styles.button : styles.secondaryButton}
|
|
43
|
+
>
|
|
44
|
+
<Text style={selected ? styles.buttonText : styles.secondaryButtonText}>{tab.label}</Text>
|
|
45
|
+
</Pressable>
|
|
46
|
+
);
|
|
47
|
+
})}
|
|
48
|
+
</View>
|
|
49
|
+
<View style={{ flex: 1 }}>
|
|
50
|
+
{view === "beads" ? (
|
|
51
|
+
<BeadsScreen {...props} />
|
|
52
|
+
) : (
|
|
53
|
+
<DashboardPanel {...props} />
|
|
54
|
+
)}
|
|
55
|
+
</View>
|
|
56
|
+
</View>
|
|
57
|
+
);
|
|
58
|
+
}
|