@yuangc/paseo-plugin-workspace-overview 0.0.0-stage → 1.0.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 (68) hide show
  1. package/client/agent-preview/PreviewActions.tsx +56 -0
  2. package/client/agent-preview/useAutoCommit.ts +91 -0
  3. package/client/components/AgentCommandMenu.tsx +51 -0
  4. package/client/components/AgentConversationPreview.tsx +620 -0
  5. package/client/components/AgentConversationTimeline.tsx +177 -0
  6. package/client/components/AgentCreatorPanel.tsx +152 -0
  7. package/client/components/AgentQuestionCard.tsx +244 -0
  8. package/client/components/AgentRow.tsx +108 -0
  9. package/client/components/BadgeWithTooltip.tsx +119 -0
  10. package/client/components/BranchRow.tsx +121 -0
  11. package/client/components/ControlGlyph.tsx +18 -0
  12. package/client/components/HeatmapCalendar.tsx +165 -0
  13. package/client/components/ModelBrowserMenu.tsx +135 -0
  14. package/client/components/PaseoAssistantMessage.tsx +216 -0
  15. package/client/components/PaseoShellDetail.tsx +128 -0
  16. package/client/components/PaseoStreamBadge.tsx +94 -0
  17. package/client/components/PaseoTimelineItem.tsx +147 -0
  18. package/client/components/PaseoToolCallGroup.tsx +52 -0
  19. package/client/components/PaseoToolDetail.tsx +55 -0
  20. package/client/components/ProjectCard.tsx +196 -0
  21. package/client/components/ProjectIcon.tsx +49 -0
  22. package/client/components/ProviderBrandIcon.tsx +52 -0
  23. package/client/components/ResizeHandle.tsx +91 -0
  24. package/client/components/SharedComposerInput.tsx +383 -0
  25. package/client/components/Tooltip.tsx +49 -0
  26. package/client/components/WorkspaceAgentRow.tsx +93 -0
  27. package/client/components/WorkspaceBadge.tsx +39 -0
  28. package/client/components/WorkspaceCreateComposer.tsx +170 -0
  29. package/client/components/WorkspaceCreatorPanel.tsx +256 -0
  30. package/client/components/WorkspaceLaunchMenu.tsx +76 -0
  31. package/client/components/WorkspaceMenu.tsx +116 -0
  32. package/client/components/WorkspaceSelectControl.tsx +52 -0
  33. package/client/components/agent-conversation-types.ts +7 -0
  34. package/client/components/workspace-creator-styles.ts +322 -0
  35. package/client/hooks/useAgentActivityHeatmap.ts +178 -0
  36. package/client/hooks/useFilter.ts +107 -0
  37. package/client/hooks/useKeyboardInset.ts +37 -0
  38. package/client/hooks/useResizableWidth.ts +70 -0
  39. package/client/hooks/useWorkspaces.ts +128 -0
  40. package/client/overview/useProjectOptions.ts +73 -0
  41. package/client/web.ts +160 -0
  42. package/client/workspace-agents.tsx +71 -0
  43. package/client/workspace-creator/badge-styles.ts +13 -0
  44. package/client/workspace-creator/constants.ts +14 -0
  45. package/client/workspace-creator/control-styles.ts +12 -0
  46. package/client/workspace-creator/provider-selection.ts +51 -0
  47. package/client/workspace-creator/submit-workspace.ts +67 -0
  48. package/client/workspace-creator/types.ts +40 -0
  49. package/client/workspace-creator/ui-utils.ts +15 -0
  50. package/client/workspace-creator/useProviderCatalog.ts +89 -0
  51. package/client/workspace-overview.tsx +533 -0
  52. package/index.client.tsx +36 -0
  53. package/index.server.ts +33 -0
  54. package/package.json +28 -4
  55. package/paseo-plugin.json +4 -0
  56. package/server/agent-config-impl.ts +57 -0
  57. package/server/auto-commit-impl.ts +221 -0
  58. package/server/git-branches-impl.ts +81 -0
  59. package/server/project-icon-impl.ts +102 -0
  60. package/server/terminal-launch-impl.ts +56 -0
  61. package/shared/agent-activity.ts +16 -0
  62. package/shared/agent-config.ts +30 -0
  63. package/shared/auto-commit.ts +38 -0
  64. package/shared/git-branches.ts +18 -0
  65. package/shared/overview-types.ts +87 -0
  66. package/shared/project-icon.ts +13 -0
  67. package/shared/terminal-launch.ts +20 -0
  68. package/README.md +0 -3
