@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,56 @@
|
|
|
1
|
+
import React, { useCallback, useRef } from "react";
|
|
2
|
+
import { Pressable, Text, View, type ViewStyle } from "react-native";
|
|
3
|
+
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
4
|
+
|
|
5
|
+
import { useTooltip } from "../components/Tooltip";
|
|
6
|
+
|
|
7
|
+
export function QuickActionButton({ icon, color, disabled, tooltip, accessibilityLabel, onPress, style }: {
|
|
8
|
+
icon: string; color: string; disabled?: boolean; tooltip: Array<{ key: string; value: string }>;
|
|
9
|
+
accessibilityLabel: string; onPress: () => void; style: ViewStyle;
|
|
10
|
+
}) {
|
|
11
|
+
const tooltipContext = useTooltip();
|
|
12
|
+
const buttonRef = useRef<View | null>(null);
|
|
13
|
+
const showTooltip = useCallback(() => {
|
|
14
|
+
if (!tooltipContext || !buttonRef.current) return;
|
|
15
|
+
buttonRef.current.measureInWindow((x, y, width, height) => {
|
|
16
|
+
tooltipContext.show({ x: x + width, y: y + height, lines: tooltip });
|
|
17
|
+
});
|
|
18
|
+
}, [tooltip, tooltipContext]);
|
|
19
|
+
const hideTooltip = useCallback(() => tooltipContext?.hide(), [tooltipContext]);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Pressable
|
|
23
|
+
ref={buttonRef}
|
|
24
|
+
accessibilityLabel={accessibilityLabel}
|
|
25
|
+
onPress={onPress}
|
|
26
|
+
disabled={disabled}
|
|
27
|
+
style={[style, disabled && { opacity: 0.55 }]}
|
|
28
|
+
{...({ onMouseEnter: showTooltip, onMouseLeave: hideTooltip } as Record<string, unknown>)}
|
|
29
|
+
>
|
|
30
|
+
<Icon name={icon} size={14} color={color} />
|
|
31
|
+
</Pressable>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function AutoCommitErrorToast({ message, copied, colors, containerStyle, copyButtonStyle, onDismiss, onCopy }: {
|
|
36
|
+
message: string; copied: boolean;
|
|
37
|
+
colors: { statusDanger: string; foregroundMuted: string; foreground: string };
|
|
38
|
+
containerStyle: ViewStyle; copyButtonStyle: ViewStyle; onDismiss: () => void; onCopy: () => void;
|
|
39
|
+
}) {
|
|
40
|
+
return (
|
|
41
|
+
<View style={containerStyle}>
|
|
42
|
+
<View style={{ flexDirection: "row", alignItems: "flex-start" }}>
|
|
43
|
+
<Icon name="TriangleAlert" size={14} color={colors.statusDanger} />
|
|
44
|
+
<Text style={{ color: colors.statusDanger, fontSize: 12, fontWeight: "600", marginLeft: 7, flex: 1 }}>一键提交失败</Text>
|
|
45
|
+
<Pressable onPress={onDismiss} style={{ padding: 2 }}><Icon name="X" size={13} color={colors.foregroundMuted} /></Pressable>
|
|
46
|
+
</View>
|
|
47
|
+
<Text selectable numberOfLines={4} style={{ color: colors.foreground, fontSize: 12, lineHeight: 17, marginTop: 6 }}>{message}</Text>
|
|
48
|
+
<View style={{ flexDirection: "row", justifyContent: "flex-end", marginTop: 8 }}>
|
|
49
|
+
<Pressable onPress={onCopy} style={copyButtonStyle}>
|
|
50
|
+
<Icon name={copied ? "Check" : "Copy"} size={12} color={colors.foregroundMuted} />
|
|
51
|
+
<Text style={{ color: colors.foregroundMuted, fontSize: 11 }}>{copied ? "已复制" : "复制错误"}</Text>
|
|
52
|
+
</Pressable>
|
|
53
|
+
</View>
|
|
54
|
+
</View>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useRpc } from "@getpaseo/plugin/client";
|
|
3
|
+
import { copyText } from "@getpaseo/plugin/client/react-native";
|
|
4
|
+
|
|
5
|
+
import { autoCommitStartRpc, autoCommitStatusRpc } from "../../shared/auto-commit";
|
|
6
|
+
|
|
7
|
+
export type AutoCommitState =
|
|
8
|
+
| { kind: "idle" }
|
|
9
|
+
| { kind: "preparing"; message: string }
|
|
10
|
+
| { kind: "running"; taskId: string; message: string }
|
|
11
|
+
| { kind: "done"; taskId: string; message: string }
|
|
12
|
+
| { kind: "error"; message: string; taskId?: string };
|
|
13
|
+
|
|
14
|
+
export function useAutoCommit(cwd: string | undefined, onDone: () => void) {
|
|
15
|
+
const startTask = useRpc(autoCommitStartRpc);
|
|
16
|
+
const getTaskStatus = useRpc(autoCommitStatusRpc);
|
|
17
|
+
const [pending, setPending] = useState(false);
|
|
18
|
+
const [state, setState] = useState<AutoCommitState>({ kind: "idle" });
|
|
19
|
+
const [copied, setCopied] = useState(false);
|
|
20
|
+
|
|
21
|
+
const runRef = useRef<{ cancelled: boolean; cancelWait?: () => void } | null>(null);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
setPending(false);
|
|
25
|
+
setState({ kind: "idle" });
|
|
26
|
+
setCopied(false);
|
|
27
|
+
return () => {
|
|
28
|
+
const run = runRef.current;
|
|
29
|
+
if (run) {
|
|
30
|
+
run.cancelled = true;
|
|
31
|
+
run.cancelWait?.();
|
|
32
|
+
}
|
|
33
|
+
runRef.current = null;
|
|
34
|
+
};
|
|
35
|
+
}, [cwd]);
|
|
36
|
+
|
|
37
|
+
const start = useCallback(async () => {
|
|
38
|
+
if (runRef.current) return;
|
|
39
|
+
if (!cwd) {
|
|
40
|
+
setState({ kind: "error", message: "当前 agent 没有可用工作目录" });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const run = { cancelled: false } as { cancelled: boolean; cancelWait?: () => void };
|
|
44
|
+
runRef.current = run;
|
|
45
|
+
setPending(true);
|
|
46
|
+
setCopied(false);
|
|
47
|
+
setState({ kind: "preparing", message: "正在启动 Pi 独立进程" });
|
|
48
|
+
try {
|
|
49
|
+
const { taskId } = await startTask({ cwd });
|
|
50
|
+
if (run.cancelled) return;
|
|
51
|
+
setState({ kind: "running", taskId, message: "Pi 已启动,正在提交" });
|
|
52
|
+
for (;;) {
|
|
53
|
+
await new Promise<void>((resolve) => {
|
|
54
|
+
const timer = setTimeout(resolve, 1_200);
|
|
55
|
+
run.cancelWait = () => { clearTimeout(timer); resolve(); };
|
|
56
|
+
});
|
|
57
|
+
run.cancelWait = undefined;
|
|
58
|
+
if (run.cancelled) return;
|
|
59
|
+
const status = await getTaskStatus({ taskId });
|
|
60
|
+
if (run.cancelled) return;
|
|
61
|
+
const message = status.output.trim().split("\n").filter(Boolean).slice(-1)[0] ?? "Pi 正在运行";
|
|
62
|
+
if (status.status === "running") {
|
|
63
|
+
setState({ kind: "running", taskId, message });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (status.status === "done") {
|
|
67
|
+
setState({ kind: "done", taskId, message: message || "Pi 已完成提交" });
|
|
68
|
+
onDone();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
setState({ kind: "error", taskId, message: [status.error, status.output].filter(Boolean).join("\n").trim() || "Pi 提交失败" });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (run.cancelled) return;
|
|
76
|
+
setState({ kind: "error", message: error instanceof Error ? error.message : String(error) });
|
|
77
|
+
} finally {
|
|
78
|
+
if (runRef.current === run) {
|
|
79
|
+
runRef.current = null;
|
|
80
|
+
setPending(false);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}, [cwd, getTaskStatus, onDone, startTask]);
|
|
84
|
+
|
|
85
|
+
const copyError = useCallback(() => {
|
|
86
|
+
if (state.kind !== "error") return;
|
|
87
|
+
void copyText(state.message).then(() => setCopied(true)).catch(() => {});
|
|
88
|
+
}, [state]);
|
|
89
|
+
|
|
90
|
+
return { state, pending, copied, start, copyError, dismiss: () => setState({ kind: "idle" }) };
|
|
91
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ActivityIndicator, Pressable, ScrollView, Text, View } from "react-native";
|
|
3
|
+
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
4
|
+
import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
|
|
5
|
+
|
|
6
|
+
export interface AgentSlashCommand {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
argumentHint: string;
|
|
10
|
+
kind?: "command" | "skill";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function filterAgentCommands(commands: AgentSlashCommand[], query: string) {
|
|
14
|
+
const normalized = query.trim().toLowerCase();
|
|
15
|
+
return commands.filter((command) => command.name.toLowerCase().includes(normalized)).slice(0, 12);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function AgentCommandMenu({ commands, query, loading, error, theme, activeIndex, onSelect }: {
|
|
19
|
+
commands: AgentSlashCommand[];
|
|
20
|
+
query: string;
|
|
21
|
+
loading: boolean;
|
|
22
|
+
error: string | null;
|
|
23
|
+
theme: PluginSurfaceProps["theme"];
|
|
24
|
+
activeIndex: number;
|
|
25
|
+
onSelect: (command: AgentSlashCommand) => void;
|
|
26
|
+
}) {
|
|
27
|
+
const matches = filterAgentCommands(commands, query);
|
|
28
|
+
return <View style={{ maxHeight: 260, marginBottom: 8, borderWidth: 1, borderColor: theme.colors.border, borderRadius: 8, backgroundColor: theme.colors.surface1, overflow: "hidden" }}>
|
|
29
|
+
{loading ? <View style={{ minHeight: 52, alignItems: "center", justifyContent: "center" }}><ActivityIndicator size="small" color={theme.colors.foregroundMuted} /></View> : null}
|
|
30
|
+
{!loading && error ? <Text style={{ padding: 12, color: theme.colors.statusDanger, fontSize: 12 }}>{error}</Text> : null}
|
|
31
|
+
{!loading && !error && matches.length === 0 ? <Text style={{ padding: 12, color: theme.colors.foregroundMuted, fontSize: 12 }}>没有匹配的命令</Text> : null}
|
|
32
|
+
{!loading && !error ? <ScrollView keyboardShouldPersistTaps="handled">
|
|
33
|
+
{matches.map((command, index) => <Pressable
|
|
34
|
+
key={`${command.kind ?? "command"}:${command.name}`}
|
|
35
|
+
accessibilityRole="button"
|
|
36
|
+
accessibilityLabel={`选择命令 /${command.name}`}
|
|
37
|
+
onPress={() => onSelect(command)}
|
|
38
|
+
style={({ hovered, pressed }: { hovered?: boolean; pressed?: boolean }) => ({ paddingHorizontal: 12, paddingVertical: 9, flexDirection: "row", gap: 10, backgroundColor: index === activeIndex || hovered || pressed ? theme.colors.surface2 : theme.colors.surface1 })}
|
|
39
|
+
>
|
|
40
|
+
<Icon name={command.kind === "skill" ? "Sparkles" : "Terminal"} size={14} color={theme.colors.foregroundMuted} />
|
|
41
|
+
<View style={{ flex: 1, minWidth: 0 }}>
|
|
42
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
43
|
+
<Text style={{ color: theme.colors.foreground, fontSize: 13, fontWeight: "600" }}>/{command.name}</Text>
|
|
44
|
+
{command.argumentHint ? <Text style={{ color: theme.colors.foregroundMuted, fontSize: 12 }}>{command.argumentHint}</Text> : null}
|
|
45
|
+
</View>
|
|
46
|
+
{command.description ? <Text numberOfLines={2} style={{ color: theme.colors.foregroundMuted, fontSize: 12, marginTop: 2 }}>{command.description}</Text> : null}
|
|
47
|
+
</View>
|
|
48
|
+
</Pressable>)}
|
|
49
|
+
</ScrollView> : null}
|
|
50
|
+
</View>;
|
|
51
|
+
}
|