@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,216 @@
|
|
|
1
|
+
import React, { useMemo, type ReactNode } from "react";
|
|
2
|
+
import { Text, View, type TextStyle, type ViewStyle } from "react-native";
|
|
3
|
+
import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
|
|
4
|
+
|
|
5
|
+
function inlineMarkdown(text: string, styles: ReturnType<typeof createStyles>): ReactNode[] {
|
|
6
|
+
return text
|
|
7
|
+
.split(/(`[^`]+`|\*\*[^*]+\*\*|__[^_]+__|\*[^*]+\*|_[^_]+_|~~[^~]+~~)/g)
|
|
8
|
+
.filter(Boolean)
|
|
9
|
+
.map((token, index) => {
|
|
10
|
+
if (token.startsWith("`") && token.endsWith("`")) {
|
|
11
|
+
return <Text key={index} style={styles.codeInline}>{token.slice(1, -1)}</Text>;
|
|
12
|
+
}
|
|
13
|
+
if (
|
|
14
|
+
(token.startsWith("**") && token.endsWith("**")) ||
|
|
15
|
+
(token.startsWith("__") && token.endsWith("__"))
|
|
16
|
+
) {
|
|
17
|
+
return <Text key={index} style={styles.strong}>{token.slice(2, -2)}</Text>;
|
|
18
|
+
}
|
|
19
|
+
if (token.startsWith("~~") && token.endsWith("~~")) {
|
|
20
|
+
return <Text key={index} style={styles.strike}>{token.slice(2, -2)}</Text>;
|
|
21
|
+
}
|
|
22
|
+
if (
|
|
23
|
+
(token.startsWith("*") && token.endsWith("*")) ||
|
|
24
|
+
(token.startsWith("_") && token.endsWith("_"))
|
|
25
|
+
) {
|
|
26
|
+
return <Text key={index} style={styles.em}>{token.slice(1, -1)}</Text>;
|
|
27
|
+
}
|
|
28
|
+
return <Text key={index} style={styles.text}>{token}</Text>;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function markdownBlocks(text: string, styles: ReturnType<typeof createStyles>): ReactNode[] {
|
|
33
|
+
const nodes: ReactNode[] = [];
|
|
34
|
+
let codeBlock: { lines: string[] } | null = null;
|
|
35
|
+
let codeKey = 0;
|
|
36
|
+
let previousWasGap = false;
|
|
37
|
+
|
|
38
|
+
const lines = text.trim().split("\n");
|
|
39
|
+
for (let index = 0; index < lines.length; index++) {
|
|
40
|
+
const line = lines[index];
|
|
41
|
+
const fence = /^\s*(```|~~~)/.test(line);
|
|
42
|
+
if (fence) {
|
|
43
|
+
if (codeBlock) {
|
|
44
|
+
nodes.push(<Text selectable key={`code-${codeKey++}`} style={styles.fence}>{codeBlock.lines.join("\n")}</Text>);
|
|
45
|
+
codeBlock = null;
|
|
46
|
+
} else {
|
|
47
|
+
codeBlock = { lines: [] };
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (codeBlock) {
|
|
53
|
+
codeBlock.lines.push(line);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!line.trim()) {
|
|
58
|
+
if (!previousWasGap && nodes.length > 0) {
|
|
59
|
+
nodes.push(<View key={`gap-${index}`} style={styles.gap} />);
|
|
60
|
+
}
|
|
61
|
+
previousWasGap = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
previousWasGap = false;
|
|
65
|
+
|
|
66
|
+
if (/^\s{0,3}(?:-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
|
|
67
|
+
nodes.push(<View key={`hr-${index}`} style={styles.hr} />);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const heading = /^(#{1,6})\s+(.+)$/.exec(line);
|
|
72
|
+
if (heading) {
|
|
73
|
+
const level = heading[1].length;
|
|
74
|
+
const headingStyle =
|
|
75
|
+
level === 1 ? styles.heading1 :
|
|
76
|
+
level === 2 ? styles.heading2 :
|
|
77
|
+
level === 3 ? styles.heading3 :
|
|
78
|
+
level === 4 ? styles.heading4 :
|
|
79
|
+
level === 5 ? styles.heading5 :
|
|
80
|
+
styles.heading6;
|
|
81
|
+
nodes.push(<Text selectable key={index} style={headingStyle}>{inlineMarkdown(heading[2], styles)}</Text>);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const bullet = /^\s*[-*+]\s+(.+)$/.exec(line);
|
|
86
|
+
if (bullet) {
|
|
87
|
+
nodes.push(
|
|
88
|
+
<View key={index} style={styles.listItem}>
|
|
89
|
+
<Text style={styles.bulletMarker}>•</Text>
|
|
90
|
+
<Text selectable style={styles.listText}>{inlineMarkdown(bullet[1], styles)}</Text>
|
|
91
|
+
</View>,
|
|
92
|
+
);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const ordered = /^\s*(\d+)\.\s+(.+)$/.exec(line);
|
|
97
|
+
if (ordered) {
|
|
98
|
+
nodes.push(
|
|
99
|
+
<View key={index} style={styles.listItem}>
|
|
100
|
+
<Text style={styles.orderedMarker}>{ordered[1]}.</Text>
|
|
101
|
+
<Text selectable style={styles.listText}>{inlineMarkdown(ordered[2], styles)}</Text>
|
|
102
|
+
</View>,
|
|
103
|
+
);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const quote = /^>\s?(.*)$/.exec(line);
|
|
108
|
+
if (quote) {
|
|
109
|
+
nodes.push(
|
|
110
|
+
<View key={index} style={styles.blockquote}>
|
|
111
|
+
<Text selectable style={styles.paragraph}>{inlineMarkdown(quote[1], styles)}</Text>
|
|
112
|
+
</View>,
|
|
113
|
+
);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
nodes.push(<Text selectable key={index} style={styles.paragraph}>{inlineMarkdown(line, styles)}</Text>);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (codeBlock) {
|
|
121
|
+
nodes.push(<Text selectable key="code-tail" style={styles.fence}>{codeBlock.lines.join("\n")}</Text>);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return nodes;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function createStyles(theme: PluginSurfaceProps["theme"]) {
|
|
128
|
+
return {
|
|
129
|
+
root: { paddingVertical: 12, width: "100%" } as ViewStyle,
|
|
130
|
+
text: { color: theme.colors.foreground, fontSize: 15, lineHeight: 21, flexShrink: 1 } as TextStyle,
|
|
131
|
+
paragraph: { color: theme.colors.foreground, fontSize: 15, lineHeight: 21, marginBottom: 10 } as TextStyle,
|
|
132
|
+
strong: { color: theme.colors.foreground, fontSize: 15, lineHeight: 21, fontWeight: "600" } as TextStyle,
|
|
133
|
+
em: { color: theme.colors.foreground, fontSize: 15, lineHeight: 21, fontStyle: "italic" } as TextStyle,
|
|
134
|
+
strike: { color: theme.colors.foregroundMuted, fontSize: 15, lineHeight: 21, textDecorationLine: "line-through" } as TextStyle,
|
|
135
|
+
codeInline: {
|
|
136
|
+
color: theme.colors.foreground,
|
|
137
|
+
fontSize: 13,
|
|
138
|
+
fontFamily: "monospace",
|
|
139
|
+
backgroundColor: theme.colors.foregroundMuted + "18",
|
|
140
|
+
paddingHorizontal: 4,
|
|
141
|
+
paddingVertical: 2,
|
|
142
|
+
borderRadius: 4,
|
|
143
|
+
} as TextStyle,
|
|
144
|
+
heading1: {
|
|
145
|
+
color: theme.colors.foreground,
|
|
146
|
+
fontSize: 23,
|
|
147
|
+
lineHeight: 30,
|
|
148
|
+
fontWeight: "700",
|
|
149
|
+
marginTop: 18,
|
|
150
|
+
marginBottom: 10,
|
|
151
|
+
borderBottomWidth: 1,
|
|
152
|
+
borderBottomColor: theme.colors.foregroundMuted + "22",
|
|
153
|
+
paddingBottom: 6,
|
|
154
|
+
} as TextStyle,
|
|
155
|
+
heading2: {
|
|
156
|
+
color: theme.colors.foreground,
|
|
157
|
+
fontSize: 20,
|
|
158
|
+
lineHeight: 27,
|
|
159
|
+
fontWeight: "700",
|
|
160
|
+
marginTop: 18,
|
|
161
|
+
marginBottom: 10,
|
|
162
|
+
borderBottomWidth: 1,
|
|
163
|
+
borderBottomColor: theme.colors.foregroundMuted + "22",
|
|
164
|
+
paddingBottom: 6,
|
|
165
|
+
} as TextStyle,
|
|
166
|
+
heading3: { color: theme.colors.foreground, fontSize: 17, lineHeight: 24, fontWeight: "600", marginTop: 12, marginBottom: 6 } as TextStyle,
|
|
167
|
+
heading4: { color: theme.colors.foreground, fontSize: 16, lineHeight: 23, fontWeight: "600", marginTop: 12, marginBottom: 6 } as TextStyle,
|
|
168
|
+
heading5: { color: theme.colors.foreground, fontSize: 15, lineHeight: 22, fontWeight: "600", marginTop: 10, marginBottom: 4 } as TextStyle,
|
|
169
|
+
heading6: {
|
|
170
|
+
color: theme.colors.foregroundMuted,
|
|
171
|
+
fontSize: 15,
|
|
172
|
+
lineHeight: 22,
|
|
173
|
+
fontWeight: "600",
|
|
174
|
+
marginTop: 10,
|
|
175
|
+
marginBottom: 4,
|
|
176
|
+
textTransform: "uppercase",
|
|
177
|
+
letterSpacing: 0.5,
|
|
178
|
+
} as TextStyle,
|
|
179
|
+
fence: {
|
|
180
|
+
color: theme.colors.foreground,
|
|
181
|
+
backgroundColor: theme.colors.foregroundMuted + "12",
|
|
182
|
+
borderColor: theme.colors.foregroundMuted + "28",
|
|
183
|
+
borderWidth: 1,
|
|
184
|
+
borderRadius: 8,
|
|
185
|
+
padding: 12,
|
|
186
|
+
fontSize: 13,
|
|
187
|
+
lineHeight: 19,
|
|
188
|
+
fontFamily: "monospace",
|
|
189
|
+
marginVertical: 8,
|
|
190
|
+
} as TextStyle,
|
|
191
|
+
listItem: { flexDirection: "row", alignItems: "flex-start", marginBottom: 4, flexShrink: 1 } as ViewStyle,
|
|
192
|
+
bulletMarker: { color: theme.colors.foregroundMuted, marginRight: 4, fontSize: 15, lineHeight: 21, width: 14 } as TextStyle,
|
|
193
|
+
orderedMarker: { color: theme.colors.foregroundMuted, marginRight: 4, fontSize: 15, lineHeight: 21, minWidth: 18 } as TextStyle,
|
|
194
|
+
listText: { flex: 1, color: theme.colors.foreground, fontSize: 15, lineHeight: 21, minWidth: 0 } as TextStyle,
|
|
195
|
+
blockquote: {
|
|
196
|
+
backgroundColor: theme.colors.foregroundMuted + "0d",
|
|
197
|
+
borderLeftWidth: 4,
|
|
198
|
+
borderLeftColor: theme.colors.foregroundMuted + "33",
|
|
199
|
+
paddingHorizontal: 12,
|
|
200
|
+
paddingTop: 10,
|
|
201
|
+
paddingBottom: 0,
|
|
202
|
+
marginVertical: 8,
|
|
203
|
+
borderRadius: 8,
|
|
204
|
+
borderTopLeftRadius: 0,
|
|
205
|
+
borderBottomLeftRadius: 0,
|
|
206
|
+
} as ViewStyle,
|
|
207
|
+
hr: { backgroundColor: theme.colors.foregroundMuted + "33", height: 1, marginVertical: 10 } as ViewStyle,
|
|
208
|
+
gap: { height: 10 } as ViewStyle,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function PaseoAssistantMessage({ text, theme }: { text: string; theme: PluginSurfaceProps["theme"] }) {
|
|
213
|
+
const styles = useMemo(() => createStyles(theme), [theme]);
|
|
214
|
+
if (!text.trim()) return null;
|
|
215
|
+
return <View style={styles.root}>{markdownBlocks(text, styles)}</View>;
|
|
216
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import { ScrollView, Text, View, type TextStyle, type ViewStyle } from "react-native";
|
|
3
|
+
import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
|
|
4
|
+
|
|
5
|
+
function shellOutputText(value: unknown): string {
|
|
6
|
+
if (value == null) return "";
|
|
7
|
+
const serialized = typeof value === "string" ? value : JSON.stringify(value);
|
|
8
|
+
const trimmed = serialized.replace(/^\n+/, "");
|
|
9
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return trimmed;
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
12
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return trimmed;
|
|
13
|
+
const record = parsed as Record<string, unknown>;
|
|
14
|
+
if (record.type !== "tool_result") return trimmed;
|
|
15
|
+
if (typeof record.content === "string") return record.content.replace(/^\n+/, "");
|
|
16
|
+
if (Array.isArray(record.content)) {
|
|
17
|
+
const text = record.content
|
|
18
|
+
.map((part) => typeof part === "string"
|
|
19
|
+
? part
|
|
20
|
+
: typeof part === "object" && part !== null && typeof (part as Record<string, unknown>).text === "string"
|
|
21
|
+
? String((part as Record<string, unknown>).text)
|
|
22
|
+
: "")
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
.join("\n");
|
|
25
|
+
return text || trimmed;
|
|
26
|
+
}
|
|
27
|
+
return trimmed;
|
|
28
|
+
} catch {
|
|
29
|
+
return trimmed;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function PaseoShellDetail({
|
|
34
|
+
command,
|
|
35
|
+
output,
|
|
36
|
+
error,
|
|
37
|
+
theme,
|
|
38
|
+
}: {
|
|
39
|
+
command: string;
|
|
40
|
+
output?: unknown;
|
|
41
|
+
error?: unknown;
|
|
42
|
+
theme: PluginSurfaceProps["theme"];
|
|
43
|
+
}) {
|
|
44
|
+
const normalizedCommand = command.replace(/\n+$/, "");
|
|
45
|
+
const normalizedOutput = shellOutputText(output);
|
|
46
|
+
const errorText = shellOutputText(error);
|
|
47
|
+
const styles = useMemo(() => ({
|
|
48
|
+
root: {
|
|
49
|
+
minWidth: 0,
|
|
50
|
+
backgroundColor: theme.colors.surface1,
|
|
51
|
+
} as ViewStyle,
|
|
52
|
+
commandBlock: {
|
|
53
|
+
minWidth: 0,
|
|
54
|
+
backgroundColor: theme.colors.surface1,
|
|
55
|
+
} as ViewStyle,
|
|
56
|
+
commandScroll: {
|
|
57
|
+
maxHeight: 400,
|
|
58
|
+
} as ViewStyle,
|
|
59
|
+
horizontalContent: {
|
|
60
|
+
minWidth: "100%",
|
|
61
|
+
} as ViewStyle,
|
|
62
|
+
codeLine: {
|
|
63
|
+
minWidth: "100%",
|
|
64
|
+
paddingHorizontal: 12,
|
|
65
|
+
paddingVertical: 12,
|
|
66
|
+
} as ViewStyle,
|
|
67
|
+
code: {
|
|
68
|
+
color: theme.colors.foreground,
|
|
69
|
+
fontSize: 12,
|
|
70
|
+
lineHeight: 18,
|
|
71
|
+
fontFamily: "monospace",
|
|
72
|
+
} as TextStyle,
|
|
73
|
+
prompt: {
|
|
74
|
+
color: theme.colors.foregroundMuted,
|
|
75
|
+
} as TextStyle,
|
|
76
|
+
errorSection: {
|
|
77
|
+
gap: 8,
|
|
78
|
+
paddingTop: 8,
|
|
79
|
+
} as ViewStyle,
|
|
80
|
+
errorTitle: {
|
|
81
|
+
color: theme.colors.statusDanger,
|
|
82
|
+
fontSize: 12,
|
|
83
|
+
lineHeight: 16,
|
|
84
|
+
fontWeight: "600",
|
|
85
|
+
letterSpacing: 0.5,
|
|
86
|
+
} as TextStyle,
|
|
87
|
+
errorBox: {
|
|
88
|
+
borderWidth: 1,
|
|
89
|
+
borderColor: theme.colors.statusDanger,
|
|
90
|
+
borderRadius: 6,
|
|
91
|
+
backgroundColor: theme.colors.surface2,
|
|
92
|
+
paddingHorizontal: 12,
|
|
93
|
+
paddingVertical: 10,
|
|
94
|
+
} as ViewStyle,
|
|
95
|
+
errorCode: {
|
|
96
|
+
color: theme.colors.statusDanger,
|
|
97
|
+
fontSize: 12,
|
|
98
|
+
lineHeight: 18,
|
|
99
|
+
fontFamily: "monospace",
|
|
100
|
+
} as TextStyle,
|
|
101
|
+
}), [theme]);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<View style={styles.root}>
|
|
105
|
+
<View style={styles.commandBlock}>
|
|
106
|
+
<ScrollView style={styles.commandScroll} nestedScrollEnabled showsVerticalScrollIndicator>
|
|
107
|
+
<ScrollView horizontal nestedScrollEnabled showsHorizontalScrollIndicator contentContainerStyle={styles.horizontalContent}>
|
|
108
|
+
<View style={styles.codeLine}>
|
|
109
|
+
<Text selectable style={styles.code}>
|
|
110
|
+
<Text style={styles.prompt}>$ </Text>
|
|
111
|
+
{normalizedCommand}
|
|
112
|
+
{normalizedOutput ? `\n\n${normalizedOutput}` : ""}
|
|
113
|
+
</Text>
|
|
114
|
+
</View>
|
|
115
|
+
</ScrollView>
|
|
116
|
+
</ScrollView>
|
|
117
|
+
</View>
|
|
118
|
+
{errorText ? (
|
|
119
|
+
<View style={styles.errorSection}>
|
|
120
|
+
<Text style={styles.errorTitle}>错误</Text>
|
|
121
|
+
<ScrollView horizontal nestedScrollEnabled showsHorizontalScrollIndicator style={styles.errorBox}>
|
|
122
|
+
<Text selectable style={styles.errorCode}>{errorText}</Text>
|
|
123
|
+
</ScrollView>
|
|
124
|
+
</View>
|
|
125
|
+
) : null}
|
|
126
|
+
</View>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import React, { useMemo, useState, type ReactNode } from "react";
|
|
2
|
+
import { Pressable, Text, View, type TextStyle, type ViewStyle } from "react-native";
|
|
3
|
+
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
4
|
+
import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
|
|
5
|
+
|
|
6
|
+
function detailText(value: unknown): string {
|
|
7
|
+
if (value == null) return "";
|
|
8
|
+
if (typeof value === "string") return value;
|
|
9
|
+
try { return JSON.stringify(value, null, 2); } catch { return String(value); }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function PaseoStreamBadge({
|
|
13
|
+
label,
|
|
14
|
+
secondaryLabel,
|
|
15
|
+
icon,
|
|
16
|
+
detail,
|
|
17
|
+
children,
|
|
18
|
+
loading = false,
|
|
19
|
+
error = false,
|
|
20
|
+
sequenceEnd = false,
|
|
21
|
+
borderlessWhenExpanded = false,
|
|
22
|
+
fullBleedDetail = false,
|
|
23
|
+
theme,
|
|
24
|
+
}: {
|
|
25
|
+
label: string;
|
|
26
|
+
secondaryLabel?: string;
|
|
27
|
+
icon: string;
|
|
28
|
+
detail?: unknown;
|
|
29
|
+
children?: ReactNode;
|
|
30
|
+
loading?: boolean;
|
|
31
|
+
error?: boolean;
|
|
32
|
+
sequenceEnd?: boolean;
|
|
33
|
+
borderlessWhenExpanded?: boolean;
|
|
34
|
+
fullBleedDetail?: boolean;
|
|
35
|
+
theme: PluginSurfaceProps["theme"];
|
|
36
|
+
}) {
|
|
37
|
+
const [expanded, setExpanded] = useState(false);
|
|
38
|
+
const content = detailText(detail);
|
|
39
|
+
const interactive = content.length > 0 || Boolean(children);
|
|
40
|
+
const styles = useMemo(() => ({
|
|
41
|
+
root: { marginHorizontal: -13, marginBottom: sequenceEnd ? 16 : 4 } as ViewStyle,
|
|
42
|
+
header: {
|
|
43
|
+
minHeight: 32,
|
|
44
|
+
paddingHorizontal: 8,
|
|
45
|
+
paddingVertical: 4,
|
|
46
|
+
flexDirection: "row",
|
|
47
|
+
alignItems: "center",
|
|
48
|
+
borderRadius: 8,
|
|
49
|
+
borderWidth: expanded ? 1 : 0,
|
|
50
|
+
borderColor: borderlessWhenExpanded ? "transparent" : theme.colors.foregroundMuted + "2a",
|
|
51
|
+
borderBottomLeftRadius: expanded ? 0 : 8,
|
|
52
|
+
borderBottomRightRadius: expanded ? 0 : 8,
|
|
53
|
+
backgroundColor: expanded ? theme.colors.foregroundMuted + "0d" : "transparent",
|
|
54
|
+
} as ViewStyle,
|
|
55
|
+
icon: { width: 22, height: 22, alignItems: "center", justifyContent: "center", marginRight: 4 } as ViewStyle,
|
|
56
|
+
label: { color: expanded || loading ? theme.colors.foreground : theme.colors.foregroundMuted, fontSize: 14 } as TextStyle,
|
|
57
|
+
secondary: { color: theme.colors.foregroundMuted, fontSize: 14, marginLeft: 8, flex: 1 } as TextStyle,
|
|
58
|
+
detail: {
|
|
59
|
+
borderWidth: borderlessWhenExpanded ? 0 : 1,
|
|
60
|
+
borderTopWidth: 0,
|
|
61
|
+
borderColor: theme.colors.foregroundMuted + "2a",
|
|
62
|
+
borderBottomLeftRadius: 8,
|
|
63
|
+
borderBottomRightRadius: 8,
|
|
64
|
+
paddingTop: fullBleedDetail ? 0 : 4,
|
|
65
|
+
paddingHorizontal: fullBleedDetail ? 0 : 13,
|
|
66
|
+
paddingBottom: fullBleedDetail || borderlessWhenExpanded ? 0 : 10,
|
|
67
|
+
maxHeight: borderlessWhenExpanded || fullBleedDetail ? 400 : 260,
|
|
68
|
+
overflow: "scroll",
|
|
69
|
+
backgroundColor: borderlessWhenExpanded ? "transparent" : fullBleedDetail ? theme.colors.surface1 : theme.colors.surface0,
|
|
70
|
+
} as ViewStyle,
|
|
71
|
+
detailText: { color: theme.colors.foreground, fontSize: 12, lineHeight: 18, fontFamily: "monospace" } as TextStyle,
|
|
72
|
+
}), [borderlessWhenExpanded, expanded, fullBleedDetail, loading, sequenceEnd, theme]);
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<View style={styles.root}>
|
|
76
|
+
<Pressable disabled={!interactive} onPress={() => setExpanded((value) => !value)} style={styles.header}>
|
|
77
|
+
<View style={styles.icon}>
|
|
78
|
+
<Icon
|
|
79
|
+
name={interactive && expanded ? "ChevronDown" : error ? "TriangleAlert" : icon}
|
|
80
|
+
color={error ? theme.colors.statusDanger : expanded ? theme.colors.foreground : theme.colors.foregroundMuted}
|
|
81
|
+
size={12}
|
|
82
|
+
/>
|
|
83
|
+
</View>
|
|
84
|
+
<Text style={styles.label} numberOfLines={1}>{label}</Text>
|
|
85
|
+
{secondaryLabel ? <Text style={styles.secondary} numberOfLines={1}>{secondaryLabel}</Text> : null}
|
|
86
|
+
</Pressable>
|
|
87
|
+
{expanded ? (
|
|
88
|
+
<View style={styles.detail}>
|
|
89
|
+
{children ?? <Text selectable style={styles.detailText}>{content}</Text>}
|
|
90
|
+
</View>
|
|
91
|
+
) : null}
|
|
92
|
+
</View>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import React, { useMemo, useState } from "react";
|
|
2
|
+
import { Pressable, Text, View, type TextStyle, type ViewStyle } from "react-native";
|
|
3
|
+
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
4
|
+
import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
|
|
5
|
+
import type { AgentTimelineEntry } from "./agent-conversation-types";
|
|
6
|
+
import { PaseoStreamBadge } from "./PaseoStreamBadge";
|
|
7
|
+
import { PaseoAssistantMessage } from "./PaseoAssistantMessage";
|
|
8
|
+
import { PaseoShellDetail } from "./PaseoShellDetail";
|
|
9
|
+
import { PaseoToolDetail } from "./PaseoToolDetail";
|
|
10
|
+
import type { AgentCapabilityFlags } from "@getpaseo/protocol/agent-types";
|
|
11
|
+
|
|
12
|
+
type RewindMode = "conversation" | "files" | "both";
|
|
13
|
+
|
|
14
|
+
function toolPresentation(item: AgentTimelineEntry["item"]): { label: string; secondary?: string; detail?: unknown; icon: string } {
|
|
15
|
+
const detail = item.detail as Record<string, unknown> | undefined;
|
|
16
|
+
const detailType = detail?.type;
|
|
17
|
+
const rawName = String(item.name ?? "Tool");
|
|
18
|
+
const secondary = typeof detail?.command === "string" ? detail.command
|
|
19
|
+
: typeof detail?.filePath === "string" ? detail.filePath
|
|
20
|
+
: typeof detail?.query === "string" ? detail.query
|
|
21
|
+
: typeof detail?.url === "string" ? detail.url
|
|
22
|
+
: typeof detail?.branchName === "string" ? detail.branchName
|
|
23
|
+
: typeof detail?.description === "string" ? detail.description
|
|
24
|
+
: typeof detail?.label === "string" ? detail.label
|
|
25
|
+
: typeof detail?.path === "string" ? detail.path : undefined;
|
|
26
|
+
return {
|
|
27
|
+
label: item.name === "thinking" ? "Thinking"
|
|
28
|
+
: detailType === "edit" ? "Edit"
|
|
29
|
+
: detailType === "write" ? "Write"
|
|
30
|
+
: detailType === "shell" ? "Shell"
|
|
31
|
+
: detailType === "read" ? "Read"
|
|
32
|
+
: detailType === "search" ? "Search"
|
|
33
|
+
: detailType === "fetch" ? "Fetch"
|
|
34
|
+
: detailType === "worktree_setup" ? "Worktree Setup"
|
|
35
|
+
: detailType === "sub_agent" && typeof detail?.subAgentType === "string" ? detail.subAgentType
|
|
36
|
+
: detailType === "sub_agent" ? "Task"
|
|
37
|
+
: detailType === "plan" ? "Plan" : rawName,
|
|
38
|
+
secondary,
|
|
39
|
+
detail: detail ?? item.result ?? item.error,
|
|
40
|
+
icon: item.name === "thinking" ? "Brain"
|
|
41
|
+
: detailType === "edit" || detailType === "write" ? "Pencil"
|
|
42
|
+
: detailType === "shell" ? "SquareTerminal"
|
|
43
|
+
: detailType === "read" ? "Eye"
|
|
44
|
+
: detailType === "search" ? "Search"
|
|
45
|
+
: detailType === "sub_agent" ? "Bot" : "Wrench",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatDuration(durationMs: number): string {
|
|
50
|
+
const seconds = Math.max(0, durationMs) / 1000;
|
|
51
|
+
if (seconds < 60) return `${seconds < 10 ? seconds.toFixed(1) : Math.round(seconds)}s`;
|
|
52
|
+
const minutes = Math.floor(seconds / 60);
|
|
53
|
+
const remainder = Math.round(seconds % 60);
|
|
54
|
+
return remainder ? `${minutes}m ${remainder}s` : `${minutes}m`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function PaseoTimelineItem({ entry, theme, durationMs, completedAt, rewindCapabilities, rewinding = false, onRewind }: { entry: AgentTimelineEntry; theme: PluginSurfaceProps["theme"]; durationMs?: number; completedAt?: string; rewindCapabilities?: AgentCapabilityFlags | null; rewinding?: boolean; onRewind?: (messageId: string, text: string, mode: RewindMode) => void }) {
|
|
58
|
+
const item = entry.item;
|
|
59
|
+
const [copied, setCopied] = useState(false);
|
|
60
|
+
const [rewindMenuOpen, setRewindMenuOpen] = useState(false);
|
|
61
|
+
const copyText = async (text: string) => {
|
|
62
|
+
const clipboard = (navigator as unknown as { clipboard?: { writeText(value: string): Promise<void> } }).clipboard;
|
|
63
|
+
if (!clipboard) return;
|
|
64
|
+
await clipboard.writeText(text);
|
|
65
|
+
setCopied(true);
|
|
66
|
+
setTimeout(() => setCopied(false), 1800);
|
|
67
|
+
};
|
|
68
|
+
const styles = useMemo(() => ({
|
|
69
|
+
userRoot: { flexDirection: "row", justifyContent: "flex-end", marginVertical: 16 } as ViewStyle,
|
|
70
|
+
userBubble: { maxWidth: "92%", alignSelf: "flex-end", backgroundColor: theme.colors.surface2, borderRadius: 20, borderTopRightRadius: 4, paddingHorizontal: 16, paddingVertical: 16, minWidth: 0, flexShrink: 1 } as ViewStyle,
|
|
71
|
+
userText: { color: theme.colors.foreground, fontSize: 15, lineHeight: 21 } as TextStyle,
|
|
72
|
+
error: { flexDirection: "row", gap: 8, paddingVertical: 10, alignItems: "flex-start" } as ViewStyle,
|
|
73
|
+
errorText: { flex: 1, color: theme.colors.statusDanger, fontSize: 14, lineHeight: 20 } as TextStyle,
|
|
74
|
+
compact: { flexDirection: "row", alignItems: "center", gap: 8, paddingVertical: 12 } as ViewStyle,
|
|
75
|
+
compactLine: { height: 1, flex: 1, backgroundColor: theme.colors.foregroundMuted + "2a" } as ViewStyle,
|
|
76
|
+
compactText: { color: theme.colors.foregroundMuted, fontSize: 13 } as TextStyle,
|
|
77
|
+
copyButton: { width: 26, height: 26, borderRadius: 6, alignItems: "center", justifyContent: "center" } as ViewStyle,
|
|
78
|
+
assistantFooter: { flexDirection: "row", alignItems: "center", minHeight: 28, marginTop: 4 } as ViewStyle,
|
|
79
|
+
timing: { color: theme.colors.foregroundMuted, fontSize: 11 } as TextStyle,
|
|
80
|
+
userActions: { width: "100%", minWidth: 0, alignItems: "flex-end", justifyContent: "flex-end", paddingRight: 4 } as ViewStyle,
|
|
81
|
+
actionRow: { flexDirection: "row", alignItems: "center", gap: 2 } as ViewStyle,
|
|
82
|
+
rewindMenu: { minWidth: 210, borderWidth: 1, borderColor: theme.colors.border, borderRadius: 8, backgroundColor: theme.colors.surface1, paddingVertical: 4, marginTop: 2 } as ViewStyle,
|
|
83
|
+
rewindMenuItem: { minHeight: 34, paddingHorizontal: 10, justifyContent: "center" } as ViewStyle,
|
|
84
|
+
}), [theme]);
|
|
85
|
+
|
|
86
|
+
const copyButton = (text: string) => <Pressable
|
|
87
|
+
accessibilityRole="button"
|
|
88
|
+
accessibilityLabel={copied ? "已复制" : "复制消息"}
|
|
89
|
+
onPress={() => void copyText(text)}
|
|
90
|
+
style={styles.copyButton}
|
|
91
|
+
><Icon name={copied ? "Check" : "Copy"} color={theme.colors.foregroundMuted} size={14} /></Pressable>;
|
|
92
|
+
|
|
93
|
+
if (item.type === "user_message") {
|
|
94
|
+
const messageId = typeof item.messageId === "string" ? item.messageId : null;
|
|
95
|
+
const rewindItems: Array<{ mode: RewindMode; label: string }> = [];
|
|
96
|
+
if (rewindCapabilities?.supportsRewindConversation) rewindItems.push({ mode: "conversation", label: "回退会话" });
|
|
97
|
+
if (rewindCapabilities?.supportsRewindFiles) rewindItems.push({ mode: "files", label: "回退文件" });
|
|
98
|
+
if (rewindCapabilities?.supportsRewindBoth) rewindItems.push({ mode: "both", label: "回退会话和文件" });
|
|
99
|
+
return <View style={styles.userRoot}><View style={styles.userActions}><View style={styles.userBubble}><Text selectable style={styles.userText}>{item.text}</Text></View><View style={styles.actionRow}>{copyButton(String(item.text ?? ""))}{messageId && rewindItems.length ? <Pressable accessibilityRole="button" accessibilityLabel={rewinding ? "正在回退" : "回退到此消息"} disabled={rewinding} onPress={() => setRewindMenuOpen((open) => !open)} style={styles.copyButton}><Icon name={rewinding ? "LoaderCircle" : "Undo2"} color={theme.colors.foregroundMuted} size={14} /></Pressable> : null}</View>{rewindMenuOpen && messageId ? <View accessibilityLabel="回退选项" style={styles.rewindMenu}>{rewindItems.map((option) => <Pressable key={option.mode} accessibilityRole="button" accessibilityLabel={option.label} onPress={() => { setRewindMenuOpen(false); onRewind?.(messageId, String(item.text ?? ""), option.mode); }} style={styles.rewindMenuItem}><Text style={{ color: theme.colors.foreground, fontSize: 13 }}>{option.label}</Text></Pressable>)}</View> : null}</View></View>;
|
|
100
|
+
}
|
|
101
|
+
if (item.type === "assistant_message") return <View><PaseoAssistantMessage text={item.text} theme={theme} /><View style={styles.assistantFooter}>{copyButton(String(item.text ?? ""))}{durationMs != null ? <Text accessibilityLabel={`完成于 ${completedAt ?? entry.timestamp}`} style={styles.timing}>Worked for {formatDuration(durationMs)}</Text> : null}</View></View>;
|
|
102
|
+
if (item.type === "reasoning") return <PaseoStreamBadge label="Thinking" icon="Brain" detail={item.text} loading={item.status !== "ready"} theme={theme} />;
|
|
103
|
+
if (item.type === "tool_call") {
|
|
104
|
+
const presentation = toolPresentation(item);
|
|
105
|
+
const shellDetail = item.detail?.type === "shell" ? item.detail : null;
|
|
106
|
+
const semanticDetail = item.detail && item.detail.type !== "shell" ? item.detail : null;
|
|
107
|
+
return (
|
|
108
|
+
<PaseoStreamBadge
|
|
109
|
+
label={presentation.label}
|
|
110
|
+
secondaryLabel={presentation.secondary}
|
|
111
|
+
icon={presentation.icon}
|
|
112
|
+
detail={undefined}
|
|
113
|
+
loading={item.status === "running" || item.status === "executing"}
|
|
114
|
+
error={item.status === "failed"}
|
|
115
|
+
fullBleedDetail={Boolean(shellDetail || semanticDetail)}
|
|
116
|
+
theme={theme}
|
|
117
|
+
>
|
|
118
|
+
{shellDetail ? (
|
|
119
|
+
<PaseoShellDetail
|
|
120
|
+
command={String(shellDetail.command ?? "")}
|
|
121
|
+
output={shellDetail.output}
|
|
122
|
+
error={item.status === "failed" ? item.error : null}
|
|
123
|
+
theme={theme}
|
|
124
|
+
/>
|
|
125
|
+
) : semanticDetail ? <PaseoToolDetail detail={semanticDetail} theme={theme} /> : null}
|
|
126
|
+
</PaseoStreamBadge>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
if (item.type === "todo") {
|
|
130
|
+
const done = item.items?.filter((todo: { completed?: boolean }) => todo.completed).length ?? 0;
|
|
131
|
+
const total = item.items?.length ?? 0;
|
|
132
|
+
const detail = item.items?.map((todo: { completed?: boolean; text: string }) => `${todo.completed ? "✓" : "○"} ${todo.text}`).join("\n");
|
|
133
|
+
return <PaseoStreamBadge label="Updated tasks" secondaryLabel={`${done}/${total}`} icon="ListChecks" detail={detail} theme={theme} />;
|
|
134
|
+
}
|
|
135
|
+
if (item.type === "error") return <View style={styles.error}><Icon name="CircleX" color={theme.colors.statusDanger} size={16} /><Text selectable style={styles.errorText}>{item.message}</Text></View>;
|
|
136
|
+
if (item.type === "notification") {
|
|
137
|
+
const color = item.level === "error"
|
|
138
|
+
? theme.colors.statusDanger
|
|
139
|
+
: item.level === "warning"
|
|
140
|
+
? theme.colors.statusWarning
|
|
141
|
+
: theme.colors.foregroundMuted;
|
|
142
|
+
return <View style={styles.error}><Icon name={item.level === "error" ? "CircleX" : item.level === "warning" ? "TriangleAlert" : "Info"} color={color} size={16} /><Text selectable style={[styles.errorText, { color }]}>{item.message}</Text></View>;
|
|
143
|
+
}
|
|
144
|
+
if (item.type === "plugin") return <PaseoStreamBadge label={`Plugin · ${item.kind}`} icon="Puzzle" detail={item.data} theme={theme} />;
|
|
145
|
+
if (item.type === "compaction") return <View style={styles.compact}><View style={styles.compactLine} /><Icon name="Scissors" color={theme.colors.foregroundMuted} size={12} /><Text style={styles.compactText}>上下文已压缩</Text><View style={styles.compactLine} /></View>;
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
|
|
3
|
+
import type { AgentTimelineEntry } from "./agent-conversation-types";
|
|
4
|
+
import { PaseoStreamBadge } from "./PaseoStreamBadge";
|
|
5
|
+
import { PaseoTimelineItem } from "./PaseoTimelineItem";
|
|
6
|
+
|
|
7
|
+
function toolKind(entry: AgentTimelineEntry): "edit" | "command" | "read" | "search" | "paseo" | "other" {
|
|
8
|
+
const item = entry.item;
|
|
9
|
+
const detailType = item.detail?.type;
|
|
10
|
+
const name = String(item.name ?? "").toLowerCase();
|
|
11
|
+
if (name.startsWith("paseo_") || name.startsWith("mcp__paseo__")) return "paseo";
|
|
12
|
+
if (detailType === "edit" || detailType === "write" || /apply[_-]?patch|edit|write/.test(name)) return "edit";
|
|
13
|
+
if (detailType === "shell" || /exec|command|shell|terminal/.test(name)) return "command";
|
|
14
|
+
if (detailType === "read" || /read|view[_-]?image/.test(name)) return "read";
|
|
15
|
+
if (detailType === "search" || /search|find|grep|glob|web_search|llm_context/.test(name)) return "search";
|
|
16
|
+
return "other";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function groupSummary(entries: AgentTimelineEntry[]): string {
|
|
20
|
+
const editedFiles = new Set<string>();
|
|
21
|
+
const readFiles = new Set<string>();
|
|
22
|
+
const counts = { edit: 0, command: 0, read: 0, search: 0, paseo: 0, other: 0 };
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const kind = toolKind(entry);
|
|
25
|
+
const path = entry.item.detail?.filePath ?? entry.item.detail?.path;
|
|
26
|
+
if (kind === "edit" && typeof path === "string") editedFiles.add(path);
|
|
27
|
+
else if (kind === "read" && typeof path === "string") readFiles.add(path);
|
|
28
|
+
else counts[kind] += 1;
|
|
29
|
+
}
|
|
30
|
+
counts.edit += editedFiles.size;
|
|
31
|
+
counts.read += readFiles.size;
|
|
32
|
+
const parts: string[] = [];
|
|
33
|
+
if (counts.edit) parts.push(`编辑了 ${counts.edit} 个文件`);
|
|
34
|
+
if (counts.command) parts.push(`运行了 ${counts.command} 个命令`);
|
|
35
|
+
if (counts.read) parts.push(`读取了 ${counts.read} 个文件`);
|
|
36
|
+
if (counts.search) parts.push(`搜索了 ${counts.search} 次`);
|
|
37
|
+
if (counts.other) parts.push(`使用了 ${counts.other} 个其他工具`);
|
|
38
|
+
if (counts.paseo) parts.push(`调用了 Paseo ${counts.paseo} 次`);
|
|
39
|
+
if (parts.length <= 1) return parts[0] ?? "使用了工具";
|
|
40
|
+
return `${parts.slice(0, -1).join(",")}并${parts.at(-1)}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function PaseoToolCallGroup({ entries, theme }: { entries: AgentTimelineEntry[]; theme: PluginSurfaceProps["theme"] }) {
|
|
44
|
+
const summary = useMemo(() => groupSummary(entries), [entries]);
|
|
45
|
+
const loading = entries.some((entry) => entry.item.status === "running" || entry.item.status === "executing");
|
|
46
|
+
const error = entries.some((entry) => entry.item.status === "failed");
|
|
47
|
+
return (
|
|
48
|
+
<PaseoStreamBadge label={summary} icon="Wrench" loading={loading} error={error} sequenceEnd borderlessWhenExpanded theme={theme}>
|
|
49
|
+
{entries.map((entry) => <PaseoTimelineItem key={`${entry.seqStart}:${entry.item.type}`} entry={entry} theme={theme} />)}
|
|
50
|
+
</PaseoStreamBadge>
|
|
51
|
+
);
|
|
52
|
+
}
|