@yannelli/paseo-linear-plugin 1.0.0 → 1.1.0-beta.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 (47) hide show
  1. package/OVERVIEW.md +19 -1
  2. package/README.md +77 -6
  3. package/client/access.tsx +66 -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/graph-labels.ts +137 -0
  8. package/client/launch-choices.ts +18 -5
  9. package/client/launch-plan.ts +88 -39
  10. package/client/launch.tsx +9 -4
  11. package/client/live-agents.tsx +407 -0
  12. package/client/live-cursor.tsx +291 -0
  13. package/client/live-feed.tsx +151 -0
  14. package/client/live-graph.tsx +434 -0
  15. package/client/live-header.tsx +312 -0
  16. package/client/live-issues.tsx +345 -0
  17. package/client/live-map.tsx +345 -0
  18. package/client/live-panel.tsx +444 -0
  19. package/client/live-timeline.ts +341 -0
  20. package/client/pill.tsx +393 -0
  21. package/client/project-init.tsx +168 -0
  22. package/client/settings-access.tsx +56 -0
  23. package/client/settings-live.tsx +121 -0
  24. package/client/settings-projects.tsx +9 -0
  25. package/index.client.tsx +39 -1
  26. package/index.server.ts +24 -4
  27. package/package.json +1 -1
  28. package/server/agent-runs.ts +299 -0
  29. package/server/handlers.ts +12 -1
  30. package/server/init.ts +64 -0
  31. package/server/links.ts +153 -0
  32. package/server/live.ts +334 -0
  33. package/server/stores.ts +119 -0
  34. package/server/sync.ts +127 -0
  35. package/shared/activity.ts +308 -0
  36. package/shared/graph-geometry.ts +122 -0
  37. package/shared/graph-model.ts +319 -0
  38. package/shared/links.ts +252 -0
  39. package/shared/live.ts +200 -0
  40. package/shared/map-model.ts +192 -0
  41. package/shared/project-setup.ts +55 -0
  42. package/shared/prompts.ts +16 -1
  43. package/shared/settings.ts +57 -1
  44. package/shared/shell-activity.ts +309 -0
  45. package/shared/shell-lexer.ts +180 -0
  46. package/shared/subagents.ts +106 -0
  47. package/shared/todo-sync.ts +158 -0
