@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,106 @@
1
+ import { pathsIn } from "./live";
2
+
3
+ // Subagents that run inside the provider (Claude's Agent tool, Codex sub-agents) report only
4
+ // a type, a description, and while the launch streams, the prompt. Their own file reads never
5
+ // reach the parent timeline, so the files their prompt names are the best clue to their place.
6
+
7
+ export type SubagentStatus = "running" | "completed" | "failed" | "canceled";
8
+
9
+ export interface SubagentRun {
10
+ /** Type and description, the same for the launch call and the subagent item. */
11
+ key: string;
12
+ type: string;
13
+ description: string;
14
+ status: SubagentStatus;
15
+ /** Repo-relative files and folders its prompt or description names. */
16
+ targets: string[];
17
+ order: number;
18
+ }
19
+
20
+ export interface SubagentSignal {
21
+ key: string;
22
+ type: string;
23
+ description: string;
24
+ /** Null for the launch call: it completes as soon as the subagent starts in the background. */
25
+ status: SubagentStatus | null;
26
+ targets: string[];
27
+ }
28
+
29
+ type ToolItem = Readonly<Record<string, unknown>>;
30
+
31
+ const LAUNCHERS = new Set(["Agent", "Task"]);
32
+
33
+ function text(value: unknown): string {
34
+ return typeof value === "string" ? value : "";
35
+ }
36
+
37
+ function statusOf(value: unknown): SubagentStatus {
38
+ return value === "failed" || value === "canceled" || value === "completed" ? value : "running";
39
+ }
40
+
41
+ /** A call that starts, reports on, or waits for a subagent. */
42
+ export function isSubagentCall(item: ToolItem): boolean {
43
+ const detail = (item.detail ?? {}) as Record<string, unknown>;
44
+ return detail.type === "sub_agent" || (LAUNCHERS.has(text(item.name)) && detail.type === "unknown");
45
+ }
46
+
47
+ /** The subagent a tool call starts or reports on, or null for any other call or a wait. */
48
+ export function subagentOf(
49
+ item: ToolItem,
50
+ resolve: (path: string) => string | null,
51
+ ): SubagentSignal | null {
52
+ const detail = (item.detail ?? {}) as Record<string, unknown>;
53
+ let type: string;
54
+ let description: string;
55
+ let prompt = "";
56
+ let status: SubagentStatus | null;
57
+ if (detail.type === "sub_agent") {
58
+ type = text(detail.subAgentType);
59
+ description = text(detail.description);
60
+ status = statusOf(item.status);
61
+ const actions = Array.isArray(detail.actions) ? detail.actions : [];
62
+ prompt = actions.map((action) => text((action as { summary?: unknown }).summary)).join("\n");
63
+ } else if (LAUNCHERS.has(text(item.name)) && detail.type === "unknown") {
64
+ const input = (detail.input ?? {}) as Record<string, unknown>;
65
+ type = text(input.subagent_type);
66
+ description = text(input.description);
67
+ prompt = text(input.prompt);
68
+ status = null;
69
+ } else {
70
+ return null;
71
+ }
72
+ if (!description && !type) return null;
73
+ // Waiting for background agents shows as one more Agent call; it starts nothing.
74
+ if (/^wait(ing)? (for|on)\b/i.test(description)) return null;
75
+ const named = pathsIn(`${description}\n${prompt}`)
76
+ .map(resolve)
77
+ .filter((path): path is string => path !== null && path !== "");
78
+ return {
79
+ key: `${type}\u0000${description}`,
80
+ type: type || "Subagent",
81
+ description,
82
+ status,
83
+ targets: [...new Set(named)],
84
+ };
85
+ }
86
+
87
+ /** Folds launch and report calls into one run per subagent, in launch order. */
88
+ export function mergeSubagent(
89
+ runs: Map<string, SubagentRun>,
90
+ signal: SubagentSignal,
91
+ order: number,
92
+ ): SubagentRun {
93
+ const known = runs.get(signal.key);
94
+ const run: SubagentRun = known ?? {
95
+ key: signal.key,
96
+ type: signal.type,
97
+ description: signal.description,
98
+ status: "running",
99
+ targets: [],
100
+ order,
101
+ };
102
+ if (signal.status) run.status = signal.status;
103
+ run.targets = [...new Set([...run.targets, ...signal.targets])];
104
+ runs.set(signal.key, run);
105
+ return run;
106
+ }
@@ -0,0 +1,158 @@
1
+ import type { WorkflowState } from "./linear";
2
+
3
+ // Claude and Codex todos share one timeline shape. Codex ids are list positions, so todos
4
+ // map to issues by a key prefix in their text, such as "ENG-124: add the queue".
5
+
6
+ export type TodoStatus = "pending" | "in_progress" | "completed";
7
+
8
+ export interface TodoLike {
9
+ text: string;
10
+ completed: boolean;
11
+ status?: TodoStatus;
12
+ }
13
+
14
+ export interface KeyProgress {
15
+ key: string;
16
+ total: number;
17
+ completed: number;
18
+ inProgress: number;
19
+ }
20
+
21
+ const KEY_PREFIX =
22
+ /^\s*(?:[-*]\s+)?(?:\[([A-Za-z][A-Za-z0-9]*-\d+)\]\s*:?|([A-Za-z][A-Za-z0-9]*-\d+)\s*(?::|-\s))/;
23
+
24
+ export function todoStatus(item: TodoLike): TodoStatus {
25
+ return item.status ?? (item.completed ? "completed" : "pending");
26
+ }
27
+
28
+ export function todoKey(text: string): string | null {
29
+ const match = KEY_PREFIX.exec(text);
30
+ const key = match?.[1] ?? match?.[2];
31
+ return key ? key.toUpperCase() : null;
32
+ }
33
+
34
+ /** Strips the key prefix so a todo reads as plain text under its issue. */
35
+ export function todoLabel(text: string): string {
36
+ const match = KEY_PREFIX.exec(text);
37
+ return match ? text.slice(match[0].length).trim() || text : text;
38
+ }
39
+
40
+ export function progressByKey(items: readonly TodoLike[]): Map<string, KeyProgress> {
41
+ const progress = new Map<string, KeyProgress>();
42
+ for (const item of items) {
43
+ const key = todoKey(item.text);
44
+ if (!key) continue;
45
+ const entry = progress.get(key) ?? { key, total: 0, completed: 0, inProgress: 0 };
46
+ const status = todoStatus(item);
47
+ entry.total += 1;
48
+ if (status === "completed") entry.completed += 1;
49
+ if (status === "in_progress") entry.inProgress += 1;
50
+ progress.set(key, entry);
51
+ }
52
+ return progress;
53
+ }
54
+
55
+ export interface SyncIssue {
56
+ id: string;
57
+ identifier: string;
58
+ state: WorkflowState;
59
+ }
60
+
61
+ export interface SyncInput {
62
+ parent: SyncIssue & { children: readonly SyncIssue[] };
63
+ progress: ReadonlyMap<string, KeyProgress>;
64
+ /** Workflow states of the parent's team. */
65
+ states: readonly WorkflowState[];
66
+ }
67
+
68
+ export interface StatusMove {
69
+ issueId: string;
70
+ identifier: string;
71
+ from: WorkflowState;
72
+ to: WorkflowState;
73
+ }
74
+
75
+ const RANK: Record<string, number> = {
76
+ triage: 0,
77
+ backlog: 0,
78
+ unstarted: 1,
79
+ started: 2,
80
+ completed: 3,
81
+ };
82
+
83
+ function rank(state: WorkflowState): number | null {
84
+ return RANK[state.type] ?? null;
85
+ }
86
+
87
+ const byPosition = (a: WorkflowState, b: WorkflowState) => a.position - b.position;
88
+
89
+ function firstOfType(states: readonly WorkflowState[], type: string, exclude?: RegExp) {
90
+ return (
91
+ states
92
+ .filter((state) => state.type === type && !(exclude && exclude.test(state.name)))
93
+ .sort(byPosition)[0] ?? null
94
+ );
95
+ }
96
+
97
+ const REVIEW = /review/i;
98
+
99
+ export function reviewState(states: readonly WorkflowState[]): WorkflowState | null {
100
+ return states.filter((s) => s.type === "started" && REVIEW.test(s.name)).sort(byPosition)[0] ?? null;
101
+ }
102
+
103
+ function targetFor(progress: KeyProgress | undefined): "started" | "completed" | null {
104
+ if (!progress || progress.total === 0) return null;
105
+ if (progress.completed === progress.total) return "completed";
106
+ return progress.completed > 0 || progress.inProgress > 0 ? "started" : null;
107
+ }
108
+
109
+ // Forward-only moves for the parent and its direct children; a todo naming any other key is
110
+ // ignored. Canceled issues never move, and the parent stops at a review state, never Done.
111
+ export function planStatusMoves(input: SyncInput): StatusMove[] {
112
+ const { parent, progress, states } = input;
113
+ const known = new Set(states.map((state) => state.id));
114
+ const started = firstOfType(states, "started", REVIEW) ?? firstOfType(states, "started");
115
+ const completed = firstOfType(states, "completed");
116
+ const moves: StatusMove[] = [];
117
+ const effective = new Map<string, WorkflowState>();
118
+
119
+ for (const child of parent.children) {
120
+ effective.set(child.id, child.state);
121
+ const target = targetFor(progress.get(child.identifier.toUpperCase()));
122
+ const to = target === "completed" ? completed : target === "started" ? started : null;
123
+ const current = rank(child.state);
124
+ if (!to || current === null || !known.has(child.state.id)) continue;
125
+ if ((rank(to) ?? 0) <= current) continue;
126
+ moves.push({ issueId: child.id, identifier: child.identifier, from: child.state, to });
127
+ effective.set(child.id, to);
128
+ }
129
+
130
+ const parentRank = rank(parent.state);
131
+ if (parentRank === null || !known.has(parent.state.id)) return moves;
132
+ const anyWork =
133
+ moves.length > 0 ||
134
+ targetFor(progress.get(parent.identifier.toUpperCase())) !== null ||
135
+ parent.children.some((child) => targetFor(progress.get(child.identifier.toUpperCase())));
136
+ const allDone =
137
+ parent.children.length > 0 &&
138
+ parent.children.every((child) => effective.get(child.id)?.type === "completed");
139
+ const review = reviewState(states);
140
+ if (allDone && review && parentRank <= RANK.started && parent.state.id !== review.id) {
141
+ if (!(parent.state.type === "started" && REVIEW.test(parent.state.name))) {
142
+ moves.push({ issueId: parent.id, identifier: parent.identifier, from: parent.state, to: review });
143
+ }
144
+ } else if (anyWork && started && parentRank < RANK.started) {
145
+ moves.push({ issueId: parent.id, identifier: parent.identifier, from: parent.state, to: started });
146
+ }
147
+ return moves;
148
+ }
149
+
150
+ /** The prompt line that makes todos map to sub-issues. */
151
+ export function todoConventionLine(children: readonly { identifier: string }[]): string | null {
152
+ const first = children[0];
153
+ if (!first) return null;
154
+ return [
155
+ "Track your work with one or more todos per sub-issue.",
156
+ `Start each todo with its sub-issue key, for example "${first.identifier}: <task>".`,
157
+ ].join(" ");
158
+ }