@yannelli/paseo-linear-plugin 1.0.0 → 1.1.0-alpha.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 (65) hide show
  1. package/OVERVIEW.md +32 -2
  2. package/README.md +138 -11
  3. package/client/access.tsx +67 -0
  4. package/client/agent-options.ts +12 -2
  5. package/client/agent-preferences.ts +112 -0
  6. package/client/browser.tsx +12 -9
  7. package/client/compat.ts +23 -5
  8. package/client/connect.tsx +3 -2
  9. package/client/launch-assignments.tsx +182 -0
  10. package/client/launch-choices.ts +23 -6
  11. package/client/launch-guidance.tsx +103 -0
  12. package/client/launch-plan.ts +166 -42
  13. package/client/launch.tsx +55 -9
  14. package/client/live-agents.tsx +446 -0
  15. package/client/live-feed.tsx +151 -0
  16. package/client/live-header.tsx +310 -0
  17. package/client/live-issues.tsx +345 -0
  18. package/client/live-map.tsx +351 -0
  19. package/client/live-panel.tsx +392 -0
  20. package/client/live-timeline.ts +379 -0
  21. package/client/pill.tsx +394 -0
  22. package/client/plugin-icon.tsx +75 -0
  23. package/client/project-init.tsx +192 -0
  24. package/client/project-knowledge.tsx +87 -0
  25. package/client/settings-access.tsx +56 -0
  26. package/client/settings-agents.tsx +113 -0
  27. package/client/settings-fields.tsx +11 -3
  28. package/client/settings-icon.tsx +277 -0
  29. package/client/settings-live.tsx +128 -0
  30. package/client/settings-overrides.tsx +148 -0
  31. package/client/settings-projects.tsx +26 -4
  32. package/client/settings-prompts.tsx +3 -3
  33. package/client/timeline-card.tsx +2 -1
  34. package/client/tool-buttons.tsx +68 -0
  35. package/client/ui.tsx +16 -7
  36. package/client/web.ts +81 -0
  37. package/index.client.tsx +160 -51
  38. package/index.server.ts +32 -5
  39. package/package.json +1 -1
  40. package/server/agent-hooks.ts +37 -0
  41. package/server/agent-runs.ts +312 -0
  42. package/server/agent-tools.ts +276 -0
  43. package/server/handlers.ts +12 -1
  44. package/server/init.ts +89 -0
  45. package/server/inspect.ts +392 -0
  46. package/server/knowledge.ts +157 -0
  47. package/server/live.ts +371 -0
  48. package/server/mcp-http.ts +156 -0
  49. package/server/stores.ts +126 -0
  50. package/server/subagent-logs.ts +146 -0
  51. package/server/sync.ts +133 -0
  52. package/shared/activity.ts +268 -0
  53. package/shared/agent-hooks.ts +68 -0
  54. package/shared/agent-tools.ts +157 -0
  55. package/shared/custom-icon.ts +188 -0
  56. package/shared/knowledge.ts +255 -0
  57. package/shared/live.ts +248 -0
  58. package/shared/map-model.ts +240 -0
  59. package/shared/project-setup.ts +56 -0
  60. package/shared/prompts.ts +109 -2
  61. package/shared/settings.ts +145 -3
  62. package/shared/shell-activity.ts +309 -0
  63. package/shared/shell-lexer.ts +180 -0
  64. package/shared/subagents.ts +106 -0
  65. package/shared/todo-sync.ts +158 -0
