@yannelli/paseo-linear-plugin 1.1.0-beta.2 → 1.1.0-beta.4
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 +8 -4
- package/README.md +24 -9
- package/client/graph-camera.tsx +28 -8
- package/client/graph-labels.ts +7 -4
- package/client/live-cursor.tsx +76 -49
- package/client/live-graph.tsx +59 -29
- 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/plugin-icon.tsx +41 -9
- package/client/project-init.tsx +28 -4
- package/client/project-knowledge.tsx +87 -0
- package/client/settings-icon.tsx +57 -8
- package/client/settings-projects.tsx +16 -5
- package/client/web.ts +32 -0
- package/index.client.tsx +134 -79
- 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 +40 -0
- package/shared/graph-camera.ts +28 -8
- package/shared/graph-geometry.ts +30 -19
- 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
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { useRpc } from "@getpaseo/plugin/client";
|
|
2
|
+
import {
|
|
3
|
+
SettingsAction,
|
|
4
|
+
SettingsCard,
|
|
5
|
+
SettingsRow,
|
|
6
|
+
SettingsSection,
|
|
7
|
+
} from "@getpaseo/plugin/client/ui";
|
|
8
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
9
|
+
import {
|
|
10
|
+
areaLabel,
|
|
11
|
+
knowledgeGetRpc,
|
|
12
|
+
knowledgeInspectRpc,
|
|
13
|
+
MAX_KNOWLEDGE_FILES,
|
|
14
|
+
type ProjectBrief,
|
|
15
|
+
} from "../shared/knowledge";
|
|
16
|
+
import { errorMessage, relativeTime } from "./ui";
|
|
17
|
+
|
|
18
|
+
const AREAS_SHOWN = 24;
|
|
19
|
+
|
|
20
|
+
export const knowledgeKey = (projectId: string) => ["linear", "knowledge", projectId] as const;
|
|
21
|
+
|
|
22
|
+
function countText(entries: readonly { name: string; files: number }[]): string {
|
|
23
|
+
return entries.map((entry) => `${entry.name} ${entry.files}`).join(", ");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function overview(brief: ProjectBrief): string {
|
|
27
|
+
const source = brief.source === "git" ? "files git tracks" : "files found in the folder";
|
|
28
|
+
const truncated = brief.truncated ? `; the first ${MAX_KNOWLEDGE_FILES.toLocaleString()} are kept` : "";
|
|
29
|
+
return `${brief.fileCount} ${source}${truncated}. Inspected ${relativeTime(brief.inspectedAt)}.`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** What the daemon knows about the project's files, and a button to inspect it again. */
|
|
33
|
+
export function ProjectKnowledgeSection(props: { projectId: string }) {
|
|
34
|
+
const { projectId } = props;
|
|
35
|
+
const get = useRpc(knowledgeGetRpc);
|
|
36
|
+
const inspect = useRpc(knowledgeInspectRpc);
|
|
37
|
+
const queries = useQueryClient();
|
|
38
|
+
const query = useQuery({
|
|
39
|
+
queryKey: knowledgeKey(projectId),
|
|
40
|
+
queryFn: () => get({ projectId }),
|
|
41
|
+
staleTime: 60_000,
|
|
42
|
+
retry: false,
|
|
43
|
+
});
|
|
44
|
+
const again = useMutation({
|
|
45
|
+
mutationFn: () => inspect({ projectId }),
|
|
46
|
+
onSuccess: (data) => queries.setQueryData(knowledgeKey(projectId), data),
|
|
47
|
+
});
|
|
48
|
+
const brief = query.data?.knowledge ?? null;
|
|
49
|
+
const busy = query.isPending || again.isPending;
|
|
50
|
+
const failure = again.error ?? query.error;
|
|
51
|
+
const areas = brief?.areas.filter((area) => area.path !== "") ?? [];
|
|
52
|
+
return (
|
|
53
|
+
<SettingsSection title="Project knowledge">
|
|
54
|
+
<SettingsCard>
|
|
55
|
+
<SettingsAction
|
|
56
|
+
label="Inspect the project"
|
|
57
|
+
hint="The daemon lists the project's files and reads its manifests to find its services, packages, and main folders. No agent runs. Explore and setup agents, ticket-text maps, and Linear Live use what it finds. It inspects again when the knowledge is 6 hours old."
|
|
58
|
+
actionLabel={busy ? "Inspecting…" : "Inspect again"}
|
|
59
|
+
disabled={busy}
|
|
60
|
+
onPress={() => again.mutate()}
|
|
61
|
+
/>
|
|
62
|
+
{failure ? <SettingsRow label="Inspection did not finish" error={errorMessage(failure)} /> : null}
|
|
63
|
+
{brief ? (
|
|
64
|
+
<>
|
|
65
|
+
<SettingsRow label="Files" hint={overview(brief)} />
|
|
66
|
+
{brief.summary ? <SettingsRow label="Project" hint={brief.summary} /> : null}
|
|
67
|
+
{brief.languages.length > 0 ? (
|
|
68
|
+
<SettingsRow label="Languages" hint={countText(brief.languages)} />
|
|
69
|
+
) : null}
|
|
70
|
+
{brief.commands.length > 0 ? (
|
|
71
|
+
<SettingsRow label="Commands" hint={brief.commands.join(", ")} />
|
|
72
|
+
) : null}
|
|
73
|
+
{brief.guides.length > 0 ? <SettingsRow label="Guides" hint={brief.guides.join(", ")} /> : null}
|
|
74
|
+
{areas.slice(0, AREAS_SHOWN).map((area) => {
|
|
75
|
+
const files = `${area.files} ${area.files === 1 ? "file" : "files"}`;
|
|
76
|
+
const parts = [areaLabel(area), files, area.languages.join("/"), area.summary];
|
|
77
|
+
return <SettingsRow key={area.path} label={area.path} hint={parts.filter(Boolean).join(" · ")} />;
|
|
78
|
+
})}
|
|
79
|
+
{areas.length > AREAS_SHOWN ? (
|
|
80
|
+
<SettingsRow label={`${areas.length - AREAS_SHOWN} more areas`} />
|
|
81
|
+
) : null}
|
|
82
|
+
</>
|
|
83
|
+
) : null}
|
|
84
|
+
</SettingsCard>
|
|
85
|
+
</SettingsSection>
|
|
86
|
+
);
|
|
87
|
+
}
|
package/client/settings-icon.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
SettingsSelect,
|
|
9
9
|
SettingsSwitch,
|
|
10
10
|
} from "@getpaseo/plugin/client/ui";
|
|
11
|
+
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
11
12
|
import { useCallback, useMemo, useState } from "react";
|
|
12
13
|
import { Platform, Text, View } from "react-native";
|
|
13
14
|
import {
|
|
@@ -15,10 +16,11 @@ import {
|
|
|
15
16
|
type IconPaint,
|
|
16
17
|
type IconSettings,
|
|
17
18
|
MAX_SVG_LENGTH,
|
|
19
|
+
MENU_ICONS,
|
|
18
20
|
parseIconSvg,
|
|
19
21
|
} from "../shared/custom-icon";
|
|
20
22
|
import { linearSettings } from "../shared/settings";
|
|
21
|
-
import { IconGlyph } from "./plugin-icon";
|
|
23
|
+
import { IconGlyph, useIconSettings } from "./plugin-icon";
|
|
22
24
|
import { MultilineField, type ReadySettings, SaveBar, SettingsGate, useSettingsDraft } from "./settings-fields";
|
|
23
25
|
import type { Theme } from "./ui";
|
|
24
26
|
import { canPickFiles, pickTextFile } from "./web";
|
|
@@ -37,6 +39,7 @@ const MAX_FILE_BYTES = MAX_SVG_LENGTH * 4;
|
|
|
37
39
|
|
|
38
40
|
export function IconSettingsScreen({ theme }: PluginSurfaceProps) {
|
|
39
41
|
const settings = useSettings(linearSettings);
|
|
42
|
+
useIconSettings();
|
|
40
43
|
const render = useCallback(
|
|
41
44
|
(ready: ReadySettings) => <IconEditor theme={theme} settings={ready} />,
|
|
42
45
|
[theme],
|
|
@@ -85,6 +88,33 @@ function IconPreview({ theme, icon }: { theme: Theme; icon: IconSettings }) {
|
|
|
85
88
|
);
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
function MenuPreview({ theme, name }: { theme: Theme; name: string }) {
|
|
92
|
+
const { colors } = theme;
|
|
93
|
+
const styles = useMemo(
|
|
94
|
+
() => ({
|
|
95
|
+
tab: {
|
|
96
|
+
flexDirection: "row" as const,
|
|
97
|
+
alignSelf: "flex-start" as const,
|
|
98
|
+
alignItems: "center" as const,
|
|
99
|
+
gap: 6,
|
|
100
|
+
marginTop: 8,
|
|
101
|
+
paddingHorizontal: 10,
|
|
102
|
+
paddingVertical: 5,
|
|
103
|
+
borderRadius: 6,
|
|
104
|
+
backgroundColor: colors.surface2,
|
|
105
|
+
},
|
|
106
|
+
label: { color: colors.foreground, fontSize: 12 },
|
|
107
|
+
}),
|
|
108
|
+
[colors],
|
|
109
|
+
);
|
|
110
|
+
return (
|
|
111
|
+
<View style={styles.tab}>
|
|
112
|
+
<Icon name={name} size={14} color={colors.foregroundMuted} />
|
|
113
|
+
<Text style={styles.label}>Linear</Text>
|
|
114
|
+
</View>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
88
118
|
function IconEditor({ theme, settings }: { theme: Theme; settings: ReadySettings }) {
|
|
89
119
|
const { values, dirty, edit, save, discard } = useSettingsDraft(settings);
|
|
90
120
|
const icon = values.icon;
|
|
@@ -123,7 +153,7 @@ function IconEditor({ theme, settings }: { theme: Theme; settings: ReadySettings
|
|
|
123
153
|
}),
|
|
124
154
|
[applyMarkup],
|
|
125
155
|
);
|
|
126
|
-
const
|
|
156
|
+
const removeSvg = useCallback(() => {
|
|
127
157
|
setFileName(null);
|
|
128
158
|
applyMarkup("");
|
|
129
159
|
}, [applyMarkup]);
|
|
@@ -145,6 +175,11 @@ function IconEditor({ theme, settings }: { theme: Theme; settings: ReadySettings
|
|
|
145
175
|
}, [discard, settings.values.icon.svg]);
|
|
146
176
|
const native = Platform.OS !== "web";
|
|
147
177
|
const custom = icon.svg !== "";
|
|
178
|
+
const menuOptions = useMemo(() => {
|
|
179
|
+
const options = MENU_ICONS.map((entry) => ({ label: entry.label, value: entry.name }));
|
|
180
|
+
if (!options.some((option) => option.value === icon.menu)) options.push({ label: icon.menu, value: icon.menu });
|
|
181
|
+
return options;
|
|
182
|
+
}, [icon.menu]);
|
|
148
183
|
|
|
149
184
|
return (
|
|
150
185
|
<View>
|
|
@@ -155,7 +190,7 @@ function IconEditor({ theme, settings }: { theme: Theme; settings: ReadySettings
|
|
|
155
190
|
hint={
|
|
156
191
|
native
|
|
157
192
|
? "The iOS and Android apps cannot draw plugin SVG images, so they show the built-in icon in the color you choose."
|
|
158
|
-
: "Composer pills, the Linear sidebar row, and Linear screens use this icon.
|
|
193
|
+
: "Composer pills, the Linear sidebar row, and Linear screens use this icon. Panel tabs and menus use the built-in icon below."
|
|
159
194
|
}
|
|
160
195
|
>
|
|
161
196
|
<IconPreview theme={theme} icon={icon} />
|
|
@@ -179,11 +214,11 @@ function IconEditor({ theme, settings }: { theme: Theme; settings: ReadySettings
|
|
|
179
214
|
onChange={applyMarkup}
|
|
180
215
|
/>
|
|
181
216
|
<SettingsAction
|
|
182
|
-
label="
|
|
183
|
-
hint="
|
|
184
|
-
actionLabel="
|
|
217
|
+
label="Remove SVG"
|
|
218
|
+
hint="Use the built-in icon everywhere."
|
|
219
|
+
actionLabel="Remove"
|
|
185
220
|
disabled={!custom && markup === ""}
|
|
186
|
-
onPress={
|
|
221
|
+
onPress={removeSvg}
|
|
187
222
|
/>
|
|
188
223
|
</SettingsCard>
|
|
189
224
|
</SettingsSection>
|
|
@@ -208,13 +243,27 @@ function IconEditor({ theme, settings }: { theme: Theme; settings: ReadySettings
|
|
|
208
243
|
) : null}
|
|
209
244
|
<SettingsSwitch
|
|
210
245
|
label="Solid"
|
|
211
|
-
hint={custom ? "Fill shapes that only have an outline, so line icons show as solid shapes." : "Add
|
|
246
|
+
hint={custom ? "Fill shapes that only have an outline, so line icons show as solid shapes." : "Add an SVG to use this. Built-in icons stay as drawn."}
|
|
212
247
|
value={icon.solid}
|
|
213
248
|
disabled={!custom}
|
|
214
249
|
onValueChange={(solid) => change({ solid })}
|
|
215
250
|
/>
|
|
216
251
|
</SettingsCard>
|
|
217
252
|
</SettingsSection>
|
|
253
|
+
<SettingsSection title="Built-in icon">
|
|
254
|
+
<SettingsCard>
|
|
255
|
+
<SettingsSelect
|
|
256
|
+
label="Built-in icon"
|
|
257
|
+
hint="Panel tabs, the new tab menu, and Command Center take built-in icons only, so they use this one. Without an SVG, every place uses it. Tabs that are open change when you reload Paseo or open them again."
|
|
258
|
+
value={icon.menu}
|
|
259
|
+
options={menuOptions}
|
|
260
|
+
onValueChange={(menu) => change({ menu })}
|
|
261
|
+
/>
|
|
262
|
+
<SettingsRow label="Tab preview">
|
|
263
|
+
<MenuPreview theme={theme} name={icon.menu} />
|
|
264
|
+
</SettingsRow>
|
|
265
|
+
</SettingsCard>
|
|
266
|
+
</SettingsSection>
|
|
218
267
|
<SaveBar
|
|
219
268
|
theme={theme}
|
|
220
269
|
dirty={dirty}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "../shared/settings";
|
|
23
23
|
import { effectiveKeyScope, KeyScopeProvider } from "./key-scope";
|
|
24
24
|
import { ProjectSetup } from "./project-init";
|
|
25
|
+
import { ProjectKnowledgeSection } from "./project-knowledge";
|
|
25
26
|
import { AccessSection } from "./settings-access";
|
|
26
27
|
import { type PaseoProjectOption, useAuthStatus, useCatalog, useProjects } from "./queries";
|
|
27
28
|
import {
|
|
@@ -42,11 +43,15 @@ const STEPS_PLACEHOLDER = "Run the type checker\nUpdate the changelog\nOpen a dr
|
|
|
42
43
|
|
|
43
44
|
type Change = (patch: Partial<ProjectConfig>) => void;
|
|
44
45
|
|
|
45
|
-
export function ProjectSettings({ theme }: PluginSurfaceProps) {
|
|
46
|
+
export function ProjectSettings({ theme, navigation }: PluginSurfaceProps) {
|
|
46
47
|
const settings = useSettings(linearSettings);
|
|
48
|
+
const openAgent = useMemo(
|
|
49
|
+
() => (navigation ? (agentId: string) => navigation.openAgent({ agentId }) : undefined),
|
|
50
|
+
[navigation],
|
|
51
|
+
);
|
|
47
52
|
const render = useCallback(
|
|
48
|
-
(ready: ReadySettings) => <ProjectEditor theme={theme} settings={ready} />,
|
|
49
|
-
[theme],
|
|
53
|
+
(ready: ReadySettings) => <ProjectEditor theme={theme} settings={ready} openAgent={openAgent} />,
|
|
54
|
+
[theme, openAgent],
|
|
50
55
|
);
|
|
51
56
|
return (
|
|
52
57
|
<SettingsGate theme={theme} title="Projects" settings={settings}>
|
|
@@ -72,7 +77,12 @@ function projectOptions(
|
|
|
72
77
|
return [...known, ...missing];
|
|
73
78
|
}
|
|
74
79
|
|
|
75
|
-
function ProjectEditor(
|
|
80
|
+
function ProjectEditor(props: {
|
|
81
|
+
theme: Theme;
|
|
82
|
+
settings: ReadySettings;
|
|
83
|
+
openAgent?: (agentId: string) => void;
|
|
84
|
+
}) {
|
|
85
|
+
const { theme, settings } = props;
|
|
76
86
|
const { values, dirty, edit, save, discard } = useSettingsDraft(settings);
|
|
77
87
|
const projects = useProjects();
|
|
78
88
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
@@ -140,7 +150,8 @@ function ProjectEditor({ theme, settings }: { theme: Theme; settings: ReadySetti
|
|
|
140
150
|
</SettingsCard>
|
|
141
151
|
</SettingsSection>
|
|
142
152
|
<TeamsSection config={config} change={change} />
|
|
143
|
-
<
|
|
153
|
+
{paseoProject ? <ProjectKnowledgeSection projectId={paseoProject.projectId} /> : null}
|
|
154
|
+
<ProjectSetup theme={theme} config={config} change={change} openAgent={props.openAgent} />
|
|
144
155
|
<InstructionsSection theme={theme} config={config} change={change} />
|
|
145
156
|
{AGENT_ACTIONS.map((action) => (
|
|
146
157
|
<ActionSection key={action} theme={theme} action={action} config={config} change={change} />
|
package/client/web.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Platform } from "react-native";
|
|
|
3
3
|
// The only module that uses the DOM. React wheel listeners are passive, so they cannot keep
|
|
4
4
|
// the page from scrolling while the wheel zooms the graph. Off the web each export does nothing.
|
|
5
5
|
// The file picker is a hidden file input, because plugins cannot use native document pickers.
|
|
6
|
+
// Local storage keeps small values for the next start; it can be missing or blocked.
|
|
6
7
|
|
|
7
8
|
interface WheelEventLike {
|
|
8
9
|
deltaY: number;
|
|
@@ -89,3 +90,34 @@ export function pickTextFile(accept: string, maxBytes: number, onPick: (file: Pi
|
|
|
89
90
|
};
|
|
90
91
|
input.click();
|
|
91
92
|
}
|
|
93
|
+
|
|
94
|
+
interface StorageLike {
|
|
95
|
+
getItem(key: string): string | null;
|
|
96
|
+
setItem(key: string, value: string): void;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function storage(): StorageLike | null {
|
|
100
|
+
if (Platform.OS !== "web") return null;
|
|
101
|
+
try {
|
|
102
|
+
return (globalThis as { localStorage?: StorageLike }).localStorage ?? null;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** A value saved on this device, or null off the web or when storage is blocked. */
|
|
109
|
+
export function readStored(key: string): string | null {
|
|
110
|
+
try {
|
|
111
|
+
return storage()?.getItem(key) ?? null;
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function writeStored(key: string, value: string): void {
|
|
118
|
+
try {
|
|
119
|
+
storage()?.setItem(key, value);
|
|
120
|
+
} catch {
|
|
121
|
+
// Blocked storage only means the next start uses the built-in icon first.
|
|
122
|
+
}
|
|
123
|
+
}
|
package/index.client.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import { openPluginScreen, registerScreen } from "./client/compat";
|
|
|
4
4
|
import { gateAgentPanel } from "./client/access";
|
|
5
5
|
import { LivePanel } from "./client/live-panel";
|
|
6
6
|
import { contributeComposerPills } from "./client/pill";
|
|
7
|
-
import { PluginIcon } from "./client/plugin-icon";
|
|
7
|
+
import { onMenuIconChange, PluginIcon, startMenuIcon } from "./client/plugin-icon";
|
|
8
8
|
import { ConnectionSettings } from "./client/settings-connection";
|
|
9
9
|
import { IconSettingsScreen } from "./client/settings-icon";
|
|
10
10
|
import { LiveSettings } from "./client/settings-live";
|
|
@@ -12,6 +12,7 @@ import { ProjectSettings } from "./client/settings-projects";
|
|
|
12
12
|
import { PromptSettings } from "./client/settings-prompts";
|
|
13
13
|
import { focusIssue, panelScope, SCREEN_SCOPE, updateBrowser } from "./client/store";
|
|
14
14
|
import { IssueCardRow } from "./client/timeline-card";
|
|
15
|
+
import { BUILT_IN_ICON } from "./shared/custom-icon";
|
|
15
16
|
import { issueAttachments } from "./shared/issues";
|
|
16
17
|
import { ISSUE_CARD_KIND, IssueCardSchema, LINEAR_IDENTIFIER } from "./shared/linear";
|
|
17
18
|
|
|
@@ -19,23 +20,140 @@ const SCREEN_ID = "linear";
|
|
|
19
20
|
const PANEL_ID = "issues";
|
|
20
21
|
const LIVE_PANEL_ID = "live";
|
|
21
22
|
|
|
23
|
+
type Cleanup = () => void;
|
|
24
|
+
|
|
25
|
+
// Panels and Command Center items take the menu icon when they register, so a new choice
|
|
26
|
+
// registers the whole group again. One group keeps their order the same.
|
|
27
|
+
function contributeMenus(client: PluginClientContext, icon: string): Cleanup {
|
|
28
|
+
const cleanups: Cleanup[] = [];
|
|
29
|
+
const removeAll = () => {
|
|
30
|
+
for (const cleanup of cleanups.splice(0).reverse()) cleanup();
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
cleanups.push(
|
|
34
|
+
client.addWorkspacePanel({
|
|
35
|
+
id: PANEL_ID,
|
|
36
|
+
title: "Linear",
|
|
37
|
+
icon,
|
|
38
|
+
context: "workspace",
|
|
39
|
+
locations: ["workspace", "explorer"],
|
|
40
|
+
Component: LinearPanel,
|
|
41
|
+
}),
|
|
42
|
+
client.addWorkspacePanel({
|
|
43
|
+
id: LIVE_PANEL_ID,
|
|
44
|
+
title: "Linear Live",
|
|
45
|
+
icon: "Radar",
|
|
46
|
+
context: "agent",
|
|
47
|
+
Component: gateAgentPanel(LivePanel),
|
|
48
|
+
}),
|
|
49
|
+
client.addCommandCenterItem({
|
|
50
|
+
id: "open-issues",
|
|
51
|
+
title: "Linear: Open issues",
|
|
52
|
+
icon,
|
|
53
|
+
keywords: ["linear", "issues", "tickets"],
|
|
54
|
+
context: "global",
|
|
55
|
+
onSelect(context) {
|
|
56
|
+
openPluginScreen(context, SCREEN_ID);
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
client.addCommandCenterItem({
|
|
60
|
+
id: "create-issue",
|
|
61
|
+
title: "Linear: Create issue",
|
|
62
|
+
icon: "Plus",
|
|
63
|
+
keywords: ["linear", "new", "ticket"],
|
|
64
|
+
context: "global",
|
|
65
|
+
onSelect(context) {
|
|
66
|
+
updateBrowser(SCREEN_SCOPE, { creating: true });
|
|
67
|
+
openPluginScreen(context, SCREEN_ID);
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
client.addCommandCenterItem({
|
|
71
|
+
id: "workspace-issues",
|
|
72
|
+
title: "Linear: Show issues in this workspace",
|
|
73
|
+
icon,
|
|
74
|
+
keywords: ["linear", "panel"],
|
|
75
|
+
context: "workspace",
|
|
76
|
+
onSelect({ openPanel }) {
|
|
77
|
+
openPanel(PANEL_ID);
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
client.addCommandCenterItem({
|
|
81
|
+
id: "open-live",
|
|
82
|
+
title: "Linear: Open Linear Live",
|
|
83
|
+
icon: "Radar",
|
|
84
|
+
keywords: ["linear", "live", "progress", "map"],
|
|
85
|
+
context: "agent",
|
|
86
|
+
onSelect({ openPanel }) {
|
|
87
|
+
openPanel(LIVE_PANEL_ID);
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
client.addCommandCenterItem({
|
|
91
|
+
id: "setup-project",
|
|
92
|
+
title: "Linear: Set up project prompts",
|
|
93
|
+
icon: "Sparkles",
|
|
94
|
+
keywords: ["linear", "init", "setup", "agents.md", "guidelines"],
|
|
95
|
+
context: "global",
|
|
96
|
+
onSelect({ openSettings }) {
|
|
97
|
+
openSettings("projects");
|
|
98
|
+
},
|
|
99
|
+
}),
|
|
100
|
+
client.addCommandCenterItem({
|
|
101
|
+
id: "settings",
|
|
102
|
+
title: "Linear: Settings",
|
|
103
|
+
icon: "Settings",
|
|
104
|
+
keywords: ["linear", "api key", "prompts", "projects"],
|
|
105
|
+
context: "global",
|
|
106
|
+
onSelect({ openSettings }) {
|
|
107
|
+
openSettings("connection");
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
removeAll();
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
return removeAll;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Registers the menus with the chosen icon, and again when it changes. */
|
|
119
|
+
function contributeMenusWithIcon(client: PluginClientContext): Cleanup {
|
|
120
|
+
let icon = startMenuIcon();
|
|
121
|
+
let removeMenus: Cleanup;
|
|
122
|
+
try {
|
|
123
|
+
removeMenus = contributeMenus(client, icon);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error("Linear menu icon", error);
|
|
126
|
+
icon = BUILT_IN_ICON;
|
|
127
|
+
removeMenus = contributeMenus(client, icon);
|
|
128
|
+
}
|
|
129
|
+
const stop = onMenuIconChange((next) => {
|
|
130
|
+
if (next === icon) return;
|
|
131
|
+
removeMenus();
|
|
132
|
+
try {
|
|
133
|
+
removeMenus = contributeMenus(client, next);
|
|
134
|
+
icon = next;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
// An icon this Paseo does not have: keep the built-in one.
|
|
137
|
+
console.error("Linear menu icon", error);
|
|
138
|
+
removeMenus = contributeMenus(client, BUILT_IN_ICON);
|
|
139
|
+
icon = BUILT_IN_ICON;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
return () => {
|
|
143
|
+
stop();
|
|
144
|
+
removeMenus();
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
22
148
|
export default function contribute(client: PluginClientContext) {
|
|
23
149
|
client.addAttachmentSource(issueAttachments);
|
|
24
150
|
registerScreen(client, {
|
|
25
151
|
id: SCREEN_ID,
|
|
26
152
|
title: "Linear",
|
|
27
|
-
icon:
|
|
153
|
+
icon: BUILT_IN_ICON,
|
|
28
154
|
RowIcon: PluginIcon,
|
|
29
155
|
Component: LinearScreen,
|
|
30
156
|
});
|
|
31
|
-
client.addWorkspacePanel({
|
|
32
|
-
id: PANEL_ID,
|
|
33
|
-
title: "Linear",
|
|
34
|
-
icon: "SquareKanban",
|
|
35
|
-
context: "workspace",
|
|
36
|
-
locations: ["workspace", "explorer"],
|
|
37
|
-
Component: LinearPanel,
|
|
38
|
-
});
|
|
39
157
|
client.addSettingsScreen({
|
|
40
158
|
id: "connection",
|
|
41
159
|
title: "Account",
|
|
@@ -48,13 +166,6 @@ export default function contribute(client: PluginClientContext) {
|
|
|
48
166
|
icon: "MessageSquare",
|
|
49
167
|
Component: PromptSettings,
|
|
50
168
|
});
|
|
51
|
-
client.addWorkspacePanel({
|
|
52
|
-
id: LIVE_PANEL_ID,
|
|
53
|
-
title: "Linear Live",
|
|
54
|
-
icon: "Radar",
|
|
55
|
-
context: "agent",
|
|
56
|
-
Component: gateAgentPanel(LivePanel),
|
|
57
|
-
});
|
|
58
169
|
client.addSettingsScreen({
|
|
59
170
|
id: "live",
|
|
60
171
|
title: "Live",
|
|
@@ -73,67 +184,7 @@ export default function contribute(client: PluginClientContext) {
|
|
|
73
184
|
icon: "FolderGit2",
|
|
74
185
|
Component: ProjectSettings,
|
|
75
186
|
});
|
|
76
|
-
client
|
|
77
|
-
id: "open-issues",
|
|
78
|
-
title: "Linear: Open issues",
|
|
79
|
-
icon: "SquareKanban",
|
|
80
|
-
keywords: ["linear", "issues", "tickets"],
|
|
81
|
-
context: "global",
|
|
82
|
-
onSelect(context) {
|
|
83
|
-
openPluginScreen(context, SCREEN_ID);
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
client.addCommandCenterItem({
|
|
87
|
-
id: "create-issue",
|
|
88
|
-
title: "Linear: Create issue",
|
|
89
|
-
icon: "Plus",
|
|
90
|
-
keywords: ["linear", "new", "ticket"],
|
|
91
|
-
context: "global",
|
|
92
|
-
onSelect(context) {
|
|
93
|
-
updateBrowser(SCREEN_SCOPE, { creating: true });
|
|
94
|
-
openPluginScreen(context, SCREEN_ID);
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
client.addCommandCenterItem({
|
|
98
|
-
id: "workspace-issues",
|
|
99
|
-
title: "Linear: Show issues in this workspace",
|
|
100
|
-
icon: "SquareKanban",
|
|
101
|
-
keywords: ["linear", "panel"],
|
|
102
|
-
context: "workspace",
|
|
103
|
-
onSelect({ openPanel }) {
|
|
104
|
-
openPanel(PANEL_ID);
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
client.addCommandCenterItem({
|
|
108
|
-
id: "open-live",
|
|
109
|
-
title: "Linear: Open Linear Live",
|
|
110
|
-
icon: "Radar",
|
|
111
|
-
keywords: ["linear", "live", "progress", "map"],
|
|
112
|
-
context: "agent",
|
|
113
|
-
onSelect({ openPanel }) {
|
|
114
|
-
openPanel(LIVE_PANEL_ID);
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
client.addCommandCenterItem({
|
|
118
|
-
id: "setup-project",
|
|
119
|
-
title: "Linear: Set up project prompts",
|
|
120
|
-
icon: "Sparkles",
|
|
121
|
-
keywords: ["linear", "init", "setup", "agents.md", "guidelines"],
|
|
122
|
-
context: "global",
|
|
123
|
-
onSelect({ openSettings }) {
|
|
124
|
-
openSettings("projects");
|
|
125
|
-
},
|
|
126
|
-
});
|
|
127
|
-
client.addCommandCenterItem({
|
|
128
|
-
id: "settings",
|
|
129
|
-
title: "Linear: Settings",
|
|
130
|
-
icon: "Settings",
|
|
131
|
-
keywords: ["linear", "api key", "prompts", "projects"],
|
|
132
|
-
context: "global",
|
|
133
|
-
onSelect({ openSettings }) {
|
|
134
|
-
openSettings("connection");
|
|
135
|
-
},
|
|
136
|
-
});
|
|
187
|
+
const removeMenus = contributeMenusWithIcon(client);
|
|
137
188
|
client.addSlashCommand({
|
|
138
189
|
name: "linear",
|
|
139
190
|
description: "Open Linear issues, or jump to one by key",
|
|
@@ -154,5 +205,9 @@ export default function contribute(client: PluginClientContext) {
|
|
|
154
205
|
schema: IssueCardSchema,
|
|
155
206
|
Component: IssueCardRow,
|
|
156
207
|
});
|
|
157
|
-
|
|
208
|
+
const removePills = contributeComposerPills(client);
|
|
209
|
+
return () => {
|
|
210
|
+
removeMenus();
|
|
211
|
+
removePills();
|
|
212
|
+
};
|
|
158
213
|
}
|
package/index.server.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { registerAgentTools } from "./server/agent-tools";
|
|
|
5
5
|
import { createCredentialStore, defaultCredentialPath } from "./server/credentials";
|
|
6
6
|
import { registerHandlers } from "./server/handlers";
|
|
7
7
|
import { registerInit } from "./server/init";
|
|
8
|
+
import { createKnowledge, registerKnowledge } from "./server/knowledge";
|
|
8
9
|
import { registerLive } from "./server/live";
|
|
9
10
|
import { createLaunchQueue, createMapStore } from "./server/stores";
|
|
10
11
|
import { registerSync } from "./server/sync";
|
|
@@ -23,9 +24,11 @@ export default function contribute(server: PluginServerContext) {
|
|
|
23
24
|
const dataDirectory = path.dirname(credentialPath);
|
|
24
25
|
const maps = createMapStore(path.join(dataDirectory, "live-maps.json"));
|
|
25
26
|
const launches = createLaunchQueue(path.join(dataDirectory, "pending-launches.json"));
|
|
26
|
-
|
|
27
|
+
const knowledge = createKnowledge(path.join(dataDirectory, "knowledge"));
|
|
28
|
+
registerKnowledge(server, knowledge);
|
|
29
|
+
registerLive(server, { access, maps, launches, knowledge, readSettings });
|
|
27
30
|
registerSync(server, { access, readSettings });
|
|
28
|
-
registerInit(server, { readSettings });
|
|
31
|
+
registerInit(server, { readSettings, knowledge });
|
|
29
32
|
registerAgentHooks(server, dataDirectory);
|
|
30
33
|
const closeTools = registerAgentTools(server, { access, dataDirectory });
|
|
31
34
|
server.handle(accessRpc, async () => {
|