@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.
- package/client/agent-preview/PreviewActions.tsx +56 -0
- package/client/agent-preview/useAutoCommit.ts +91 -0
- package/client/components/AgentCommandMenu.tsx +51 -0
- package/client/components/AgentConversationPreview.tsx +620 -0
- package/client/components/AgentConversationTimeline.tsx +177 -0
- package/client/components/AgentCreatorPanel.tsx +152 -0
- package/client/components/AgentQuestionCard.tsx +244 -0
- package/client/components/AgentRow.tsx +108 -0
- package/client/components/BadgeWithTooltip.tsx +119 -0
- package/client/components/BranchRow.tsx +121 -0
- package/client/components/ControlGlyph.tsx +18 -0
- package/client/components/HeatmapCalendar.tsx +165 -0
- package/client/components/ModelBrowserMenu.tsx +135 -0
- package/client/components/PaseoAssistantMessage.tsx +216 -0
- package/client/components/PaseoShellDetail.tsx +128 -0
- package/client/components/PaseoStreamBadge.tsx +94 -0
- package/client/components/PaseoTimelineItem.tsx +147 -0
- package/client/components/PaseoToolCallGroup.tsx +52 -0
- package/client/components/PaseoToolDetail.tsx +55 -0
- package/client/components/ProjectCard.tsx +196 -0
- package/client/components/ProjectIcon.tsx +49 -0
- package/client/components/ProviderBrandIcon.tsx +52 -0
- package/client/components/ResizeHandle.tsx +91 -0
- package/client/components/SharedComposerInput.tsx +383 -0
- package/client/components/Tooltip.tsx +49 -0
- package/client/components/WorkspaceAgentRow.tsx +93 -0
- package/client/components/WorkspaceBadge.tsx +39 -0
- package/client/components/WorkspaceCreateComposer.tsx +170 -0
- package/client/components/WorkspaceCreatorPanel.tsx +256 -0
- package/client/components/WorkspaceLaunchMenu.tsx +76 -0
- package/client/components/WorkspaceMenu.tsx +116 -0
- package/client/components/WorkspaceSelectControl.tsx +52 -0
- package/client/components/agent-conversation-types.ts +7 -0
- package/client/components/workspace-creator-styles.ts +322 -0
- package/client/hooks/useAgentActivityHeatmap.ts +178 -0
- package/client/hooks/useFilter.ts +107 -0
- package/client/hooks/useKeyboardInset.ts +37 -0
- package/client/hooks/useResizableWidth.ts +70 -0
- package/client/hooks/useWorkspaces.ts +128 -0
- package/client/overview/useProjectOptions.ts +73 -0
- package/client/web.ts +160 -0
- package/client/workspace-agents.tsx +71 -0
- package/client/workspace-creator/badge-styles.ts +13 -0
- package/client/workspace-creator/constants.ts +14 -0
- package/client/workspace-creator/control-styles.ts +12 -0
- package/client/workspace-creator/provider-selection.ts +51 -0
- package/client/workspace-creator/submit-workspace.ts +67 -0
- package/client/workspace-creator/types.ts +40 -0
- package/client/workspace-creator/ui-utils.ts +15 -0
- package/client/workspace-creator/useProviderCatalog.ts +89 -0
- package/client/workspace-overview.tsx +533 -0
- package/index.client.tsx +36 -0
- package/index.server.ts +33 -0
- package/package.json +28 -4
- package/paseo-plugin.json +4 -0
- package/server/agent-config-impl.ts +57 -0
- package/server/auto-commit-impl.ts +221 -0
- package/server/git-branches-impl.ts +81 -0
- package/server/project-icon-impl.ts +102 -0
- package/server/terminal-launch-impl.ts +56 -0
- package/shared/agent-activity.ts +16 -0
- package/shared/agent-config.ts +30 -0
- package/shared/auto-commit.ts +38 -0
- package/shared/git-branches.ts +18 -0
- package/shared/overview-types.ts +87 -0
- package/shared/project-icon.ts +13 -0
- package/shared/terminal-launch.ts +20 -0
- 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
|
+
}
|