@@ -0,0 +1,151 @@
1
+ import { Icon } from "@getpaseo/plugin/client/react-native";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { ActivityIndicator, Animated, StyleSheet, Text, View } from "react-native";
4
+ import type { ActivityEvent, EventKind } from "../shared/activity";
5
+ import { MONO } from "./live-issues";
6
+ import { NATIVE_DRIVER } from "./live-timeline";
7
+ import type { Theme } from "./ui";
8
+
9
+ export const KIND_ICONS: Record<EventKind, string> = {
10
+ read: "Eye",
11
+ edit: "Pencil",
12
+ write: "FilePlus",
13
+ search: "Search",
14
+ list: "FolderOpen",
15
+ shell: "SquareTerminal",
16
+ agent: "Bot",
17
+ tool: "Wrench",
18
+ };
19
+ const FEED_SIZE = 14;
20
+
21
+ export function eventFailed(event: ActivityEvent): boolean {
22
+ return event.status === "failed" || (event.exitCode !== null && event.exitCode !== 0);
23
+ }
24
+
25
+ /** The color of an event's icon: changes in green, failures in red, the rest muted. */
26
+ export function eventTone(theme: Theme, event: ActivityEvent): string {
27
+ const { colors } = theme;
28
+ if (eventFailed(event) && event.kind !== "search") return colors.statusDanger;
29
+ if (event.kind === "edit" || event.kind === "write") return colors.statusSuccess;
30
+ if (event.kind === "read" || event.kind === "search") return colors.foreground;
31
+ return colors.foregroundMuted;
32
+ }
33
+
34
+ function FeedRow(props: { theme: Theme; event: ActivityEvent; animate: boolean }) {
35
+ const { theme, event } = props;
36
+ const { colors } = theme;
37
+ const [entering] = useState(props.animate);
38
+ const [progress] = useState(() => new Animated.Value(entering ? 0 : 1));
39
+ useEffect(() => {
40
+ if (!entering) return;
41
+ const animation = Animated.timing(progress, {
42
+ toValue: 1,
43
+ duration: 280,
44
+ useNativeDriver: NATIVE_DRIVER,
45
+ });
46
+ animation.start();
47
+ return () => animation.stop();
48
+ }, [entering, progress]);
49
+ const failed = eventFailed(event);
50
+ const tone = eventTone(theme, event);
51
+ const styles = useMemo(() => {
52
+ const slide = progress.interpolate({ inputRange: [0, 1], outputRange: [-8, 0] });
53
+ return {
54
+ row: {
55
+ flexDirection: "row",
56
+ alignItems: "center",
57
+ gap: 10,
58
+ paddingVertical: 6,
59
+ paddingHorizontal: 8,
60
+ borderRadius: 8,
61
+ opacity: progress,
62
+ transform: [{ translateY: slide }],
63
+ },
64
+ icon: {
65
+ width: 24,
66
+ height: 24,
67
+ borderRadius: 7,
68
+ alignItems: "center",
69
+ justifyContent: "center",
70
+ backgroundColor: colors.surface2,
71
+ },
72
+ tint: { ...StyleSheet.absoluteFillObject, borderRadius: 7, backgroundColor: tone, opacity: 0.12 },
73
+ body: { flex: 1, minWidth: 0, gap: 2 },
74
+ text: { color: colors.foreground, fontSize: 13 },
75
+ detail: { color: colors.foregroundMuted, fontFamily: MONO, fontSize: 11 },
76
+ added: { color: colors.statusSuccess, fontSize: 11 },
77
+ removed: { color: colors.statusDanger, fontSize: 11 },
78
+ exit: { color: failed ? colors.statusDanger : colors.foregroundMuted, fontSize: 11 },
79
+ } as const;
80
+ }, [colors, progress, failed, tone]);
81
+ const showExit = event.exitCode !== null && event.exitCode !== 0 && event.kind !== "search";
82
+ return (
83
+ <Animated.View style={styles.row} accessible accessibilityLabel={event.text}>
84
+ <View style={styles.icon}>
85
+ <View style={styles.tint} />
86
+ <Icon name={KIND_ICONS[event.kind]} size={13} color={tone} />
87
+ </View>
88
+ <View style={styles.body}>
89
+ <Text numberOfLines={1} style={styles.text}>
90
+ {event.text}
91
+ </Text>
92
+ {event.detail ? (
93
+ <Text numberOfLines={1} style={styles.detail}>
94
+ {event.detail}
95
+ </Text>
96
+ ) : null}
97
+ </View>
98
+ {event.added > 0 ? <Text style={styles.added}>{`+${event.added}`}</Text> : null}
99
+ {event.removed > 0 ? <Text style={styles.removed}>{`-${event.removed}`}</Text> : null}
100
+ {showExit ? <Text style={styles.exit}>{`exit ${event.exitCode}`}</Text> : null}
101
+ {event.status === "running" ? (
102
+ <ActivityIndicator size="small" color={colors.foregroundMuted} />
103
+ ) : null}
104
+ {event.status === "failed" && event.exitCode === null ? (
105
+ <Icon name="TriangleAlert" size={14} color={colors.statusDanger} />
106
+ ) : null}
107
+ </Animated.View>
108
+ );
109
+ }
110
+
111
+ export function ActivityFeed(props: {
112
+ theme: Theme;
113
+ events: readonly ActivityEvent[];
114
+ loading: boolean;
115
+ error: string | null;
116
+ reduceMotion: boolean;
117
+ }) {
118
+ const { theme } = props;
119
+ const { colors } = theme;
120
+ const rows = props.events.slice(0, FEED_SIZE);
121
+ // The loaded tail renders in place; only rows that arrive after it slide in.
122
+ const [settled, setSettled] = useState(false);
123
+ useEffect(() => {
124
+ if (!props.loading) setSettled(true);
125
+ }, [props.loading]);
126
+ const styles = useMemo(
127
+ () =>
128
+ ({
129
+ list: { gap: 2 },
130
+ note: { color: colors.foregroundMuted, fontSize: 13, padding: 8 },
131
+ error: { color: colors.statusDanger, fontSize: 13, padding: 8 },
132
+ }) as const,
133
+ [colors],
134
+ );
135
+ let empty: string | null = null;
136
+ if (rows.length === 0) empty = props.loading ? "Loading activity" : "No tool calls yet";
137
+ return (
138
+ <View style={styles.list}>
139
+ {props.error ? <Text style={styles.error}>{props.error}</Text> : null}
140
+ {empty ? <Text style={styles.note}>{empty}</Text> : null}
141
+ {rows.map((event) => (
142
+ <FeedRow
143
+ key={event.id}
144
+ theme={theme}
145
+ event={event}
146
+ animate={settled && !props.reduceMotion}
147
+ />
148
+ ))}
149
+ </View>
150
+ );
151
+ }
@@ -0,0 +1,310 @@
1
+ import type { PluginAgentSnapshot } from "@getpaseo/plugin";
2
+ import { Icon } from "@getpaseo/plugin/client/react-native";
3
+ import { useCallback, useEffect, useMemo, useState } from "react";
4
+ import { ActivityIndicator, Animated, StyleSheet, Text, View } from "react-native";
5
+ import type { ActivityEvent } from "../shared/activity";
6
+ import type { IssueDetail } from "../shared/linear";
7
+ import type { MappingMode } from "../shared/settings";
8
+ import { todoStatus } from "../shared/todo-sync";
9
+ import { eventTone, KIND_ICONS } from "./live-feed";
10
+ import { type IssueProgress, MONO, ProgressBar } from "./live-issues";
11
+ import { type LiveMap, NATIVE_DRIVER, type OwnerColors } from "./live-timeline";
12
+ import { ProviderIcon } from "./provider-icon";
13
+ import { ToolButton, ToolGroup } from "./tool-buttons";
14
+ import { Button, StateIcon, type Theme } from "./ui";
15
+
16
+ type AgentStatus = PluginAgentSnapshot["status"];
17
+
18
+ const STATUS_COPY: Record<AgentStatus, string> = {
19
+ initializing: "Starting",
20
+ idle: "Idle",
21
+ running: "Working",
22
+ error: "Error",
23
+ closed: "Closed",
24
+ };
25
+
26
+ function providerName(provider: string): string {
27
+ return provider ? provider.charAt(0).toUpperCase() + provider.slice(1) : "Agent";
28
+ }
29
+
30
+ function StatusChip(props: { theme: Theme; status: AgentStatus; provider: string; reduce: boolean }) {
31
+ const { theme, status } = props;
32
+ const { colors } = theme;
33
+ const running = status === "running" || status === "initializing";
34
+ const [glow] = useState(() => new Animated.Value(1));
35
+ useEffect(() => {
36
+ if (!running || props.reduce) return;
37
+ const loop = Animated.loop(
38
+ Animated.sequence([
39
+ Animated.timing(glow, { toValue: 0.35, duration: 700, useNativeDriver: NATIVE_DRIVER }),
40
+ Animated.timing(glow, { toValue: 1, duration: 700, useNativeDriver: NATIVE_DRIVER }),
41
+ ]),
42
+ );
43
+ loop.start();
44
+ return () => {
45
+ loop.stop();
46
+ glow.setValue(1);
47
+ };
48
+ }, [running, props.reduce, glow]);
49
+ let tone = colors.foregroundMuted;
50
+ if (running) tone = colors.statusSuccess;
51
+ else if (status === "error") tone = colors.statusDanger;
52
+ const styles = useMemo(
53
+ () =>
54
+ ({
55
+ chip: {
56
+ flexDirection: "row",
57
+ alignItems: "center",
58
+ gap: 7,
59
+ height: 26,
60
+ paddingHorizontal: 10,
61
+ borderRadius: 13,
62
+ borderWidth: 1,
63
+ borderColor: colors.border,
64
+ backgroundColor: colors.surface1,
65
+ },
66
+ dot: { width: 7, height: 7, borderRadius: 4, backgroundColor: tone, opacity: glow },
67
+ text: { color: colors.foreground, fontSize: 12, fontWeight: "500" },
68
+ muted: { color: colors.foregroundMuted, fontSize: 12 },
69
+ }) as const,
70
+ [colors, tone, glow],
71
+ );
72
+ return (
73
+ <View style={styles.chip}>
74
+ <Animated.View style={styles.dot} />
75
+ <Text style={styles.text}>{STATUS_COPY[status] ?? status}</Text>
76
+ <ProviderIcon provider={props.provider} size={12} color={colors.foregroundMuted} />
77
+ <Text style={styles.muted}>{providerName(props.provider)}</Text>
78
+ </View>
79
+ );
80
+ }
81
+
82
+ function NowLine(props: { theme: Theme; event: ActivityEvent; live: boolean; offMap: string | null }) {
83
+ const { theme, event } = props;
84
+ const { colors } = theme;
85
+ const tone = eventTone(theme, event);
86
+ const styles = useMemo(
87
+ () =>
88
+ ({
89
+ row: { flexDirection: "row", alignItems: "center", gap: 10, minWidth: 0 },
90
+ label: {
91
+ color: props.live ? colors.foreground : colors.foregroundMuted,
92
+ fontSize: 10,
93
+ fontWeight: "700",
94
+ letterSpacing: 0.6,
95
+ textTransform: "uppercase",
96
+ width: 34,
97
+ },
98
+ icon: {
99
+ width: 22,
100
+ height: 22,
101
+ borderRadius: 6,
102
+ alignItems: "center",
103
+ justifyContent: "center",
104
+ backgroundColor: colors.surface2,
105
+ },
106
+ tint: { ...StyleSheet.absoluteFillObject, borderRadius: 6, backgroundColor: tone, opacity: 0.14 },
107
+ text: { color: props.live ? colors.foreground : colors.foregroundMuted, fontSize: 13 },
108
+ detail: { flex: 1, color: colors.foregroundMuted, fontFamily: MONO, fontSize: 11 },
109
+ off: {
110
+ flexShrink: 0,
111
+ maxWidth: 220,
112
+ paddingHorizontal: 7,
113
+ paddingVertical: 2,
114
+ borderRadius: 6,
115
+ borderWidth: 1,
116
+ borderStyle: "dashed",
117
+ borderColor: colors.statusWarning,
118
+ color: colors.statusWarning,
119
+ fontSize: 11,
120
+ },
121
+ }) as const,
122
+ [colors, tone, props.live],
123
+ );
124
+ return (
125
+ <View style={styles.row} accessible accessibilityLabel={`${props.live ? "Now" : "Last"}: ${event.text}`}>
126
+ <Text style={styles.label}>{props.live ? "Now" : "Last"}</Text>
127
+ <View style={styles.icon}>
128
+ <View style={styles.tint} />
129
+ <Icon name={KIND_ICONS[event.kind]} size={12} color={tone} />
130
+ </View>
131
+ <Text numberOfLines={1} style={styles.text}>
132
+ {event.text}
133
+ </Text>
134
+ {event.detail ? (
135
+ <Text numberOfLines={1} style={styles.detail}>
136
+ {event.detail}
137
+ </Text>
138
+ ) : null}
139
+ {props.offMap ? (
140
+ <Text numberOfLines={1} style={styles.off} accessibilityLabel={`Off the map, in ${props.offMap}`}>
141
+ {`Off the map · ${props.offMap}`}
142
+ </Text>
143
+ ) : null}
144
+ </View>
145
+ );
146
+ }
147
+
148
+ export function LiveHeader(props: {
149
+ theme: Theme;
150
+ issue: IssueDetail;
151
+ progress: IssueProgress;
152
+ owners: OwnerColors;
153
+ status: AgentStatus;
154
+ provider: string;
155
+ current: ActivityEvent | null;
156
+ /** Where the current file is when the agent works off the map. */
157
+ offMap: string | null;
158
+ reduceMotion: boolean;
159
+ }) {
160
+ const { theme, issue, progress, owners } = props;
161
+ const { colors } = theme;
162
+ const rows = issue.children.length > 0 ? issue.children : [issue];
163
+ const segments = rows.map((row) => {
164
+ const key = row.identifier.toUpperCase();
165
+ const todos = progress.groups.get(key) ?? [];
166
+ const done = todos.filter((todo) => todoStatus(todo) === "completed").length;
167
+ const finished = row.state.type === "completed";
168
+ return {
169
+ key,
170
+ color: owners.get(key) ?? colors.accent,
171
+ value: finished ? 1 : todos.length > 0 ? done / todos.length : 0,
172
+ finished,
173
+ };
174
+ });
175
+ const allTodos = [...progress.groups.values()].flat();
176
+ const todosDone = allTodos.filter((todo) => todoStatus(todo) === "completed").length;
177
+ const issuesDone = segments.filter((segment) => segment.finished).length;
178
+ const counts = [
179
+ issue.children.length > 0 ? `${issuesDone} of ${issue.children.length} sub-issues done` : null,
180
+ allTodos.length > 0 ? `${todosDone} of ${allTodos.length} todos` : null,
181
+ ].filter(Boolean);
182
+ const styles = useMemo(
183
+ () =>
184
+ ({
185
+ root: { gap: 12 },
186
+ top: { flexDirection: "row", alignItems: "center", gap: 10, minWidth: 0 },
187
+ key: { color: colors.foregroundMuted, fontFamily: MONO, fontSize: 12 },
188
+ title: { flex: 1, color: colors.foreground, fontSize: 16, fontWeight: "600" },
189
+ strip: { flexDirection: "row", gap: 4 },
190
+ segment: { flex: 1 },
191
+ counts: { color: colors.foregroundMuted, fontSize: 12 },
192
+ }) as const,
193
+ [colors],
194
+ );
195
+ return (
196
+ <View style={styles.root}>
197
+ <View style={styles.top}>
198
+ <StateIcon state={issue.state} size={16} />
199
+ <Text style={styles.key}>{issue.identifier}</Text>
200
+ <Text numberOfLines={1} style={styles.title}>
201
+ {issue.title}
202
+ </Text>
203
+ <StatusChip
204
+ theme={theme}
205
+ status={props.status}
206
+ provider={props.provider}
207
+ reduce={props.reduceMotion}
208
+ />
209
+ </View>
210
+ <View style={styles.strip}>
211
+ {segments.map((segment) => (
212
+ <View key={segment.key} style={styles.segment}>
213
+ <ProgressBar
214
+ theme={theme}
215
+ value={segment.value}
216
+ color={segment.color}
217
+ reduce={props.reduceMotion}
218
+ height={5}
219
+ tinted
220
+ />
221
+ </View>
222
+ ))}
223
+ </View>
224
+ {counts.length > 0 ? <Text style={styles.counts}>{counts.join(" · ")}</Text> : null}
225
+ {props.current ? (
226
+ <NowLine
227
+ theme={theme}
228
+ event={props.current}
229
+ live={props.status === "running"}
230
+ offMap={props.offMap}
231
+ />
232
+ ) : null}
233
+ </View>
234
+ );
235
+ }
236
+
237
+ /** Where the map came from, and the explore job's state, for the map card header. */
238
+ export function MapSource(props: { theme: Theme; live: LiveMap; mapping: MappingMode }) {
239
+ const { theme, live } = props;
240
+ const { colors } = theme;
241
+ const { explore } = live;
242
+ const styles = useMemo(
243
+ () =>
244
+ ({
245
+ row: { flexDirection: "row", alignItems: "center", gap: 8, flexWrap: "wrap" },
246
+ note: { color: colors.foregroundMuted, fontSize: 12 },
247
+ error: { color: colors.statusDanger, fontSize: 12, flexShrink: 1 },
248
+ }) as const,
249
+ [colors],
250
+ );
251
+ const start = useCallback(() => explore(false), [explore]);
252
+ const retry = useCallback(() => explore(true), [explore]);
253
+ const job = live.job;
254
+ const running = job?.status === "running";
255
+ const semantic = live.map.source === "semantic";
256
+ const canExplore =
257
+ props.mapping === "explore" && !live.stored && !running && job?.status !== "failed";
258
+ return (
259
+ <View style={styles.row}>
260
+ <Text style={styles.note}>{semantic ? "From ticket text" : "From explore agent"}</Text>
261
+ {running ? <ActivityIndicator size="small" color={colors.foregroundMuted} /> : null}
262
+ {running ? <Text style={styles.note}>Exploring…</Text> : null}
263
+ {job?.status === "failed" ? (
264
+ <>
265
+ <Text style={styles.error}>{job.error ?? "Explore failed"}</Text>
266
+ <Button theme={theme} size="xs" icon="RefreshCw" label="Retry" busy={live.starting} onPress={retry} />
267
+ </>
268
+ ) : null}
269
+ {canExplore ? (
270
+ <Button
271
+ theme={theme}
272
+ size="xs"
273
+ icon="Sparkles"
274
+ label="Map with explore agent"
275
+ busy={live.starting}
276
+ onPress={start}
277
+ />
278
+ ) : null}
279
+ {live.error ? <Text style={styles.error}>{live.error}</Text> : null}
280
+ </View>
281
+ );
282
+ }
283
+
284
+ /** Read the file data again, rebuild the map with the explore agent, or clear the saved map. */
285
+ export function MapActions(props: { theme: Theme; live: LiveMap }) {
286
+ const { theme, live } = props;
287
+ const { explore, refresh, clear } = live;
288
+ const running = live.job?.status === "running";
289
+ const rebuild = useCallback(() => explore(true), [explore]);
290
+ return (
291
+ <ToolGroup theme={theme}>
292
+ <ToolButton theme={theme} icon="RefreshCw" label="Read the files again" onPress={refresh} />
293
+ <ToolButton
294
+ theme={theme}
295
+ icon="Sparkles"
296
+ label={live.stored ? "Rebuild the map with the explore agent" : "Build the map with the explore agent"}
297
+ busy={live.starting || running}
298
+ onPress={rebuild}
299
+ />
300
+ <ToolButton
301
+ theme={theme}
302
+ icon="Eraser"
303
+ label="Clear the saved map and use the ticket text"
304
+ disabled={!live.stored || running}
305
+ busy={live.clearing}
306
+ onPress={clear}
307
+ />
308
+ </ToolGroup>
309
+ );
310
+ }