@yannelli/paseo-linear-plugin 1.0.1 → 1.1.0-beta.1

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