@@ -0,0 +1,444 @@
1
+ import type { PluginAgentSnapshot } from "@getpaseo/plugin";
2
+ import { type PluginAgentPanelProps, useAgent, useSettings } from "@getpaseo/plugin/client";
3
+ import { ScrollView } from "@getpaseo/plugin/client/react-native";
4
+ import { type ReactNode, useCallback, useMemo, useRef, useState } from "react";
5
+ import { type LayoutChangeEvent, Text, View } from "react-native";
6
+ import { deriveActivity } from "../shared/activity";
7
+ import type { IssueDetail } from "../shared/linear";
8
+ import { AGENT_LABELS } from "../shared/prompts";
9
+ import { type LinearSettings, type LiveView, linearSettings, type MappingMode } from "../shared/settings";
10
+ import { KeyScopeProvider } from "./key-scope";
11
+ import { buildGraph, graphFiles } from "../shared/graph-model";
12
+ import { AgentsList, cursorAgents, useLiveAgents } from "./live-agents";
13
+ import { ActivityFeed } from "./live-feed";
14
+ import { LiveGraph } from "./live-graph";
15
+ import { LiveHeader, MapSource, ViewToggle } from "./live-header";
16
+ import { type FocusKey, IssueRail, issueProgress, SubIssues } from "./live-issues";
17
+ import { MapLegend, RepoMap } from "./live-map";
18
+ import {
19
+ ownerColors,
20
+ useAgentTimeline,
21
+ useFileLinks,
22
+ useLiveMap,
23
+ useMapModel,
24
+ useReduceMotion,
25
+ } from "./live-timeline";
26
+ import { hitRateLine } from "../shared/map-model";
27
+ import { useIssue } from "./queries";
28
+ import { Button, EmptyState, errorMessage, IconButton, type Theme } from "./ui";
29
+
30
+ type AgentStatus = PluginAgentSnapshot["status"];
31
+
32
+ interface AgentInfo {
33
+ identifier: string | null;
34
+ project: string | null;
35
+ cwd: string;
36
+ status: AgentStatus;
37
+ provider: string;
38
+ }
39
+
40
+ const selectAgent = (agent: PluginAgentSnapshot): AgentInfo => ({
41
+ identifier: agent.labels[AGENT_LABELS.issue] ?? null,
42
+ project: agent.labels[AGENT_LABELS.project] ?? null,
43
+ cwd: agent.cwd,
44
+ status: agent.status,
45
+ provider: agent.provider,
46
+ });
47
+
48
+ type LivePatch = Partial<Pick<LinearSettings["live"], "view" | "issuesCollapsed">>;
49
+
50
+ const STACK_WIDTH = 760;
51
+ const COLUMN_WIDTH = 330;
52
+
53
+ export function LivePanel(props: PluginAgentPanelProps) {
54
+ const { theme, agentId } = props;
55
+ const settings = useSettings(linearSettings);
56
+ const agent = useAgent(agentId, selectAgent);
57
+ const live = settings.status === "ready" ? settings.values.live : null;
58
+ // The panel switches at once and each save carries every choice made here, so two
59
+ // choices made in a row do not undo each other. A failed save keeps them for this session.
60
+ const latest = useRef(settings);
61
+ latest.current = settings;
62
+ const choices = useRef<LivePatch>({});
63
+ const saveLive = useCallback((patch: LivePatch) => {
64
+ const current = latest.current;
65
+ if (current.status !== "ready") return;
66
+ choices.current = { ...choices.current, ...patch };
67
+ const values = { ...current.values, live: { ...current.values.live, ...choices.current } };
68
+ void current.save(values, current.revision);
69
+ }, []);
70
+ if (settings.status === "loading") {
71
+ return <EmptyState theme={theme} icon="Radar" title="Loading Linear Live" />;
72
+ }
73
+ if (live && !live.enabled) {
74
+ return (
75
+ <EmptyState
76
+ theme={theme}
77
+ icon="Radar"
78
+ title="Linear Live is off"
79
+ detail="Turn it on in Linear settings, under Live."
80
+ />
81
+ );
82
+ }
83
+ if (!agent) return <EmptyState theme={theme} icon="Radar" title="Loading agent" />;
84
+ if (!agent.identifier) {
85
+ return (
86
+ <EmptyState
87
+ theme={theme}
88
+ icon="Radar"
89
+ title="This agent is not linked to a Linear issue"
90
+ detail="Agents started from a Linear issue show here."
91
+ />
92
+ );
93
+ }
94
+ return (
95
+ <KeyScopeProvider projectId={agent.project}>
96
+ <LiveBody
97
+ key={agentId}
98
+ theme={theme}
99
+ compact={props.layout.compact}
100
+ agentId={agentId}
101
+ agent={agent}
102
+ identifier={agent.identifier}
103
+ mapping={live?.mapping ?? "semantic"}
104
+ view={live?.view ?? "map"}
105
+ issuesCollapsed={live?.issuesCollapsed ?? false}
106
+ saveLive={saveLive}
107
+ openAgent={props.navigation ? (id) => props.navigation?.openAgent({ agentId: id }) : undefined}
108
+ />
109
+ </KeyScopeProvider>
110
+ );
111
+ }
112
+
113
+ interface BodyProps {
114
+ theme: Theme;
115
+ compact: boolean;
116
+ agentId: string;
117
+ agent: AgentInfo;
118
+ identifier: string;
119
+ mapping: MappingMode;
120
+ view: LiveView;
121
+ issuesCollapsed: boolean;
122
+ saveLive(patch: LivePatch): void;
123
+ openAgent?: (agentId: string) => void;
124
+ }
125
+
126
+ function LiveBody(props: BodyProps) {
127
+ const { theme } = props;
128
+ const issue = useIssue(props.identifier);
129
+ const { refetch } = issue;
130
+ const retry = useCallback(() => void refetch(), [refetch]);
131
+ if (issue.isPending) return <EmptyState theme={theme} icon="Loader" title="Loading issue" />;
132
+ if (issue.isError) {
133
+ return (
134
+ <EmptyState
135
+ theme={theme}
136
+ icon="TriangleAlert"
137
+ tone="danger"
138
+ title="Could not load the issue"
139
+ detail={errorMessage(issue.error)}
140
+ >
141
+ <Button theme={theme} label="Try again" icon="RefreshCw" onPress={retry} />
142
+ </EmptyState>
143
+ );
144
+ }
145
+ return <LiveView {...props} issue={issue.data} />;
146
+ }
147
+
148
+ function Card(props: {
149
+ theme: Theme;
150
+ title: string;
151
+ meta?: ReactNode;
152
+ right?: ReactNode;
153
+ fill?: boolean;
154
+ children?: ReactNode;
155
+ }) {
156
+ const { colors } = props.theme;
157
+ const styles = useMemo(
158
+ () =>
159
+ ({
160
+ card: {
161
+ borderWidth: 1,
162
+ borderColor: colors.border,
163
+ borderRadius: 12,
164
+ backgroundColor: colors.surface0,
165
+ overflow: "hidden",
166
+ ...(props.fill ? { flex: 1, minHeight: 0 } : {}),
167
+ },
168
+ head: {
169
+ flexDirection: "row",
170
+ alignItems: "center",
171
+ flexWrap: "wrap",
172
+ gap: 10,
173
+ paddingHorizontal: 14,
174
+ paddingVertical: 10,
175
+ borderBottomWidth: props.children ? 1 : 0,
176
+ borderBottomColor: colors.border,
177
+ },
178
+ title: { color: colors.foreground, fontSize: 13, fontWeight: "600" },
179
+ right: { marginLeft: "auto" },
180
+ body: { padding: 12, ...(props.fill ? { flex: 1, minHeight: 0 } : {}) },
181
+ }) as const,
182
+ [colors, props.fill, props.children],
183
+ );
184
+ return (
185
+ <View style={styles.card}>
186
+ <View style={styles.head}>
187
+ <Text style={styles.title}>{props.title}</Text>
188
+ {props.meta}
189
+ {props.right ? <View style={styles.right}>{props.right}</View> : null}
190
+ </View>
191
+ {props.children ? <View style={styles.body}>{props.children}</View> : null}
192
+ </View>
193
+ );
194
+ }
195
+
196
+ function LiveView(props: BodyProps & { issue: IssueDetail }) {
197
+ const { theme, issue, agent, agentId } = props;
198
+ const { colors } = theme;
199
+ const timeline = useAgentTimeline(agentId);
200
+ const activity = useMemo(
201
+ () => deriveActivity(timeline.items, agent.cwd),
202
+ [timeline.items, agent.cwd],
203
+ );
204
+ const liveMap = useLiveMap(agentId, props.identifier, issue, props.mapping);
205
+ const agents = useLiveAgents({
206
+ theme,
207
+ agentId,
208
+ cwd: agent.cwd,
209
+ status: agent.status,
210
+ provider: agent.provider,
211
+ activity,
212
+ });
213
+ const model = useMapModel(agentId, liveMap.map.files, agents.files, agents.dirs);
214
+ // Hold the map until its first full data, so it does not draw once and then swap.
215
+ const mapShown = useRef(false);
216
+ if (liveMap.ready && model.ready) mapShown.current = true;
217
+ const [view, setView] = useState<LiveView>(props.view);
218
+ const [collapsed, setCollapsed] = useState(props.issuesCollapsed);
219
+ const { saveLive } = props;
220
+ const chooseView = useCallback(
221
+ (next: LiveView) => {
222
+ setView(next);
223
+ saveLive({ view: next });
224
+ },
225
+ [saveLive],
226
+ );
227
+ const toggleIssues = useCallback(() => {
228
+ setCollapsed(!collapsed);
229
+ saveLive({ issuesCollapsed: !collapsed });
230
+ }, [collapsed, saveLive]);
231
+ const working = agent.status === "running";
232
+ const graphInput = useMemo(
233
+ () => ({
234
+ issue: issue.identifier,
235
+ children: issue.children.map((child) => child.identifier),
236
+ tiles: model.zones.flatMap((zone) => zone.tiles),
237
+ }),
238
+ [issue, model.zones],
239
+ );
240
+ const graphPaths = useMemo(
241
+ () => (view === "graph" ? graphFiles(graphInput).tiles.map((tile) => tile.path) : []),
242
+ [view, graphInput],
243
+ );
244
+ // Each edit can add or drop an import, so the links are read again.
245
+ const edits = graphInput.tiles.reduce((sum, tile) => sum + tile.added + tile.removed, 0);
246
+ const links = useFileLinks(agentId, graphPaths, edits, view === "graph");
247
+ const graph = useMemo(
248
+ () => (view === "graph" ? buildGraph({ ...graphInput, links: links.links }) : null),
249
+ [view, graphInput, links.links],
250
+ );
251
+ const cursors = useMemo(
252
+ () => cursorAgents({ theme, provider: agent.provider, working, activity, children: agents.children }),
253
+ [theme, agent.provider, working, activity, agents.children],
254
+ );
255
+ const owners = useMemo(() => ownerColors(issue, colors.accent), [issue, colors.accent]);
256
+ const progress = useMemo(() => issueProgress(issue, activity.todos), [issue, activity.todos]);
257
+ const reduceMotion = useReduceMotion();
258
+ const [focus, setFocus] = useState<FocusKey>(null);
259
+ const toggleFocus = useCallback(
260
+ (key: string) => setFocus((current) => (current === key ? null : key)),
261
+ [],
262
+ );
263
+ const [width, setWidth] = useState(0);
264
+ const measure = useCallback((event: LayoutChangeEvent) => {
265
+ setWidth(event.nativeEvent.layout.width);
266
+ }, []);
267
+ const stacked = props.compact || (width > 0 && width < STACK_WIDTH);
268
+ const styles = useMemo(
269
+ () =>
270
+ ({
271
+ root: { flex: 1, minHeight: 0, backgroundColor: colors.surface0 },
272
+ scroll: { flex: 1 },
273
+ stack: { padding: 12, gap: 12, paddingBottom: 48 },
274
+ header: {
275
+ paddingHorizontal: 20,
276
+ paddingVertical: 16,
277
+ borderBottomWidth: 1,
278
+ borderBottomColor: colors.border,
279
+ },
280
+ split: { flex: 1, minHeight: 0, flexDirection: "row", gap: 16, padding: 16 },
281
+ // Not `flex: 0`: on web it sets flex-basis 0, which overrides the width.
282
+ column: { flexGrow: 0, flexShrink: 0, width: COLUMN_WIDTH, minHeight: 0, gap: 16 },
283
+ main: { flex: 1, minWidth: 0 },
284
+ mainContent: { gap: 16, paddingBottom: 32 },
285
+ hit: { color: colors.foregroundMuted, fontSize: 12 },
286
+ tools: { flexDirection: "row", alignItems: "center", gap: 12 },
287
+ mapHead: { gap: 8 },
288
+ mapWait: { minHeight: 240, alignItems: "center", justifyContent: "center" },
289
+ }) as const,
290
+ [colors],
291
+ );
292
+
293
+ const header = (
294
+ <LiveHeader
295
+ theme={theme}
296
+ issue={issue}
297
+ progress={progress}
298
+ owners={owners}
299
+ status={agent.status}
300
+ provider={agent.provider}
301
+ current={activity.current}
302
+ reduceMotion={reduceMotion}
303
+ />
304
+ );
305
+ const issues = (
306
+ <SubIssues
307
+ theme={theme}
308
+ issue={issue}
309
+ progress={progress}
310
+ owners={owners}
311
+ focus={focus}
312
+ onFocus={toggleFocus}
313
+ reduceMotion={reduceMotion}
314
+ />
315
+ );
316
+ const toggle = <ViewToggle theme={theme} value={view} onChange={chooseView} />;
317
+ const map = (
318
+ <Card
319
+ theme={theme}
320
+ title={view === "graph" ? "Graph" : "Repository map"}
321
+ meta={mapShown.current ? <MapSource theme={theme} live={liveMap} mapping={props.mapping} /> : null}
322
+ right={
323
+ <View style={styles.tools}>
324
+ {stacked || view === "graph" ? null : <MapLegend theme={theme} />}
325
+ {toggle}
326
+ </View>
327
+ }
328
+ >
329
+ {!mapShown.current ? (
330
+ <View style={styles.mapWait}>
331
+ <Text style={styles.hit}>Loading map…</Text>
332
+ </View>
333
+ ) : (
334
+ <View style={styles.mapHead}>
335
+ <Text style={styles.hit}>{hitRateLine(model)}</Text>
336
+ {graph ? (
337
+ <LiveGraph
338
+ theme={theme}
339
+ compact={stacked}
340
+ full={collapsed && !stacked}
341
+ graph={graph}
342
+ ready={links.ready}
343
+ issue={issue}
344
+ progress={progress}
345
+ owners={owners}
346
+ agents={cursors}
347
+ focus={focus}
348
+ onFocus={toggleFocus}
349
+ reduceMotion={reduceMotion}
350
+ />
351
+ ) : (
352
+ <RepoMap
353
+ theme={theme}
354
+ compact={stacked}
355
+ model={model}
356
+ colors={owners}
357
+ focus={focus}
358
+ markers={agents.markers}
359
+ reduceMotion={reduceMotion}
360
+ />
361
+ )}
362
+ </View>
363
+ )}
364
+ </Card>
365
+ );
366
+ const feed = (
367
+ <Card theme={theme} title="Activity">
368
+ <ActivityFeed
369
+ theme={theme}
370
+ events={activity.events}
371
+ loading={timeline.loading}
372
+ error={timeline.error}
373
+ reduceMotion={reduceMotion}
374
+ />
375
+ </Card>
376
+ );
377
+ const helpers = agents.children.length + activity.subagents.length;
378
+ const agentsCard =
379
+ helpers > 0 ? (
380
+ <Card theme={theme} title="Agents" meta={<Text style={styles.hit}>{`${helpers + 1}`}</Text>}>
381
+ <AgentsList
382
+ theme={theme}
383
+ provider={agent.provider}
384
+ status={agent.status}
385
+ current={activity.current}
386
+ childAgents={agents.children}
387
+ subagents={activity.subagents}
388
+ onOpenAgent={props.openAgent}
389
+ />
390
+ </Card>
391
+ ) : null;
392
+ const collapse = (
393
+ <IconButton
394
+ theme={theme}
395
+ icon={stacked ? (collapsed ? "ChevronDown" : "ChevronUp") : "PanelLeftClose"}
396
+ label={collapsed ? "Show issues" : "Hide issues"}
397
+ onPress={toggleIssues}
398
+ />
399
+ );
400
+ if (stacked) {
401
+ return (
402
+ <View style={styles.root} onLayout={measure}>
403
+ <ScrollView style={styles.scroll} contentContainerStyle={styles.stack}>
404
+ {header}
405
+ <Card theme={theme} title="Issue" right={collapse}>
406
+ {collapsed ? null : issues}
407
+ </Card>
408
+ {agentsCard}
409
+ {map}
410
+ {feed}
411
+ </ScrollView>
412
+ </View>
413
+ );
414
+ }
415
+ return (
416
+ <View style={styles.root} onLayout={measure}>
417
+ <View style={styles.header}>{header}</View>
418
+ <View style={styles.split}>
419
+ {collapsed ? (
420
+ <IssueRail
421
+ theme={theme}
422
+ issue={issue}
423
+ owners={owners}
424
+ focus={focus}
425
+ onFocus={toggleFocus}
426
+ onExpand={toggleIssues}
427
+ />
428
+ ) : (
429
+ <View style={styles.column}>
430
+ <Card theme={theme} title="Issue" right={collapse} fill>
431
+ <ScrollView style={styles.scroll}>{issues}</ScrollView>
432
+ </Card>
433
+ {agentsCard}
434
+ </View>
435
+ )}
436
+ <ScrollView style={styles.main} contentContainerStyle={styles.mainContent}>
437
+ {map}
438
+ {collapsed ? agentsCard : null}
439
+ {feed}
440
+ </ScrollView>
441
+ </View>
442
+ </View>
443
+ );
444
+ }