@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.
@@ -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
+ }
@@ -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 useBuiltIn = useCallback(() => {
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. Paseo menus and panel tabs keep the built-in 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="Built-in icon"
183
- hint="Remove your SVG and use the Linear board icon."
184
- actionLabel="Use built-in"
217
+ label="Remove SVG"
218
+ hint="Use the built-in icon everywhere."
219
+ actionLabel="Remove"
185
220
  disabled={!custom && markup === ""}
186
- onPress={useBuiltIn}
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 your SVG to use this. The built-in icon stays as drawn."}
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({ theme, settings }: { theme: Theme; settings: ReadySettings }) {
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
- <ProjectSetup theme={theme} config={config} change={change} />
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: "SquareKanban",
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.addCommandCenterItem({
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
- return contributeComposerPills(client);
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
- registerLive(server, { access, maps, launches, readSettings });
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 () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yannelli/paseo-linear-plugin",
3
- "version": "1.1.0-beta.2",
3
+ "version": "1.1.0-beta.4",
4
4
  "description": "Browse and triage Linear issues in Paseo and start agents from them",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",