@yannelli/paseo-linear-plugin 1.1.0-beta.1 → 1.1.0-beta.3
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/OVERVIEW.md +10 -2
- package/README.md +34 -9
- package/client/access.tsx +2 -1
- package/client/compat.ts +23 -5
- package/client/connect.tsx +3 -2
- package/client/live-graph.tsx +6 -3
- package/client/live-header.tsx +26 -2
- package/client/live-map.tsx +7 -1
- package/client/live-panel.tsx +9 -2
- package/client/live-timeline.ts +36 -5
- package/client/pill.tsx +4 -3
- package/client/plugin-icon.tsx +43 -0
- package/client/project-init.tsx +28 -4
- package/client/project-knowledge.tsx +87 -0
- package/client/settings-fields.tsx +11 -3
- package/client/settings-icon.tsx +228 -0
- package/client/settings-projects.tsx +16 -5
- package/client/timeline-card.tsx +2 -1
- package/client/ui.tsx +16 -7
- package/client/web.ts +46 -0
- package/index.client.tsx +9 -0
- package/index.server.ts +5 -2
- package/package.json +1 -1
- package/server/init.ts +31 -7
- package/server/inspect.ts +392 -0
- package/server/knowledge.ts +157 -0
- package/server/live.ts +21 -3
- package/server/stores.ts +1 -1
- package/shared/custom-icon.ts +148 -0
- package/shared/knowledge.ts +255 -0
- package/shared/live.ts +19 -3
- package/shared/map-model.ts +50 -2
- package/shared/project-setup.ts +3 -2
- package/shared/settings.ts +3 -0
package/shared/live.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineRpc } from "@getpaseo/plugin";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { McpHttpServerSchema, ToolPolicySchema } from "./agent-tools";
|
|
4
|
+
import { type AreaRef, areasIn } from "./knowledge";
|
|
4
5
|
import { IssueCardSchema, WorkflowStateSchema } from "./linear";
|
|
5
6
|
|
|
6
7
|
// Contracts for Linear Live, todo sync, and project setup. Agents the plugin starts for itself
|
|
@@ -40,6 +41,9 @@ export const InitProposalSchema = z.object({
|
|
|
40
41
|
steps: z.array(z.string()).default([]),
|
|
41
42
|
implement: z.string().default(""),
|
|
42
43
|
review: z.string().default(""),
|
|
44
|
+
/** Saved to the project knowledge at once; not part of the prompt proposal. */
|
|
45
|
+
summary: z.string().default(""),
|
|
46
|
+
areas: z.array(z.object({ path: z.string(), summary: z.string() })).max(80).default([]),
|
|
43
47
|
});
|
|
44
48
|
export type InitProposal = z.infer<typeof InitProposalSchema>;
|
|
45
49
|
|
|
@@ -194,18 +198,30 @@ export interface SemanticIssue {
|
|
|
194
198
|
children: readonly { identifier: string; title: string }[];
|
|
195
199
|
}
|
|
196
200
|
|
|
201
|
+
/** What project knowledge adds to a ticket-text map. */
|
|
202
|
+
export interface MapHints {
|
|
203
|
+
/** Ticket paths matched to real files, such as "linear.ts" to "server/linear.ts". */
|
|
204
|
+
resolved?: ReadonlyMap<string, string>;
|
|
205
|
+
/** A service or package the text names puts its folder on the map. */
|
|
206
|
+
areas?: readonly AreaRef[];
|
|
207
|
+
}
|
|
208
|
+
|
|
197
209
|
// Linear sends sub-issue titles but not their descriptions, so a path in the parent's text
|
|
198
210
|
// belongs to the sub-issue its line names and otherwise to the parent.
|
|
199
|
-
export function semanticMap(issue: SemanticIssue, now = new Date()): IssueMap {
|
|
211
|
+
export function semanticMap(issue: SemanticIssue, now = new Date(), hints: MapHints = {}): IssueMap {
|
|
200
212
|
const owners = new Map<string, string>();
|
|
201
213
|
const childKeys = new Set(issue.children.map((child) => child.identifier.toUpperCase()));
|
|
214
|
+
const found = (text: string) => [
|
|
215
|
+
...pathsIn(text).map((path) => hints.resolved?.get(path) ?? path),
|
|
216
|
+
...areasIn(text, hints.areas ?? []).map((area) => area.path),
|
|
217
|
+
];
|
|
202
218
|
for (const child of issue.children) {
|
|
203
|
-
for (const path of
|
|
219
|
+
for (const path of found(child.title)) owners.set(path, child.identifier);
|
|
204
220
|
}
|
|
205
221
|
const lines = `${issue.title}\n${issue.description ?? ""}`.split("\n");
|
|
206
222
|
for (const line of lines) {
|
|
207
223
|
const named = (line.match(ISSUE_KEY) ?? []).find((key) => childKeys.has(key));
|
|
208
|
-
for (const path of
|
|
224
|
+
for (const path of found(line)) {
|
|
209
225
|
if (!owners.has(path)) owners.set(path, named ?? issue.identifier);
|
|
210
226
|
}
|
|
211
227
|
}
|
package/shared/map-model.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DirTouch, FileTouch } from "./activity";
|
|
2
|
+
import { type AreaRef, areaLabel, areaOf, whereLabel } from "./knowledge";
|
|
2
3
|
import { dirOf, type MapFile } from "./live";
|
|
3
4
|
|
|
4
5
|
// The Linear Live map: predicted files and touched files grouped into folder zones, and the
|
|
@@ -31,12 +32,21 @@ export interface MapZone {
|
|
|
31
32
|
/** Touched files that are on the map. */
|
|
32
33
|
hits: number;
|
|
33
34
|
explored: ZoneExplored;
|
|
35
|
+
/** The service, package, or main folder the zone is in, from the project knowledge. */
|
|
36
|
+
area: AreaRef | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Touched files the map did not predict, grouped by where they are. */
|
|
40
|
+
export interface OffMapGroup {
|
|
41
|
+
where: string;
|
|
42
|
+
files: number;
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
export interface MapModel {
|
|
37
46
|
zones: MapZone[];
|
|
38
47
|
touched: number;
|
|
39
48
|
hits: number;
|
|
49
|
+
offMap: OffMapGroup[];
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
// Search hits alone do not count: one broad search would flood the map with off-map tiles.
|
|
@@ -98,6 +108,7 @@ export function buildMapModel(
|
|
|
98
108
|
touches: readonly FileTouch[],
|
|
99
109
|
listing: ReadonlyMap<string, readonly string[]>,
|
|
100
110
|
dirs: readonly DirTouch[] = [],
|
|
111
|
+
areas: readonly AreaRef[] = [],
|
|
101
112
|
): MapModel {
|
|
102
113
|
const predicted = predictions(files);
|
|
103
114
|
const touched = new Map(touches.filter(isTouched).map((file) => [file.path, file]));
|
|
@@ -134,11 +145,19 @@ export function buildMapModel(
|
|
|
134
145
|
touched: tiles.filter((tile) => tile.touch !== "none").length,
|
|
135
146
|
hits: tiles.filter((tile) => tile.touch !== "none" && tile.onMap).length,
|
|
136
147
|
explored: exploredState(dir, dirs),
|
|
148
|
+
area: areaOf(dir, areas),
|
|
137
149
|
};
|
|
138
150
|
});
|
|
139
151
|
let hits = 0;
|
|
140
|
-
|
|
141
|
-
|
|
152
|
+
const off = new Map<string, number>();
|
|
153
|
+
for (const path of touched.keys()) {
|
|
154
|
+
if (ownerOf(path, predicted)) hits += 1;
|
|
155
|
+
else off.set(whereLabel(path, areas), (off.get(whereLabel(path, areas)) ?? 0) + 1);
|
|
156
|
+
}
|
|
157
|
+
const offMap = [...off]
|
|
158
|
+
.map(([where, count]) => ({ where, files: count }))
|
|
159
|
+
.sort((a, b) => b.files - a.files || a.where.localeCompare(b.where));
|
|
160
|
+
return { zones, touched: touched.size, hits, offMap };
|
|
142
161
|
}
|
|
143
162
|
|
|
144
163
|
export function hitRateLine(model: MapModel): string {
|
|
@@ -147,6 +166,35 @@ export function hitRateLine(model: MapModel): string {
|
|
|
147
166
|
return `${model.touched} ${noun} touched, ${model.hits} on the map`;
|
|
148
167
|
}
|
|
149
168
|
|
|
169
|
+
const OFF_MAP_SHOWN = 4;
|
|
170
|
+
|
|
171
|
+
/** Such as "Off the map: billing-api · service (3), docs (1)"; null when all are on it. */
|
|
172
|
+
export function offMapLine(model: MapModel): string | null {
|
|
173
|
+
if (model.offMap.length === 0) return null;
|
|
174
|
+
const shown = model.offMap.slice(0, OFF_MAP_SHOWN).map((group) => `${group.where} (${group.files})`);
|
|
175
|
+
const more = model.offMap.length - OFF_MAP_SHOWN;
|
|
176
|
+
return `Off the map: ${shown.join(", ")}${more > 0 ? `, ${more} more` : ""}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Where the file is when the agent touched it off the map; null on the map or untouched. */
|
|
180
|
+
export function offMapWhere(model: MapModel, path: string | null, areas: readonly AreaRef[]): string | null {
|
|
181
|
+
if (!path) return null;
|
|
182
|
+
for (const zone of model.zones) {
|
|
183
|
+
const tile = zone.tiles.find((entry) => entry.path === path && !entry.ghost);
|
|
184
|
+
if (tile) return tile.touch !== "none" && !tile.onMap ? whereLabel(path, areas) : null;
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The area a zone header names; only the part its folder name does not already say. */
|
|
190
|
+
export function zoneAreaLabel(zone: MapZone): string | null {
|
|
191
|
+
const area = zone.area;
|
|
192
|
+
if (!area || area.path === "") return null;
|
|
193
|
+
const folder = area.path.replace(/\/$/, "").split("/").pop();
|
|
194
|
+
if (zone.dir !== area.path || area.name !== folder) return areaLabel(area);
|
|
195
|
+
return area.role || (area.kind === "folder" ? null : area.kind);
|
|
196
|
+
}
|
|
197
|
+
|
|
150
198
|
export const ZONE_GAP = 10;
|
|
151
199
|
|
|
152
200
|
// Rough zone height from its tiles, so zones can go to the shortest column. Tiles wrap, so
|
package/shared/project-setup.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ProjectConfig } from "./settings";
|
|
|
3
3
|
|
|
4
4
|
// A setup proposal shows only fields that change; accepted fields append to the prompts.
|
|
5
5
|
export type ProposalField = "instructions" | "steps" | "implement" | "review";
|
|
6
|
+
type PromptProposal = Pick<InitProposal, ProposalField>;
|
|
6
7
|
|
|
7
8
|
export interface FieldDiff {
|
|
8
9
|
field: ProposalField;
|
|
@@ -11,7 +12,7 @@ export interface FieldDiff {
|
|
|
11
12
|
proposed: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
export function proposalDiffs(config: ProjectConfig, proposal:
|
|
15
|
+
export function proposalDiffs(config: ProjectConfig, proposal: PromptProposal): FieldDiff[] {
|
|
15
16
|
const steps = proposal.steps.map((step) => step.trim()).filter(Boolean);
|
|
16
17
|
const rows: FieldDiff[] = [
|
|
17
18
|
{
|
|
@@ -43,7 +44,7 @@ export function proposalDiffs(config: ProjectConfig, proposal: InitProposal): Fi
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export function applyProposal(
|
|
46
|
-
proposal:
|
|
47
|
+
proposal: PromptProposal,
|
|
47
48
|
fields: ReadonlySet<ProposalField>,
|
|
48
49
|
): Partial<ProjectConfig> {
|
|
49
50
|
const patch: Partial<ProjectConfig> = {};
|
package/shared/settings.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineRpc, defineSettings } from "@getpaseo/plugin";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { IconSettingsSchema } from "./custom-icon";
|
|
3
4
|
|
|
4
5
|
export const AGENT_ACTIONS = ["implement", "review"] as const;
|
|
5
6
|
export type AgentAction = (typeof AGENT_ACTIONS)[number];
|
|
@@ -101,6 +102,8 @@ export const linearSettings = defineSettings({
|
|
|
101
102
|
})
|
|
102
103
|
.prefault({}),
|
|
103
104
|
projects: z.array(ProjectConfigSchema).default([]),
|
|
105
|
+
/** The icon of pills and other plugin places: built-in or the user's SVG. */
|
|
106
|
+
icon: IconSettingsSchema.prefault({}),
|
|
104
107
|
}),
|
|
105
108
|
});
|
|
106
109
|
|