@@ -0,0 +1,383 @@
1
+ import React, { useEffect, useMemo, useState } from "react";
2
+ import { Image, Platform, Pressable, Text, View, useWindowDimensions, type NativeSyntheticEvent, type TextInputKeyPressEventData } from "react-native";
3
+ import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
4
+ import { Icon, Modal, TextInput } from "@getpaseo/plugin/client/react-native";
5
+ import { ModelBrowserMenu } from "./ModelBrowserMenu";
6
+ import { Menu } from "./WorkspaceMenu";
7
+ import { SelectControl } from "./WorkspaceSelectControl";
8
+ import {
9
+ providerById,
10
+ readyProviders,
11
+ selectableModels,
12
+ defaultModel,
13
+ } from "../workspace-creator/provider-selection";
14
+ import { formatControlValue } from "../workspace-creator/ui-utils";
15
+ import type {
16
+ ComposerSelection,
17
+ OpenMenu,
18
+ ProviderSnapshot,
19
+ } from "../workspace-creator/types";
20
+ import { createComposerStyles } from "./workspace-creator-styles";
21
+ import { subscribeToImagePaste, subscribeToImeEnter, type PastedImage } from "../web";
22
+
23
+ function getColors(theme: PluginSurfaceProps["theme"]) {
24
+ if (!theme || !theme.colors) {
25
+ return {
26
+ surface0: "#1e1e1e",
27
+ surface1: "#252526",
28
+ foreground: "#ffffff",
29
+ foregroundMuted: "#888888",
30
+ accent: "#007acc",
31
+ accentForeground: "#ffffff",
32
+ statusWarning: "#f59e0b",
33
+ statusDanger: "#ef4444",
34
+ };
35
+ }
36
+ return theme.colors;
37
+ }
38
+
39
+ function PastedImagePill({
40
+ image,
41
+ disabled,
42
+ onRemove,
43
+ theme,
44
+ }: {
45
+ image: PastedImage;
46
+ disabled: boolean;
47
+ onRemove: () => void;
48
+ theme: PluginSurfaceProps["theme"];
49
+ }) {
50
+ const [hovered, setHovered] = useState(false);
51
+ const [previewOpen, setPreviewOpen] = useState(false);
52
+ const { height } = useWindowDimensions();
53
+ const showRemove = Platform.OS !== "web" || hovered;
54
+ return (
55
+ <View style={{ position: "relative" }}>
56
+ <Modal title={image.fileName} open={previewOpen} onOpenChange={setPreviewOpen}>
57
+ <Modal.Content scrollable={false}>
58
+ <Image
59
+ accessibilityLabel={`图片预览 ${image.fileName}`}
60
+ source={{ uri: image.dataUrl }}
61
+ resizeMode="contain"
62
+ style={{ width: "100%", height: Math.max(120, Math.min(640, height * 0.65)) }}
63
+ />
64
+ </Modal.Content>
65
+ </Modal>
66
+ <Pressable
67
+ accessibilityRole="button"
68
+ accessibilityLabel={`打开图片 ${image.fileName}`}
69
+ onPress={() => setPreviewOpen(true)}
70
+ disabled={disabled}
71
+ onHoverIn={() => setHovered(true)}
72
+ onHoverOut={() => setHovered(false)}
73
+ style={{
74
+ width: 50,
75
+ height: 50,
76
+ borderRadius: 8,
77
+ borderWidth: 1,
78
+ borderColor: theme.colors.border,
79
+ overflow: "hidden",
80
+ }}
81
+ >
82
+ <Image source={{ uri: image.dataUrl }} style={{ width: 48, height: 48 }} resizeMode="cover" />
83
+ </Pressable>
84
+ <Pressable
85
+ accessibilityRole="button"
86
+ accessibilityLabel={`移除图片 ${image.fileName}`}
87
+ disabled={disabled}
88
+ hitSlop={8}
89
+ onHoverIn={() => setHovered(true)}
90
+ onHoverOut={() => setHovered(false)}
91
+ onPress={onRemove}
92
+ style={{
93
+ position: "absolute",
94
+ top: -8,
95
+ left: -8,
96
+ width: 24,
97
+ height: 24,
98
+ borderRadius: 12,
99
+ borderWidth: 1,
100
+ borderColor: theme.colors.border,
101
+ backgroundColor: theme.colors.surface2,
102
+ alignItems: "center",
103
+ justifyContent: "center",
104
+ opacity: showRemove ? 1 : 0,
105
+ pointerEvents: showRemove ? "auto" : "none",
106
+ zIndex: 1,
107
+ }}
108
+ >
109
+ <Icon name="X" size={12} color={theme.colors.foregroundMuted} />
110
+ </Pressable>
111
+ </View>
112
+ );
113
+ }
114
+
115
+ export interface SharedComposerInputProps {
116
+ /** Current selection state (provider/model/mode/thinking) */
117
+ selection: ComposerSelection | null;
118
+ /** Provider snapshot with all available providers */
119
+ snapshot: ProviderSnapshot | null;
120
+ /** Whether providers are still loading */
121
+ providerLoading: boolean;
122
+ /** Currently open dropdown menu */
123
+ openMenu: OpenMenu;
124
+ /** Callback to toggle/close menus */
125
+ onToggleMenu: (menu: OpenMenu) => void;
126
+ /** Callback when selection changes */
127
+ onSelectModel: (combinedId: string) => void;
128
+ onSelectMode: (modeId: string) => void;
129
+ onSelectThinking: (thinkingId: string) => void;
130
+ /** Whether agent is currently running (shows stop button instead of send) */
131
+ isAgentRunning: boolean;
132
+ /** Whether a cancel operation is in progress */
133
+ isCancelling: boolean;
134
+ /** Callback to cancel the running agent */
135
+ onCancel: () => void;
136
+ /** Placeholder text for the input */
137
+ placeholder: string;
138
+ /** Theme from plugin surface */
139
+ theme: PluginSurfaceProps["theme"];
140
+ /** Optional: disable the send button (e.g., when pending) */
141
+ disabled?: boolean;
142
+ showAgentControls?: boolean;
143
+ autoFocus?: boolean;
144
+ inputNativeID?: string;
145
+ pendingLabel?: string | null;
146
+ value: string;
147
+ onChangeText: (value: string) => void;
148
+ onSubmit: () => void | Promise<void>;
149
+ images?: PastedImage[];
150
+ onImagesChange?: (images: PastedImage[]) => void;
151
+ onPasteError?: (message: string) => void;
152
+ usage?: { contextWindowMaxTokens?: number; contextWindowUsedTokens?: number; totalCostUsd?: number } | null;
153
+ onKeyPress?: (event: NativeSyntheticEvent<TextInputKeyPressEventData>) => void;
154
+ }
155
+
156
+ export function SharedComposerInput({
157
+ selection,
158
+ snapshot,
159
+ providerLoading,
160
+ openMenu,
161
+ onToggleMenu,
162
+ onSelectModel,
163
+ onSelectMode,
164
+ onSelectThinking,
165
+ isAgentRunning,
166
+ isCancelling,
167
+ onCancel,
168
+ placeholder,
169
+ theme,
170
+ disabled = false,
171
+ showAgentControls = true,
172
+ autoFocus = false,
173
+ inputNativeID = "workspace-create-prompt",
174
+ pendingLabel = null,
175
+ value,
176
+ onChangeText,
177
+ onSubmit,
178
+ images = [],
179
+ onImagesChange,
180
+ onPasteError,
181
+ usage = null,
182
+ onKeyPress,
183
+ }: SharedComposerInputProps) {
184
+ const [usageOpen, setUsageOpen] = useState(false);
185
+ const colors = getColors(theme);
186
+ const styles = useMemo(() => createComposerStyles(theme), [theme]);
187
+ const hasInput = value.trim().length > 0 || images.length > 0;
188
+ const usagePercentage = usage?.contextWindowMaxTokens && usage.contextWindowUsedTokens != null
189
+ ? Math.max(0, Math.min(100, Math.round((usage.contextWindowUsedTokens / usage.contextWindowMaxTokens) * 100)))
190
+ : null;
191
+ const usageColor = usagePercentage != null && usagePercentage > 90
192
+ ? colors.statusDanger
193
+ : usagePercentage != null && usagePercentage >= 70
194
+ ? colors.statusWarning
195
+ : colors.foregroundMuted;
196
+
197
+ // Derived state for selectors (mirrors WorkspaceCreateComposer)
198
+ const currentProvider = providerById(snapshot, selection?.providerId ?? "");
199
+ const models = selectableModels(currentProvider);
200
+ const modes = currentProvider?.modes ?? [];
201
+ const selectedMode = modes.find((item) => item.id === selection?.modeId) ?? null;
202
+ const model = models.find((item) => item.id === selection?.modelId) ?? defaultModel(currentProvider);
203
+ const thinkingOptions = model?.thinkingOptions ?? [];
204
+
205
+ const providers = readyProviders(snapshot ?? { entries: [] });
206
+ const modelOptions = providers.flatMap((entry) =>
207
+ selectableModels(entry).map((item) => ({
208
+ id: `${entry.provider}::${item.id}`,
209
+ label: item.label,
210
+ detail: entry.label ?? entry.provider,
211
+ })),
212
+ );
213
+ const modeOptions = (modes.length > 0
214
+ ? modes
215
+ : selection?.modeId ? [{ id: selection.modeId, label: selection.modeLabel ?? selection.modeId }] : []
216
+ ).map((item) => ({
217
+ id: item.id,
218
+ label: item.label ?? item.id,
219
+ detail: item.description,
220
+ iconName: item.icon,
221
+ }));
222
+ const thinkingOptionsForMenu = (thinkingOptions.length > 0
223
+ ? thinkingOptions
224
+ : selection?.thinkingOptionId ? [{ id: selection.thinkingOptionId, label: selection.thinkingLabel ?? selection.thinkingOptionId }] : []
225
+ ).map((item) => ({
226
+ id: item.id,
227
+ label: item.label ?? item.id,
228
+ iconName: "Brain",
229
+ }));
230
+
231
+ const composerMenuOpen =
232
+ openMenu === "model" || openMenu === "mode" || openMenu === "thinking";
233
+
234
+ useEffect(() => subscribeToImagePaste(
235
+ inputNativeID,
236
+ (pasted) => onImagesChange?.([...images, ...pasted]),
237
+ (message) => onPasteError?.(message),
238
+ ), [images, inputNativeID, onImagesChange, onPasteError]);
239
+
240
+ useEffect(() => subscribeToImeEnter(inputNativeID), [inputNativeID]);
241
+
242
+ return (
243
+ <View style={[styles.inputWrapper, composerMenuOpen && styles.menuRegionActive]}>
244
+ {images.length > 0 ? (
245
+ <View accessibilityLabel="图片附件" style={{ flexDirection: "row", flexWrap: "wrap", gap: 8 }}>
246
+ {images.map((image, index) => (
247
+ <PastedImagePill
248
+ key={`${image.fileName}-${index}`}
249
+ image={image}
250
+ disabled={disabled}
251
+ onRemove={() => onImagesChange?.(images.filter((_, imageIndex) => imageIndex !== index))}
252
+ theme={theme}
253
+ />
254
+ ))}
255
+ </View>
256
+ ) : null}
257
+
258
+ <TextInput
259
+ nativeID={inputNativeID}
260
+ style={styles.textInput}
261
+ placeholder={placeholder}
262
+ placeholderTextColor={colors.foregroundMuted + "66"}
263
+ multiline
264
+ autoFocus={autoFocus}
265
+ editable={!disabled}
266
+ value={value}
267
+ onChangeText={onChangeText}
268
+ onKeyPress={onKeyPress}
269
+ />
270
+
271
+ <View style={styles.buttonRow}>
272
+ {showAgentControls ? (
273
+ <View style={styles.leftControls}>
274
+ <View nativeID="workspace-create-dropdown-model" style={[styles.controlAnchor, openMenu === "model" && styles.controlAnchorOpen]}>
275
+ <SelectControl
276
+ kind="model"
277
+ providerId={selection?.providerId}
278
+ value={formatControlValue(selection?.modelLabel ?? null, "Model")}
279
+ disabled={disabled || providerLoading}
280
+ open={openMenu === "model"}
281
+ onPress={() => onToggleMenu(openMenu === "model" ? null : "model")}
282
+ theme={theme}
283
+ />
284
+ {openMenu === "model" ? (
285
+ <ModelBrowserMenu
286
+ providers={providers}
287
+ selection={selection}
288
+ onSelect={onSelectModel}
289
+ theme={theme}
290
+ />
291
+ ) : null}
292
+ </View>
293
+
294
+ {thinkingOptionsForMenu.length > 0 ? (
295
+ <View nativeID="workspace-create-dropdown-thinking" style={[styles.controlAnchor, openMenu === "thinking" && styles.controlAnchorOpen]}>
296
+ <SelectControl
297
+ kind="thinking"
298
+ value={formatControlValue(selection?.thinkingLabel ?? null, "Thinking")}
299
+ disabled={disabled || providerLoading}
300
+ open={openMenu === "thinking"}
301
+ onPress={() => onToggleMenu(openMenu === "thinking" ? null : "thinking")}
302
+ theme={theme}
303
+ />
304
+ {openMenu === "thinking" ? (
305
+ <Menu
306
+ kind="thinking"
307
+ options={thinkingOptionsForMenu}
308
+ selectedId={selection?.thinkingOptionId ?? null}
309
+ onSelect={onSelectThinking}
310
+ theme={theme}
311
+ />
312
+ ) : null}
313
+ </View>
314
+ ) : null}
315
+
316
+ {modeOptions.length > 0 ? (
317
+ <View nativeID="workspace-create-dropdown-mode" style={[styles.controlAnchor, openMenu === "mode" && styles.controlAnchorOpen]}>
318
+ <SelectControl
319
+ kind="mode"
320
+ iconName={selectedMode?.icon ?? "Bot"}
321
+ value={formatControlValue(selection?.modeLabel ?? null, "Mode")}
322
+ disabled={disabled || providerLoading}
323
+ open={openMenu === "mode"}
324
+ onPress={() => onToggleMenu(openMenu === "mode" ? null : "mode")}
325
+ theme={theme}
326
+ />
327
+ {openMenu === "mode" ? (
328
+ <Menu
329
+ kind="mode"
330
+ options={modeOptions}
331
+ selectedId={selection?.modeId ?? null}
332
+ onSelect={onSelectMode}
333
+ theme={theme}
334
+ />
335
+ ) : null}
336
+ </View>
337
+ ) : null}
338
+ </View>
339
+ ) : (
340
+ <View style={styles.leftControls} />
341
+ )}
342
+
343
+ <View style={styles.rightControls}>
344
+ {usagePercentage != null ? <View style={{ position: "relative" }}><Pressable
345
+ accessibilityRole="button"
346
+ accessibilityLabel={`上下文已使用 ${usagePercentage}%${usage?.totalCostUsd ? `,费用 $${usage.totalCostUsd.toFixed(usage.totalCostUsd < 0.01 ? 4 : 2)}` : ""}`}
347
+ onPress={() => setUsageOpen((open) => !open)}
348
+ style={{ flexDirection: "row", alignItems: "center", gap: 5 }}
349
+ >
350
+ <View style={{ width: 12, height: 12, borderRadius: 6, borderWidth: 2, borderColor: usageColor }} />
351
+ <Text style={{ color: colors.foregroundMuted, fontSize: 11 }}>{usagePercentage}%</Text>
352
+ </Pressable>{usageOpen ? <View accessibilityLabel="上下文用量详情" style={{ position: "absolute", right: 0, bottom: 24, minWidth: 210, padding: 10, gap: 5, borderRadius: 8, borderWidth: 1, borderColor: colors.foregroundMuted + "44", backgroundColor: colors.surface1, zIndex: 50 }}>
353
+ <Text style={{ color: colors.foreground, fontSize: 12, fontWeight: "600" }}>Context usage</Text>
354
+ <Text style={{ color: colors.foregroundMuted, fontSize: 11 }}>Used: {(usage?.contextWindowUsedTokens ?? 0).toLocaleString()} tokens</Text>
355
+ <Text style={{ color: colors.foregroundMuted, fontSize: 11 }}>Window: {(usage?.contextWindowMaxTokens ?? 0).toLocaleString()} tokens</Text>
356
+ {usage?.totalCostUsd != null ? <Text style={{ color: colors.foregroundMuted, fontSize: 11 }}>Cost: ${usage.totalCostUsd.toFixed(usage.totalCostUsd < 0.01 ? 4 : 2)}</Text> : null}
357
+ </View> : null}</View> : null}
358
+ {pendingLabel ? <Text style={styles.pendingText}>{pendingLabel}</Text> : null}
359
+ {isAgentRunning && !hasInput ? (
360
+ <Pressable
361
+ accessibilityLabel={isCancelling ? "正在停止…" : "停止 Agent"}
362
+ onPress={onCancel}
363
+ disabled={isCancelling}
364
+ style={[styles.sendButton, disabled && styles.disabled]}
365
+ >
366
+ <Icon name={isCancelling ? "Loader" : "Square"} color="#fff" size={14} />
367
+ </Pressable>
368
+ ) : hasInput ? (
369
+ <Pressable
370
+ accessibilityRole="button"
371
+ accessibilityLabel="发送"
372
+ disabled={disabled || !hasInput}
373
+ onPress={() => void onSubmit()}
374
+ style={[styles.sendButton, (disabled || !hasInput) && styles.disabled]}
375
+ >
376
+ <Icon name="CornerDownLeft" color={colors.accentForeground} size={16} />
377
+ </Pressable>
378
+ ) : null}
379
+ </View>
380
+ </View>
381
+ </View>
382
+ );
383
+ }
@@ -0,0 +1,49 @@
1
+ import React, { createContext, useContext } from "react";
2
+ import { type PluginSurfaceProps } from "@getpaseo/plugin/client";
3
+ import { type TooltipContextValue } from "../../shared/overview-types";
4
+
5
+ /**
6
+ * Semantic status colors, chosen to read clearly on both light and dark themes.
7
+ * Independent of theme.colors because the theme palette is limited.
8
+ */
9
+ const STATUS_COLORS = {
10
+ active: "#60a5fa", // light blue: running
11
+ pending: "#86efac", // light green: needs_input, attention
12
+ starting: "#3b82f6", // blue-500: initializing
13
+ danger: "#ef4444", // red-500: error, failed
14
+ warning: "#f97316", // orange-500: attention / requiresAttention (legacy)
15
+ idle: "#9ca3af", // gray-400: everything else
16
+ } as const;
17
+
18
+ export function statusColor(status: string, theme: PluginSurfaceProps["theme"], requiresAttention?: boolean): string {
19
+ if (requiresAttention || status === "attention" || status === "needs_input") return STATUS_COLORS.pending;
20
+ if (status === "running") return STATUS_COLORS.active;
21
+ if (status === "initializing") return STATUS_COLORS.starting;
22
+ if (status === "error" || status === "failed") return STATUS_COLORS.danger;
23
+ return theme.colors.foregroundMuted;
24
+ }
25
+
26
+ export function statusRowBackground(status: string, requiresAttention: boolean | undefined): string {
27
+ if (requiresAttention || status === "attention" || status === "needs_input") return STATUS_COLORS.pending + "18";
28
+ if (status === "error" || status === "failed") return STATUS_COLORS.danger + "22";
29
+ if (status === "running") return STATUS_COLORS.active + "12";
30
+ if (status === "initializing") return STATUS_COLORS.starting + "12";
31
+ return "";
32
+ }
33
+
34
+ const TooltipContext = createContext<TooltipContextValue | null>(null);
35
+
36
+ export function useTooltip(): TooltipContextValue | null {
37
+ return useContext(TooltipContext);
38
+ }
39
+
40
+ export function TooltipProvider({ children, value }: { children: React.ReactNode; value: TooltipContextValue }) {
41
+ return (
42
+ <TooltipContext.Provider value={value}>
43
+ {children}
44
+ </TooltipContext.Provider>
45
+ );
46
+ }
47
+
48
+
49
+ export { BadgeWithTooltip } from "./BadgeWithTooltip";
@@ -0,0 +1,93 @@
1
+ import React, { useMemo } from "react";
2
+ import { Text, TouchableOpacity, View, type TextStyle, type ViewStyle } from "react-native";
3
+ import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
4
+
5
+ export function WorkspaceAgentRow({
6
+ agent,
7
+ theme,
8
+ compact,
9
+ navigation,
10
+ }: {
11
+ agent: {
12
+ id: string;
13
+ title: string | null;
14
+ provider: string;
15
+ model: string | null;
16
+ status: string;
17
+ requiresAttention?: boolean;
18
+ archivedAt?: string | null;
19
+ };
20
+ theme: PluginWorkspacePanelProps["theme"];
21
+ compact: boolean;
22
+ navigation: NonNullable<PluginWorkspacePanelProps["navigation"]>;
23
+ }) {
24
+ const isArchived = !!agent.archivedAt;
25
+
26
+ const styles = useMemo(
27
+ () =>
28
+ ({
29
+ row: {
30
+ flexDirection: "row" as const,
31
+ alignItems: "flex-start" as const,
32
+ padding: compact ? 8 : 10,
33
+ borderRadius: 6,
34
+ marginBottom: 4,
35
+ backgroundColor: agent.requiresAttention ? theme.colors.statusDanger + "20" : theme.colors.surface0,
36
+ opacity: isArchived ? 0.5 : 1,
37
+ } as ViewStyle,
38
+ dot: {
39
+ width: 8,
40
+ height: 8,
41
+ borderRadius: 4,
42
+ backgroundColor:
43
+ agent.status === "running"
44
+ ? theme.colors.accent
45
+ : agent.status === "idle"
46
+ ? theme.colors.foregroundMuted
47
+ : theme.colors.statusDanger,
48
+ marginTop: 5,
49
+ marginRight: 8,
50
+ flexShrink: 0,
51
+ opacity: isArchived ? 0.5 : 1,
52
+ } as ViewStyle,
53
+ info: { flex: 1 } as ViewStyle,
54
+ title: {
55
+ color: theme.colors.foreground,
56
+ fontSize: compact ? 13 : 14,
57
+ fontWeight: "500" as const,
58
+ textDecorationLine: isArchived ? "line-through" as const : "none" as const,
59
+ } as TextStyle,
60
+ meta: {
61
+ color: theme.colors.foregroundMuted,
62
+ fontSize: 11,
63
+ marginTop: 2,
64
+ textDecorationLine: isArchived ? "line-through" as const : "none" as const,
65
+ } as TextStyle,
66
+ provider: {
67
+ color: theme.colors.foregroundMuted,
68
+ fontSize: 10,
69
+ marginTop: 1,
70
+ textDecorationLine: isArchived ? "line-through" as const : "none" as const,
71
+ } as TextStyle,
72
+ }),
73
+ [theme, compact, agent.requiresAttention, agent.status, isArchived],
74
+ );
75
+
76
+ return (
77
+ <TouchableOpacity onPress={() => navigation.openAgent({ agentId: agent.id })} activeOpacity={0.7}>
78
+ <View style={styles.row}>
79
+ <View style={styles.dot} />
80
+ <View style={styles.info}>
81
+ <Text style={styles.title} numberOfLines={1}>
82
+ {agent.title ?? agent.id.slice(0, 8)}
83
+ </Text>
84
+ <Text style={styles.meta}>{agent.status}{isArchived ? " (archived)" : ""}</Text>
85
+ <Text style={styles.provider}>
86
+ {agent.provider}
87
+ {agent.model ? ` · ${agent.model}` : ""}
88
+ </Text>
89
+ </View>
90
+ </View>
91
+ </TouchableOpacity>
92
+ );
93
+ }
@@ -0,0 +1,39 @@
1
+ import React, { useMemo } from "react";
2
+ import { Pressable, Text, View } from "react-native";
3
+ import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
4
+ import { Icon } from "@getpaseo/plugin/client/react-native";
5
+ import { createBadgeStyles } from "../workspace-creator/badge-styles";
6
+
7
+ export function Badge({
8
+ icon,
9
+ label,
10
+ selectable = false,
11
+ disabled = false,
12
+ onPress,
13
+ theme,
14
+ }: {
15
+ icon: React.ReactNode;
16
+ label: string;
17
+ selectable?: boolean;
18
+ disabled?: boolean;
19
+ onPress?: () => void;
20
+ theme: PluginSurfaceProps["theme"];
21
+ }) {
22
+ const styles = useMemo(() => createBadgeStyles(theme), [theme]);
23
+ return (
24
+ <Pressable
25
+ accessibilityRole={selectable ? "button" : undefined}
26
+ accessibilityLabel={selectable ? `切换 ${label}` : undefined}
27
+ accessibilityState={{ disabled: !selectable || disabled }}
28
+ disabled={!selectable || disabled}
29
+ onPress={onPress}
30
+ style={[styles.badge, selectable && styles.badgeSelectable, disabled && styles.badgeDisabled]}
31
+ >
32
+ <View style={styles.badgeIcon}>{icon}</View>
33
+ <Text style={styles.badgeText} numberOfLines={1}>
34
+ {label}
35
+ </Text>
36
+ {selectable ? <Icon name="ChevronDown" color={theme.colors.foregroundMuted} size={12} /> : null}
37
+ </Pressable>
38
+ );
39
+ }