@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/OVERVIEW.md
CHANGED
|
@@ -31,13 +31,21 @@ Open **Linear Live** from the Linear pill in an agent's composer. It shows the i
|
|
|
31
31
|
|
|
32
32
|
On agents not started from an issue, the Linear pill searches Linear and sends an issue to the agent.
|
|
33
33
|
|
|
34
|
+
## Plugin icon
|
|
35
|
+
|
|
36
|
+
In **Settings → Plugins → Linear → Icon**, choose an SVG file or paste SVG markup. Composer pills, the Linear sidebar row, the timeline card, and the Linear screens use it. Paseo menus and panel tabs keep the built-in icon. Keep the original colors, or paint the icon with the theme color, the theme accent, Linear indigo, or a custom color. **Solid** fills outline shapes. The plugin refuses SVGs with scripts, event handlers, or links to other files. The iOS and Android apps show the built-in icon in the chosen color.
|
|
37
|
+
|
|
34
38
|
## Update Linear from todos
|
|
35
39
|
|
|
36
40
|
Turn on **Update Linear from agent todos** in the Live settings. After each turn, a todo that starts with a sub-issue key, such as `ENG-124: add the queue`, moves that sub-issue to In Progress, and then to Done when all its todos are done. The parent moves to In Review when every sub-issue is done, never to Done. Statuses only move forward.
|
|
37
41
|
|
|
42
|
+
## Project knowledge
|
|
43
|
+
|
|
44
|
+
The daemon keeps a map of each project's files without an agent: its folders, services, packages, languages, commands, and guide files. Explore and setup agents start from it. Ticket-text maps use it to match short paths to real files and to put a named service on the map. When an agent works off the map, Linear Live shows the service or folder it is in. See it, or inspect again, under **Project knowledge** in **Projects**.
|
|
45
|
+
|
|
38
46
|
## Set up project prompts
|
|
39
47
|
|
|
40
|
-
In **Projects**, select **Start** under **Set up with an agent**. An agent reads the repository's guidelines, scripts, and CI files and proposes project instructions, steps, and prompt additions. You accept each change before it is saved.
|
|
48
|
+
In **Projects**, select **Start** under **Set up with an agent**. An agent reads the repository's guidelines, scripts, and CI files and proposes project instructions, steps, and prompt additions. You accept each change before it is saved. **Open agent** shows the agent while it works, and the lines it writes about the project and its areas go into the project knowledge.
|
|
41
49
|
|
|
42
50
|
## Choose projects
|
|
43
51
|
|
|
@@ -49,4 +57,4 @@ Choose **Attach Linear issue** in the composer attachment menu. The agent gets t
|
|
|
49
57
|
|
|
50
58
|
## Data and permissions
|
|
51
59
|
|
|
52
|
-
Only the daemon sends requests to `https://api.linear.app/graphql`. The daemon never sends a key back to the app. Paseo sends the prompt, with the issue snapshot, to the agent provider. Explore and setup agents read project files and send them to your agent provider. The daemon saves explore maps, and agents that wait for a map, beside the key file, and Linear Live lists and reads files only inside the agent's folder. For Claude agents, the daemon also reads the subagent transcripts Claude saves for the session, to show what each subagent does. With agent guidance on, it writes Claude Code hooks with the issue keys and titles beside the key file, and it serves the Linear tools on 127.0.0.1 with a token for each agent. Each token reaches only its issue and that issue's sub-issues. With todo sync on, the daemon changes issue statuses in Linear. The daemon keeps recent Linear responses in memory for up to 24 hours, and the list shows them with **Showing saved results. Updating…** while it gets new data.
|
|
60
|
+
Only the daemon sends requests to `https://api.linear.app/graphql`. The daemon never sends a key back to the app. Paseo sends the prompt, with the issue snapshot, to the agent provider. Explore and setup agents read project files and send them to your agent provider. The daemon saves explore maps, project knowledge, and agents that wait for a map, beside the key file, and Linear Live lists and reads files only inside the agent's folder. For Claude agents, the daemon also reads the subagent transcripts Claude saves for the session, to show what each subagent does. With agent guidance on, it writes Claude Code hooks with the issue keys and titles beside the key file, and it serves the Linear tools on 127.0.0.1 with a token for each agent. Each token reaches only its issue and that issue's sub-issues. With todo sync on, the daemon changes issue statuses in Linear. The daemon keeps recent Linear responses in memory for up to 24 hours, and the list shows them with **Showing saved results. Updating…** while it gets new data.
|
package/README.md
CHANGED
|
@@ -156,16 +156,16 @@ The **Linear** screen in the sidebar is not tied to a project, so it stays avail
|
|
|
156
156
|
Linear Live is an agent panel. Open it from the Linear pill, or with **Linear: Open Linear Live** in the command center while an agent is open. It works for agents started from an issue, with Claude, Codex, and other providers.
|
|
157
157
|
|
|
158
158
|
- **Sub-issues:** each sub-issue shows its Linear status and the agent's todo progress. The todos of the current sub-issue show under it. Select a sub-issue to show only its files on the map. Hide the issues column with the button on the **Issue** card to give the map or the graph the full width. The column becomes a strip with the status of each issue. Select an issue there to show only its files.
|
|
159
|
-
- **Map:** files are grouped by folder. The map marks the files the agent read, edited, or created, and the file it is on now. Files the agent touched but the map did not predict have a dashed border. A folder the agent listed or searched gets a solid border, and its header shows how many of its files the agent touched. Reads through the shell count too: `cat`, `sed -n`, `head`, `grep`, `rg`, `ls`, `find`, `git show`, and redirects to files.
|
|
160
|
-
- **Graph:** the files and the links between them. A line joins two files when one imports the other, a Markdown or style file links to the other, `package.json` names it, or the text of one names the path of the other. This works for TypeScript and JavaScript (with `tsconfig` paths and workspace packages), Python, Go, Rust, Ruby, CSS and Sass, Markdown, HTML, and config files. Files of one folder sit together under the folder name and take the color of their sub-issue. The issue is in the center, with its sub-issues joined to the files the map gives them. The agent hovers over what it works on: the file it reads or edits, or the sub-issue whose todo it just started. When the work moves, it glides along the links to the next node, and the lines at that node take its color. New files join without moving the rest. Agents that this agent started get their own cursor when they work in the same folder or in a worktree of the same project. An agent with no file activity yet hovers over the sub-issue whose key starts its title. Running subagents circle the agent that started them. Select a sub-issue to show only its files. Select any node to see what it is
|
|
159
|
+
- **Map:** files are grouped by folder. The map marks the files the agent read, edited, or created, and the file it is on now. Files the agent touched but the map did not predict have a dashed border. A folder the agent listed or searched gets a solid border, and its header shows how many of its files the agent touched. The header also names the service, package, or main folder the folder is in, from the [project knowledge](#project-knowledge). When the agent touches files the map did not predict, the line under the map tells where they are, such as `Off the map: billing-api · service (2), docs (1)`, and the **Now** line in the header shows **Off the map** with the place of the current file. Reads through the shell count too: `cat`, `sed -n`, `head`, `grep`, `rg`, `ls`, `find`, `git show`, and redirects to files.
|
|
160
|
+
- **Graph:** the files and the links between them. A line joins two files when one imports the other, a Markdown or style file links to the other, `package.json` names it, or the text of one names the path of the other. This works for TypeScript and JavaScript (with `tsconfig` paths and workspace packages), Python, Go, Rust, Ruby, CSS and Sass, Markdown, HTML, and config files. Files of one folder sit together under the folder name and take the color of their sub-issue. The issue is in the center, with its sub-issues joined to the files the map gives them. The agent hovers over what it works on: the file it reads or edits, or the sub-issue whose todo it just started. When the work moves, it glides along the links to the next node, and the lines at that node take its color. New files join without moving the rest. Agents that this agent started get their own cursor when they work in the same folder or in a worktree of the same project. An agent with no file activity yet hovers over the sub-issue whose key starts its title. Running subagents circle the agent that started them. Select a sub-issue to show only its files. Select any node to see what it is, how many files it links to, and for an off-map file, the service or folder it is in. Switch between **Map** and **Graph** on the card, or set the default in **Show files as** in the Live settings.
|
|
161
161
|
- **Graph view:** the icon buttons at the top right of the graph move the view. **Follow the running agents** zooms to the nodes the running agents work on and the files next to them, and moves with the agents. **Show the whole graph** zooms out. **Zoom out** and **Zoom in** change the zoom in steps. Scroll to zoom at the pointer, drag to move when zoomed in, or pinch on a touch screen. Each of these stops following. The lock button lets the wheel and drags scroll the page instead. Nodes, lines, and labels keep their size when you zoom, and more file names show when there is room. The graph keeps your follow and lock choices.
|
|
162
162
|
- **Agents:** the agents that work under this agent. Paseo agents that this agent started show their status, their files, and their own markers on the map when they work in the same folder or in a worktree of the same project. Select one to open it. Subagents that run inside the provider, such as Claude's Agent tool, show their type, description, and status. For Claude, the daemon reads each subagent's transcript, also for subagents that run in the background, so a subagent shows what it does now, gets its own cursor, and adds its files to the map. Other providers do not send subagent file reads, so the map shows only the files named in the subagent's prompt.
|
|
163
163
|
- **Activity:** the latest reads, edits, searches, and commands, with line counts and exit codes.
|
|
164
164
|
|
|
165
165
|
The map comes from one of two sources. Choose it in **Settings → Plugins → Linear → Live**:
|
|
166
166
|
|
|
167
|
-
- **Ticket text** (the default) finds file and folder paths in the issue title, description, and sub-issue titles. It costs nothing, but it finds only paths the issue names. Linear sends sub-issue titles but not their descriptions.
|
|
168
|
-
- **Explore agent** maps the files first when you start an implement agent. It reads the repository and lists the files for each sub-issue. The implement agent starts when the map is ready, or with the ticket-text map if exploring fails. A saved map is used again, so each issue is explored once. The plugin archives the explore agent when it finishes. Choose its model and effort in the Live settings. A faster, cheaper model, such as Haiku, is usually enough. It lists up to 150 files.
|
|
167
|
+
- **Ticket text** (the default) finds file and folder paths in the issue title, description, and sub-issue titles. It costs nothing, but it finds only paths the issue names. Linear sends sub-issue titles but not their descriptions. With project knowledge, a short path such as `queries.ts` becomes the one project file that ends with it, and a service or package that the text names, such as `billing-api` or "the billing service", puts its folder on the map. A plain folder counts only when the text calls it a folder, such as "the docs folder".
|
|
168
|
+
- **Explore agent** maps the files first when you start an implement agent. It starts from the project map in the project knowledge, reads the repository, and lists the files for each sub-issue. The implement agent starts when the map is ready, or with the ticket-text map if exploring fails. A saved map is used again, so each issue is explored once. The plugin archives the explore agent when it finishes. Choose its model and effort in the Live settings. A faster, cheaper model, such as Haiku, is usually enough. It lists up to 150 files.
|
|
169
169
|
|
|
170
170
|
The buttons on the map card control the saved map. **Read files and links again** reloads the map, the folder lists, and the links. **Build** or **Rebuild the map with the explore agent** starts an explore run now and replaces the saved map when it finishes. **Clear the saved map** deletes it, so the map uses the ticket text until the next explore run. You cannot clear a map while an explore run makes it.
|
|
171
171
|
|
|
@@ -179,6 +179,15 @@ For implement agents on an issue with sub-issues, the prompt asks the agent to s
|
|
|
179
179
|
|
|
180
180
|
Each agent's composer has a **Linear** pill. On an agent started from an issue, the pill shows the issue key. It opens the issue status, each sub-issue with its todo progress, and buttons for Linear Live, the issue, and **Sync now**. On other agents, the pill searches Linear and sends the issue text to the agent as a message.
|
|
181
181
|
|
|
182
|
+
### Plugin icon
|
|
183
|
+
|
|
184
|
+
In **Settings → Plugins → Linear → Icon**, choose an SVG file or paste SVG markup. Composer pills, the Linear sidebar row on Paseo 0.11 and newer, the issue card in the timeline, and the Linear screens then use it. Paseo menus, panel tabs, and settings take only built-in icon names, so they keep the Linear board icon. The plugin accepts SVG only. It refuses scripts, event handlers, HTML content, entities, and links to other files or bitmap images. The SVG needs a viewBox, or a width and height in pixels.
|
|
185
|
+
|
|
186
|
+
- **Color:** **Original colors** keeps the SVG as drawn, and `currentColor` takes the color Paseo gives the icon. **Theme color**, **Theme accent**, **Linear indigo**, and **Custom color** paint the whole icon one color and keep its transparency. These choices also apply to the built-in icon.
|
|
187
|
+
- **Solid:** fills shapes that only have an outline, with their stroke color, so a line icon shows as a solid shape.
|
|
188
|
+
|
|
189
|
+
The preview shows each change before you save. The iOS and Android apps cannot draw SVG images from plugins, so they show the built-in icon in the chosen color.
|
|
190
|
+
|
|
182
191
|
### Update Linear from agent todos
|
|
183
192
|
|
|
184
193
|
Turn on **Update Linear from agent todos** in the Live settings. After each turn of an implement agent, the daemon reads the agent's todos:
|
|
@@ -190,9 +199,20 @@ Turn on **Update Linear from agent todos** in the Live settings. After each turn
|
|
|
190
199
|
|
|
191
200
|
The daemon reads the issue from Linear before each sync, so a status you changed by hand counts. **Sync now** in the pill runs the same check at once.
|
|
192
201
|
|
|
202
|
+
### Project knowledge
|
|
203
|
+
|
|
204
|
+
The daemon keeps a map of each project's files, so explore and setup agents, ticket-text maps, and Linear Live know the project before an agent reads it. No agent runs for it, and it costs no provider usage. The daemon makes it the first time a project needs it: when you open the project in **Settings → Plugins → Linear → Projects**, open Linear Live for one of its agents, or start an explore or setup run. It makes it again when it is 6 hours old, and **Inspect again** under **Project knowledge** makes it at once.
|
|
205
|
+
|
|
206
|
+
- **Files:** the files git tracks or would track, without ignored files. Outside a git repository, the daemon walks the folder and skips folders such as `node_modules`, `dist`, and `.git`. It keeps up to 20,000 paths, the shallow ones first.
|
|
207
|
+
- **Areas:** services, packages, and main folders. A folder with a manifest such as `package.json`, `go.mod`, `Cargo.toml`, `pyproject.toml`, or a `.csproj` file is a package, named by the manifest. A folder with a `Dockerfile`, `Procfile`, `fly.toml`, or a similar file, a folder that a compose file builds, and a package under a folder such as `apps/` or `services/` is a service. Each top folder is an area too, and so is each subfolder of a top folder that holds 40% or more of the files. Folder names give a role, such as backend, frontend, tests, or docs. Manifests in fixture, example, and template folders do not count.
|
|
208
|
+
- **Languages, commands, and guides:** file counts by language, the root package scripts and Makefile targets, and files that guide agents, such as `AGENTS.md`, `CLAUDE.md`, the README, CONTRIBUTING, and CI workflows.
|
|
209
|
+
- **Summaries:** the setup agent adds one line about the project and one line about each area. They stay when the daemon makes the map again.
|
|
210
|
+
|
|
211
|
+
The **Project knowledge** section in the Projects settings shows what the daemon found.
|
|
212
|
+
|
|
193
213
|
### Set up project prompts with an agent
|
|
194
214
|
|
|
195
|
-
In **Settings → Plugins → Linear → Projects**, select **Start** under **Set up with an agent**.
|
|
215
|
+
In **Settings → Plugins → Linear → Projects**, select **Start** under **Set up with an agent**. The daemon makes the project knowledge again first, and the agent starts from that project map. While it runs, **Open agent** opens the setup agent so you can watch it. The agent reads the project's AGENTS.md, CLAUDE.md, CONTRIBUTING, README, scripts, and CI files. It then proposes project instructions, steps, and text to add to the Implement and Review prompts. Each change shows the current and proposed text. Turn off the ones you do not want, select **Use accepted changes**, and then save. Nothing changes until you save. The agent also describes the project and each area, and the daemon saves those lines to the project knowledge at once.
|
|
196
216
|
|
|
197
217
|
## Data and permissions
|
|
198
218
|
|
|
@@ -200,9 +220,11 @@ In **Settings → Plugins → Linear → Projects**, select **Start** under **Se
|
|
|
200
220
|
- **Data sent to Linear:** queries for issues, teams, users, and comments, and the changes you make. When you start an agent with the options on, the plugin moves the issue to In Progress and assigns it to you.
|
|
201
221
|
- **Data sent to the agent provider:** the prompt. It holds the template text and an issue snapshot: identifier, title, URL, team, status, priority, assignee, project, labels, parent, description, sub-issues, and links. It holds comments only when **Include comments in the prompt** is on. It also holds the project instructions and steps from the Projects settings. An attached issue sends the identifier, title, URL, status, priority, assignee, project, labels, and description.
|
|
202
222
|
- **Linear Live and setup agents:** the explore agent gets the issue snapshot without comments, and both agents read files in the project. Your agent provider gets what they read. The map of an explore run is saved in `live-maps.json` beside the key file, for up to 200 issues. An agent that waits for a map is saved in `pending-launches.json` there until it starts. Linear Live lists files only in the folders it shows, and reads the files on the graph to find their links, inside the agent's own folder. It does not follow links that lead outside that folder. For a Claude agent with subagents, the daemon reads the tool calls in the subagent transcripts that Claude saves for that session. It sends the app only the tool names, file paths, and commands, never file text.
|
|
223
|
+
- **Project knowledge:** the daemon lists the files of a Paseo project with `git ls-files`, or by walking the folder, and reads small manifest files (up to 256 KB) inside the project folder. It does not follow links that lead outside the folder. It saves file paths, manifest names, script names, and summaries in `knowledge/` beside the key file, one file per project. The app gets the areas, counts, commands, and guides, never the file list. Explore and setup prompts hold the project map: folder names with file counts, areas, commands, and guide file names. Your agent provider gets that text.
|
|
203
224
|
- **Linear tools for agents:** the daemon runs the `linear` MCP server on `127.0.0.1` only, and only after an agent gets it. Each agent gets its own random token, and its tools reach only its issue and that issue's sub-issues. Edits use the Linear key of the project that loaded the issue. Requests from a web page are refused. `agent-tools.json` beside the key file keeps the port and a hash of each token, so agents keep their tools after a restart. The token itself is in the agent's Paseo config.
|
|
204
225
|
- **Claude hooks:** when an **Agent guidance** switch is on for a Claude agent, the daemon writes a Claude Code plugin to `claude-hooks/` beside the key file, one folder for each issue and set of hooks. It holds the issue key and title and the sub-issue keys and titles. The agent starts with `--plugin-dir` set to that folder. The hooks print that text and run no other command. The daemon does not write hooks on Windows hosts.
|
|
205
226
|
- **Todo sync:** when it is on, the daemon changes issue statuses in Linear with the key of the project that loaded the issue.
|
|
227
|
+
- **Plugin icon:** the SVG is saved in the plugin settings on the daemon host. The app draws it as an image, which does not run scripts or load other files.
|
|
206
228
|
- **Cached results:** the daemon keeps up to 300 recent Linear responses in memory for up to 24 hours. It groups them by a hash of the key, so two keys never share results. The app shows saved results at once with **Showing saved results. Updating…** while it gets new data. An edit clears the cached lists and issues for that key. Saving or removing a key clears the whole cache. The cache is not written to disk, and a daemon restart clears it.
|
|
207
229
|
|
|
208
230
|
Linear errors, such as a rejected key or a rate limit, show in the panel or as a message.
|
|
@@ -211,7 +233,7 @@ Linear errors, such as a rejected key or a rate limit, show in the panel or as a
|
|
|
211
233
|
|
|
212
234
|
| File | Runtime | Role |
|
|
213
235
|
| --- | --- | --- |
|
|
214
|
-
| `index.client.tsx` | App | Registers the Linear screen, the workspace panel, the Linear Live panel, the Account, Prompts, Live, and Projects settings, the command center items, the `/linear` command, the timeline card, the composer attachment, and the Linear pill |
|
|
236
|
+
| `index.client.tsx` | App | Registers the Linear screen, the workspace panel, the Linear Live panel, the Account, Prompts, Live, Icon, and Projects settings, the command center items, the `/linear` command, the timeline card, the composer attachment, and the Linear pill |
|
|
215
237
|
| `index.server.ts` | Daemon | Registers the settings, the RPC handlers, and the todo sync hook |
|
|
216
238
|
| `client/browser.tsx` | App | Screen and panel: picks the key, then shows the list, the issue, or the agent setup page |
|
|
217
239
|
| `client/issue-list.tsx`, `client/issue-row.tsx`, `client/issue-tree.ts` | App | Search, filters, sort, rows, status groups, and sub-issue nesting |
|
|
@@ -220,6 +242,7 @@ Linear errors, such as a rejected key or a rate limit, show in the panel or as a
|
|
|
220
242
|
| `client/markdown.tsx` | App | Renders Markdown |
|
|
221
243
|
| `client/launch.tsx`, `client/launch-fields.tsx`, `client/launch-guidance.tsx`, `client/launch-choices.ts`, `client/launch-plan.ts`, `client/agent-options.ts` | App | Agent setup page, Run in options, agent guidance, and agent start |
|
|
222
244
|
| `client/model-browser.tsx`, `client/provider-icon.tsx` | App | Model picker and provider icons |
|
|
245
|
+
| `client/plugin-icon.tsx`, `client/settings-icon.tsx`, `shared/custom-icon.ts` | App, Both | The plugin icon, its settings screen, and the SVG checks, colors, and fill |
|
|
223
246
|
| `client/create-issue.tsx`, `client/pickers.tsx` | App | New issue form and option pickers |
|
|
224
247
|
| `client/connect.tsx`, `client/settings-*.tsx` | App | Key form and settings screens |
|
|
225
248
|
| `client/queries.ts`, `client/store.ts`, `client/key-scope.tsx` | App | Data hooks with saved results, browser state, and the key in use |
|
|
@@ -227,9 +250,9 @@ Linear errors, such as a rejected key or a rate limit, show in the panel or as a
|
|
|
227
250
|
| `client/timeline-card.tsx`, `client/ui.tsx`, `client/glyphs.tsx` | App | Timeline card, shared controls, and icons |
|
|
228
251
|
| `client/live-panel.tsx`, `client/live-header.tsx`, `client/live-issues.tsx`, `client/live-feed.tsx`, `client/live-timeline.ts` | App | Linear Live panel, header, sub-issues, activity, and the agent timeline feed |
|
|
229
252
|
| `client/live-map.tsx`, `client/live-graph.tsx`, `client/graph-labels.ts`, `client/graph-camera.tsx`, `client/live-cursor.tsx`, `client/live-agents.tsx` | App | File map, file graph with its labels and camera, agent cursors, and the agents list |
|
|
230
|
-
| `client/tool-buttons.tsx`, `client/web.ts` | App | Icon buttons for the map and graph, and the wheel listener on the web |
|
|
253
|
+
| `client/tool-buttons.tsx`, `client/web.ts` | App | Icon buttons for the map and graph, and the wheel listener and file picker on the web |
|
|
231
254
|
| `client/pill.tsx` | App | Linear pill in each agent's composer |
|
|
232
|
-
| `client/project-init.tsx` | App | Project setup proposal in the Projects settings |
|
|
255
|
+
| `client/project-init.tsx`, `client/project-knowledge.tsx` | App | Project setup proposal and the Project knowledge section in the Projects settings |
|
|
233
256
|
| `shared/linear.ts` | Both | RPC contracts and issue schemas |
|
|
234
257
|
| `shared/issues.ts` | Both | The `issues.search` RPC and the composer attachment source |
|
|
235
258
|
| `shared/settings.ts` | Both | Settings schema: templates, agent defaults, and project settings |
|
|
@@ -241,6 +264,7 @@ Linear errors, such as a rejected key or a rate limit, show in the panel or as a
|
|
|
241
264
|
| `shared/agent-hooks.ts`, `server/agent-hooks.ts` | Both, Daemon | Claude Code hooks for agent guidance, and the plugin folder the daemon writes |
|
|
242
265
|
| `shared/agent-tools.ts`, `server/agent-tools.ts`, `server/mcp-http.ts` | Both, Daemon | The `linear` MCP server: its tools, tokens, and HTTP transport |
|
|
243
266
|
| `shared/todo-sync.ts`, `shared/project-setup.ts` | Both | Todo keys, status moves, and setup proposals |
|
|
267
|
+
| `shared/knowledge.ts` | Both | Project knowledge contracts, areas and their labels, the project map for prompts, and ticket path matching |
|
|
244
268
|
| `server/handlers.ts` | Daemon | RPC handlers, key lookup, and cache use |
|
|
245
269
|
| `server/credentials.ts` | Daemon | Key file and key order |
|
|
246
270
|
| `server/cache.ts` | Daemon | Response cache in memory |
|
|
@@ -248,6 +272,7 @@ Linear errors, such as a rejected key or a rate limit, show in the panel or as a
|
|
|
248
272
|
| `server/linear.ts` | Daemon | Search and snapshot text for the composer attachment |
|
|
249
273
|
| `server/live.ts`, `server/links.ts`, `server/subagent-logs.ts`, `server/agent-runs.ts` | Daemon | File listing, links between files, subagent transcripts, explore runs, and the saved maps |
|
|
250
274
|
| `server/sync.ts`, `server/init.ts` | Daemon | Todo sync after each turn, and project setup runs |
|
|
275
|
+
| `server/inspect.ts`, `server/knowledge.ts` | Daemon | Project inspection without an agent, and the saved project knowledge |
|
|
251
276
|
|
|
252
277
|
The client bundle holds no credentials and makes no calls to Linear.
|
|
253
278
|
|
|
@@ -263,7 +288,7 @@ npm run check
|
|
|
263
288
|
paseo plugin add "$PWD"
|
|
264
289
|
```
|
|
265
290
|
|
|
266
|
-
After you edit the source, run `paseo plugin reload paseo-linear-plugin`. `npm run check` runs the typecheck and the tests. The tests cover the Linear client, the key file, the cache, the handlers, sub-issue nesting, the Markdown parser and task list toggle, the description autosave, the agent options, the prompt guidance and its hooks, the MCP server and its tools, the links between files in many languages, the graph layout and camera, and the release scripts. They use a local GraphQL server and do not call Linear.
|
|
291
|
+
After you edit the source, run `paseo plugin reload paseo-linear-plugin`. `npm run check` runs the typecheck and the tests. The tests cover the Linear client, the key file, the cache, the handlers, sub-issue nesting, the Markdown parser and task list toggle, the description autosave, the agent options, the prompt guidance and its hooks, the MCP server and its tools, the links between files in many languages, the graph layout and camera, the SVG icon checks and colors, and the release scripts. They use a local GraphQL server and do not call Linear.
|
|
267
292
|
|
|
268
293
|
## Graphics
|
|
269
294
|
|
package/client/access.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type PluginAgentPanelProps, useSettings, useWorkspace } from "@getpaseo/plugin/client";
|
|
2
2
|
import { type ComponentType, type ReactNode, useEffect } from "react";
|
|
3
3
|
import { linearSettings, type ProjectAccess, projectEnabled } from "../shared/settings";
|
|
4
|
+
import { PluginIcon } from "./plugin-icon";
|
|
4
5
|
import { EmptyState, type Theme } from "./ui";
|
|
5
6
|
|
|
6
7
|
// Composer pills live outside React, so components that read settings share the project
|
|
@@ -44,7 +45,7 @@ export function ProjectGate(props: {
|
|
|
44
45
|
return (
|
|
45
46
|
<EmptyState
|
|
46
47
|
theme={props.theme}
|
|
47
|
-
icon=
|
|
48
|
+
icon={PluginIcon}
|
|
48
49
|
title="Linear is off for this project"
|
|
49
50
|
detail="Turn it on in Linear settings, under Projects."
|
|
50
51
|
/>
|
package/client/compat.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PluginClientContext, PluginSurfaceProps } from "@getpaseo/plugin/client";
|
|
2
2
|
import * as hostUi from "@getpaseo/plugin/client/ui";
|
|
3
|
-
import { type ComponentType, createElement } from "react";
|
|
3
|
+
import { type ComponentType, createContext, createElement, useContext } from "react";
|
|
4
4
|
|
|
5
5
|
// Paseo 0.11 replaced surfaces with screens and sidebar header items. 0.10 only has
|
|
6
6
|
// addSurface/addSidebarItem; later releases drop those aliases. Detect once, register once.
|
|
@@ -13,6 +13,7 @@ interface ScreenRef {
|
|
|
13
13
|
interface SidebarItemProps {
|
|
14
14
|
currentScreen: ScreenRef | null;
|
|
15
15
|
openScreen(input: ScreenRef): void;
|
|
16
|
+
theme?: PluginSurfaceProps["theme"];
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
interface ScreenClient {
|
|
@@ -33,8 +34,13 @@ interface SurfaceClient {
|
|
|
33
34
|
addSidebarItem(input: { id: string; title: string; icon: string; surface: string }): Cleanup;
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
interface RowIconProps {
|
|
38
|
+
size: number;
|
|
39
|
+
color: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
type SidebarRowComponent = ComponentType<{
|
|
37
|
-
icon?: string
|
|
43
|
+
icon?: string | ComponentType<RowIconProps>;
|
|
38
44
|
label?: string;
|
|
39
45
|
active?: boolean;
|
|
40
46
|
onPress(): void;
|
|
@@ -48,9 +54,14 @@ export interface ScreenRegistration {
|
|
|
48
54
|
id: string;
|
|
49
55
|
title: string;
|
|
50
56
|
icon: string;
|
|
57
|
+
/** Drawn in the sidebar row where the host accepts icon components; else `icon`. */
|
|
58
|
+
RowIcon?: ComponentType<RowIconProps & { theme: PluginSurfaceProps["theme"] | null }>;
|
|
51
59
|
Component: ComponentType<PluginSurfaceProps>;
|
|
52
60
|
}
|
|
53
61
|
|
|
62
|
+
// The row draws its icon with a size and a color only, so the item passes its theme down.
|
|
63
|
+
const RowTheme = createContext<PluginSurfaceProps["theme"] | null>(null);
|
|
64
|
+
|
|
54
65
|
export function registerScreen(client: PluginClientContext, screen: ScreenRegistration): Cleanup {
|
|
55
66
|
const SidebarRow = (hostUi as Record<string, unknown>).SidebarRow as
|
|
56
67
|
| SidebarRowComponent
|
|
@@ -61,12 +72,19 @@ export function registerScreen(client: PluginClientContext, screen: ScreenRegist
|
|
|
61
72
|
title: screen.title,
|
|
62
73
|
Component: screen.Component,
|
|
63
74
|
});
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
75
|
+
const { RowIcon } = screen;
|
|
76
|
+
const ThemedIcon = RowIcon
|
|
77
|
+
? function ThemedIcon(props: RowIconProps) {
|
|
78
|
+
return createElement(RowIcon, { ...props, theme: useContext(RowTheme) });
|
|
79
|
+
}
|
|
80
|
+
: null;
|
|
81
|
+
function SidebarItem({ currentScreen, openScreen, theme }: SidebarItemProps) {
|
|
82
|
+
const row = createElement(SidebarRow as SidebarRowComponent, {
|
|
83
|
+
icon: ThemedIcon ?? screen.icon,
|
|
67
84
|
active: currentScreen?.screenId === screen.id,
|
|
68
85
|
onPress: () => openScreen({ screenId: screen.id }),
|
|
69
86
|
});
|
|
87
|
+
return createElement(RowTheme.Provider, { value: theme ?? null }, row);
|
|
70
88
|
}
|
|
71
89
|
const removeItem = client.addSidebarHeaderItem({
|
|
72
90
|
id: screen.id,
|
package/client/connect.tsx
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { useRpc } from "@getpaseo/plugin/client";
|
|
2
|
-
import {
|
|
2
|
+
import { TextInput, useToast } from "@getpaseo/plugin/client/react-native";
|
|
3
3
|
import { ExternalLink } from "@getpaseo/plugin/client/ui";
|
|
4
4
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
5
5
|
import { useCallback, useMemo, useState } from "react";
|
|
6
6
|
import { Text, View } from "react-native";
|
|
7
7
|
import { authSaveRpc } from "../shared/linear";
|
|
8
|
+
import { PluginIcon } from "./plugin-icon";
|
|
8
9
|
import { Button, errorMessage, type Theme } from "./ui";
|
|
9
10
|
|
|
10
11
|
export const LINEAR_KEY_SETTINGS_URL = "https://linear.app/settings/account/security";
|
|
@@ -121,7 +122,7 @@ export function ConnectCard({ theme, compact }: { theme: Theme; compact: boolean
|
|
|
121
122
|
<View style={styles.root}>
|
|
122
123
|
<View style={styles.card}>
|
|
123
124
|
<View style={styles.header}>
|
|
124
|
-
<
|
|
125
|
+
<PluginIcon size={22} color={colors.foreground} theme={theme} />
|
|
125
126
|
<Text style={styles.title}>Connect Linear</Text>
|
|
126
127
|
</View>
|
|
127
128
|
<Text style={styles.body}>
|
package/client/live-graph.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { Animated, type LayoutChangeEvent, Pressable, StyleSheet, Text, View } f
|
|
|
3
3
|
import { focusBox, project, unproject } from "../shared/graph-camera";
|
|
4
4
|
import type { Placed, Point } from "../shared/graph-geometry";
|
|
5
5
|
import { focusNode, type Graph, type GraphEdge, type GraphNode, graphKey, layoutGraph } from "../shared/graph-model";
|
|
6
|
+
import { type AreaRef, whereLabel } from "../shared/knowledge";
|
|
6
7
|
import type { IssueDetail, WorkflowState } from "../shared/linear";
|
|
7
8
|
import type { GraphCamera } from "../shared/settings";
|
|
8
9
|
import { CameraButtons, useGraphCamera } from "./graph-camera";
|
|
@@ -29,6 +30,8 @@ export interface GraphViewProps {
|
|
|
29
30
|
progress: IssueProgress;
|
|
30
31
|
owners: OwnerColors;
|
|
31
32
|
agents: readonly CursorAgent[];
|
|
33
|
+
/** Areas of the project, to say where an off-map file is. */
|
|
34
|
+
areas: readonly AreaRef[];
|
|
32
35
|
focus: string | null;
|
|
33
36
|
onFocus(key: string): void;
|
|
34
37
|
/** Saved camera mode: follow the running agents, or show the whole graph. */
|
|
@@ -223,7 +226,7 @@ const GraphNodeView = memo(function GraphNodeView(props: NodeProps) {
|
|
|
223
226
|
);
|
|
224
227
|
}, sameNode);
|
|
225
228
|
|
|
226
|
-
function caption(node: GraphNode, issue: IssueDetail, graph: Graph): string {
|
|
229
|
+
function caption(node: GraphNode, issue: IssueDetail, graph: Graph, areas: readonly AreaRef[]): string {
|
|
227
230
|
if (node.kind === "issue") return `${issue.identifier} · ${issue.title}`;
|
|
228
231
|
if (node.kind === "subissue") {
|
|
229
232
|
const child = issue.children.find((entry) => entry.identifier.toUpperCase() === node.label);
|
|
@@ -231,7 +234,7 @@ function caption(node: GraphNode, issue: IssueDetail, graph: Graph): string {
|
|
|
231
234
|
return `${node.label} · ${child?.state.name ?? ""} · ${child?.title ?? ""}${folded}`;
|
|
232
235
|
}
|
|
233
236
|
const tile = node.tile;
|
|
234
|
-
const where = tile?.onMap ? `on the map for ${node.owner}` :
|
|
237
|
+
const where = tile?.onMap ? `on the map for ${node.owner}` : `not on the map, in ${whereLabel(node.path ?? "", areas)}`;
|
|
235
238
|
const touch = !tile || tile.touch === "none" ? "not touched yet" : tile.touch;
|
|
236
239
|
const lines = tile && (tile.added > 0 || tile.removed > 0) ? ` · +${tile.added} -${tile.removed}` : "";
|
|
237
240
|
const linked = graph.edges.filter((edge) => edge.kind !== "owns" && (edge.from === node.id || edge.to === node.id)).length;
|
|
@@ -507,7 +510,7 @@ export function LiveGraph(props: GraphViewProps) {
|
|
|
507
510
|
<View style={styles.caption}>
|
|
508
511
|
{selectedNode ? (
|
|
509
512
|
<Text numberOfLines={2} style={styles.captionText}>
|
|
510
|
-
{caption(selectedNode, issue, graph)}
|
|
513
|
+
{caption(selectedNode, issue, graph, props.areas)}
|
|
511
514
|
</Text>
|
|
512
515
|
) : (
|
|
513
516
|
<Text style={styles.hint}>Select a node to see its file or issue.</Text>
|
package/client/live-header.tsx
CHANGED
|
@@ -79,7 +79,7 @@ function StatusChip(props: { theme: Theme; status: AgentStatus; provider: string
|
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
function NowLine(props: { theme: Theme; event: ActivityEvent; live: boolean }) {
|
|
82
|
+
function NowLine(props: { theme: Theme; event: ActivityEvent; live: boolean; offMap: string | null }) {
|
|
83
83
|
const { theme, event } = props;
|
|
84
84
|
const { colors } = theme;
|
|
85
85
|
const tone = eventTone(theme, event);
|
|
@@ -106,6 +106,18 @@ function NowLine(props: { theme: Theme; event: ActivityEvent; live: boolean }) {
|
|
|
106
106
|
tint: { ...StyleSheet.absoluteFillObject, borderRadius: 6, backgroundColor: tone, opacity: 0.14 },
|
|
107
107
|
text: { color: props.live ? colors.foreground : colors.foregroundMuted, fontSize: 13 },
|
|
108
108
|
detail: { flex: 1, color: colors.foregroundMuted, fontFamily: MONO, fontSize: 11 },
|
|
109
|
+
off: {
|
|
110
|
+
flexShrink: 0,
|
|
111
|
+
maxWidth: 220,
|
|
112
|
+
paddingHorizontal: 7,
|
|
113
|
+
paddingVertical: 2,
|
|
114
|
+
borderRadius: 6,
|
|
115
|
+
borderWidth: 1,
|
|
116
|
+
borderStyle: "dashed",
|
|
117
|
+
borderColor: colors.statusWarning,
|
|
118
|
+
color: colors.statusWarning,
|
|
119
|
+
fontSize: 11,
|
|
120
|
+
},
|
|
109
121
|
}) as const,
|
|
110
122
|
[colors, tone, props.live],
|
|
111
123
|
);
|
|
@@ -124,6 +136,11 @@ function NowLine(props: { theme: Theme; event: ActivityEvent; live: boolean }) {
|
|
|
124
136
|
{event.detail}
|
|
125
137
|
</Text>
|
|
126
138
|
) : null}
|
|
139
|
+
{props.offMap ? (
|
|
140
|
+
<Text numberOfLines={1} style={styles.off} accessibilityLabel={`Off the map, in ${props.offMap}`}>
|
|
141
|
+
{`Off the map · ${props.offMap}`}
|
|
142
|
+
</Text>
|
|
143
|
+
) : null}
|
|
127
144
|
</View>
|
|
128
145
|
);
|
|
129
146
|
}
|
|
@@ -136,6 +153,8 @@ export function LiveHeader(props: {
|
|
|
136
153
|
status: AgentStatus;
|
|
137
154
|
provider: string;
|
|
138
155
|
current: ActivityEvent | null;
|
|
156
|
+
/** Where the current file is when the agent works off the map. */
|
|
157
|
+
offMap: string | null;
|
|
139
158
|
reduceMotion: boolean;
|
|
140
159
|
}) {
|
|
141
160
|
const { theme, issue, progress, owners } = props;
|
|
@@ -204,7 +223,12 @@ export function LiveHeader(props: {
|
|
|
204
223
|
</View>
|
|
205
224
|
{counts.length > 0 ? <Text style={styles.counts}>{counts.join(" · ")}</Text> : null}
|
|
206
225
|
{props.current ? (
|
|
207
|
-
<NowLine
|
|
226
|
+
<NowLine
|
|
227
|
+
theme={theme}
|
|
228
|
+
event={props.current}
|
|
229
|
+
live={props.status === "running"}
|
|
230
|
+
offMap={props.offMap}
|
|
231
|
+
/>
|
|
208
232
|
) : null}
|
|
209
233
|
</View>
|
|
210
234
|
);
|
package/client/live-map.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type MapZone,
|
|
9
9
|
masonry,
|
|
10
10
|
ZONE_GAP,
|
|
11
|
+
zoneAreaLabel,
|
|
11
12
|
} from "../shared/map-model";
|
|
12
13
|
import { NATIVE_DRIVER, type OwnerColors } from "./live-timeline";
|
|
13
14
|
import type { AgentMarker } from "./live-agents";
|
|
@@ -197,7 +198,8 @@ function Zone(props: RepoMapProps & { zone: MapZone }) {
|
|
|
197
198
|
opacity: 0.35,
|
|
198
199
|
},
|
|
199
200
|
header: { flexDirection: "row", alignItems: "center", gap: 6 },
|
|
200
|
-
dir: {
|
|
201
|
+
dir: { flexShrink: 1, color: colors.foregroundMuted, fontFamily: MONO, fontSize: 11 },
|
|
202
|
+
area: { flex: 1, color: colors.foregroundMuted, fontSize: 11, opacity: 0.8 },
|
|
201
203
|
count: { color: hot ? colors.foreground : colors.foregroundMuted, fontSize: 11 },
|
|
202
204
|
tiles: { flexDirection: "row", flexWrap: "wrap", gap: 6 },
|
|
203
205
|
}) as const,
|
|
@@ -207,6 +209,7 @@ function Zone(props: RepoMapProps & { zone: MapZone }) {
|
|
|
207
209
|
let count: string | null = null;
|
|
208
210
|
if (zone.predicted > 0) count = `${zone.hits}/${zone.predicted}${off > 0 ? ` +${off} off` : ""}`;
|
|
209
211
|
else if (off > 0) count = `${off} off the map`;
|
|
212
|
+
const area = zoneAreaLabel(zone);
|
|
210
213
|
return (
|
|
211
214
|
<View style={styles.zone}>
|
|
212
215
|
{hot ? <View pointerEvents="none" style={styles.ring} /> : null}
|
|
@@ -219,6 +222,9 @@ function Zone(props: RepoMapProps & { zone: MapZone }) {
|
|
|
219
222
|
<Text numberOfLines={1} style={styles.dir}>
|
|
220
223
|
{zone.dir || "./"}
|
|
221
224
|
</Text>
|
|
225
|
+
<Text numberOfLines={1} style={styles.area}>
|
|
226
|
+
{area ?? ""}
|
|
227
|
+
</Text>
|
|
222
228
|
{count ? <Text style={styles.count}>{count}</Text> : null}
|
|
223
229
|
</View>
|
|
224
230
|
<View style={styles.tiles}>
|
package/client/live-panel.tsx
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
useMapModel,
|
|
24
24
|
useReduceMotion,
|
|
25
25
|
} from "./live-timeline";
|
|
26
|
-
import { hitRateLine } from "../shared/map-model";
|
|
26
|
+
import { hitRateLine, offMapLine, offMapWhere } from "../shared/map-model";
|
|
27
27
|
import { useIssue } from "./queries";
|
|
28
28
|
import { Button, EmptyState, errorMessage, IconButton, type Theme } from "./ui";
|
|
29
29
|
|
|
@@ -219,7 +219,10 @@ function LiveView(props: BodyProps & { issue: IssueDetail }) {
|
|
|
219
219
|
provider: agent.provider,
|
|
220
220
|
activity,
|
|
221
221
|
});
|
|
222
|
-
const model = useMapModel(agentId, liveMap.map.files, agents.files, agents.dirs);
|
|
222
|
+
const model = useMapModel(agentId, liveMap.map.files, agents.files, agents.dirs, liveMap.areas);
|
|
223
|
+
const offMap = offMapLine(model);
|
|
224
|
+
const currentPath = activity.current?.path ?? null;
|
|
225
|
+
const where = useMemo(() => offMapWhere(model, currentPath, liveMap.areas), [model, currentPath, liveMap.areas]);
|
|
223
226
|
// Hold the map until its first full data, so it does not draw once and then swap.
|
|
224
227
|
const mapShown = useRef(false);
|
|
225
228
|
if (liveMap.ready && model.ready) mapShown.current = true;
|
|
@@ -300,6 +303,7 @@ function LiveView(props: BodyProps & { issue: IssueDetail }) {
|
|
|
300
303
|
main: { flex: 1, minWidth: 0 },
|
|
301
304
|
mainContent: { gap: 16, paddingBottom: 32 },
|
|
302
305
|
hit: { color: colors.foregroundMuted, fontSize: 12 },
|
|
306
|
+
off: { color: colors.statusWarning, fontSize: 12 },
|
|
303
307
|
tools: { flexDirection: "row", alignItems: "center", gap: 12 },
|
|
304
308
|
mapHead: { gap: 8 },
|
|
305
309
|
mapWait: { minHeight: 240, alignItems: "center", justifyContent: "center" },
|
|
@@ -316,6 +320,7 @@ function LiveView(props: BodyProps & { issue: IssueDetail }) {
|
|
|
316
320
|
status={agent.status}
|
|
317
321
|
provider={agent.provider}
|
|
318
322
|
current={activity.current}
|
|
323
|
+
offMap={where}
|
|
319
324
|
reduceMotion={reduceMotion}
|
|
320
325
|
/>
|
|
321
326
|
);
|
|
@@ -351,6 +356,7 @@ function LiveView(props: BodyProps & { issue: IssueDetail }) {
|
|
|
351
356
|
) : (
|
|
352
357
|
<View style={styles.mapHead}>
|
|
353
358
|
<Text style={styles.hit}>{hitRateLine(model)}</Text>
|
|
359
|
+
{offMap ? <Text style={styles.off}>{offMap}</Text> : null}
|
|
354
360
|
{graph ? (
|
|
355
361
|
<LiveGraph
|
|
356
362
|
theme={theme}
|
|
@@ -362,6 +368,7 @@ function LiveView(props: BodyProps & { issue: IssueDetail }) {
|
|
|
362
368
|
progress={progress}
|
|
363
369
|
owners={owners}
|
|
364
370
|
agents={cursors}
|
|
371
|
+
areas={liveMap.areas}
|
|
365
372
|
focus={focus}
|
|
366
373
|
onFocus={toggleFocus}
|
|
367
374
|
camera={props.graphCamera}
|
package/client/live-timeline.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { keepPreviousData, useMutation, useQuery, useQueryClient } from "@tansta
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
4
4
|
import { AccessibilityInfo, Platform } from "react-native";
|
|
5
5
|
import type { DirTouch, FileTouch, TimelineItemLike } from "../shared/activity";
|
|
6
|
+
import { type AreaRef, liveProjectRpc, MAX_RESOLVE_PATHS } from "../shared/knowledge";
|
|
6
7
|
import type { IssueDetail } from "../shared/linear";
|
|
7
8
|
import type { FileLink } from "../shared/links";
|
|
8
9
|
import {
|
|
@@ -240,6 +241,8 @@ export function ownerColors(issue: IssueDetail, accent: string): OwnerColors {
|
|
|
240
241
|
// runs, and slowly while there is no map, since a launch can start a job after this mounts.
|
|
241
242
|
export interface LiveMap {
|
|
242
243
|
map: IssueMap;
|
|
244
|
+
/** Services, packages, and main folders of the agent's project, from its saved knowledge. */
|
|
245
|
+
areas: readonly AreaRef[];
|
|
243
246
|
stored: boolean;
|
|
244
247
|
/** The stored map has been read once, so the map no longer swaps from the fallback. */
|
|
245
248
|
ready: boolean;
|
|
@@ -282,13 +285,14 @@ export function useLiveMap(
|
|
|
282
285
|
const refresh = useCallback(() => void queries.invalidateQueries({ queryKey: ["linear", "live"] }), [queries]);
|
|
283
286
|
const clearMap = useRpc(clearMapRpc);
|
|
284
287
|
const clear = useMutation({ mutationFn: () => clearMap({ agentId }), onSettled: refresh });
|
|
285
|
-
const
|
|
288
|
+
const project = useProjectKnowledge(agentId, issue);
|
|
286
289
|
const stored = query.data?.map ?? null;
|
|
287
290
|
const failure = explore.error ?? clear.error ?? query.error;
|
|
288
291
|
return {
|
|
289
|
-
map: stored ?? fallback,
|
|
292
|
+
map: stored ?? project.fallback,
|
|
293
|
+
areas: project.areas,
|
|
290
294
|
stored: stored !== null,
|
|
291
|
-
ready: query.data !== undefined || query.isError,
|
|
295
|
+
ready: (query.data !== undefined || query.isError) && project.ready,
|
|
292
296
|
job: query.data?.job ?? null,
|
|
293
297
|
starting: explore.isPending,
|
|
294
298
|
error: failure ? errorMessage(failure) : null,
|
|
@@ -299,6 +303,32 @@ export function useLiveMap(
|
|
|
299
303
|
};
|
|
300
304
|
}
|
|
301
305
|
|
|
306
|
+
const NO_AREAS: readonly AreaRef[] = [];
|
|
307
|
+
|
|
308
|
+
/** The project's areas, and the ticket-text map with its paths matched to real files. */
|
|
309
|
+
function useProjectKnowledge(agentId: string, issue: IssueDetail) {
|
|
310
|
+
const read = useRpc(liveProjectRpc);
|
|
311
|
+
const base = useMemo(() => semanticMap(issue), [issue]);
|
|
312
|
+
const paths = useMemo(
|
|
313
|
+
() => base.files.map((file) => file.path).slice(0, MAX_RESOLVE_PATHS).sort(),
|
|
314
|
+
[base],
|
|
315
|
+
);
|
|
316
|
+
const query = useQuery({
|
|
317
|
+
queryKey: ["linear", "live", "project", agentId, paths],
|
|
318
|
+
queryFn: () => read({ agentId, paths }),
|
|
319
|
+
staleTime: 5 * 60_000,
|
|
320
|
+
// Without knowledge the map still works, so a failure shows it at once.
|
|
321
|
+
retry: false,
|
|
322
|
+
});
|
|
323
|
+
const { data } = query;
|
|
324
|
+
const fallback = useMemo(() => {
|
|
325
|
+
if (!data) return base;
|
|
326
|
+
const resolved = new Map(data.resolved.map((entry) => [entry.from, entry.to]));
|
|
327
|
+
return semanticMap(issue, new Date(base.createdAt), { resolved, areas: data.areas });
|
|
328
|
+
}, [issue, base, data]);
|
|
329
|
+
return { fallback, areas: data?.areas ?? NO_AREAS, ready: data !== undefined || query.isError };
|
|
330
|
+
}
|
|
331
|
+
|
|
302
332
|
const NO_LISTING: ReadonlyMap<string, readonly string[]> = new Map();
|
|
303
333
|
|
|
304
334
|
export function useMapModel(
|
|
@@ -306,6 +336,7 @@ export function useMapModel(
|
|
|
306
336
|
files: readonly MapFile[],
|
|
307
337
|
touches: readonly FileTouch[],
|
|
308
338
|
explored: readonly DirTouch[],
|
|
339
|
+
areas: readonly AreaRef[] = NO_AREAS,
|
|
309
340
|
): MapModel & { ready: boolean } {
|
|
310
341
|
const list = useRpc(listFilesRpc);
|
|
311
342
|
const dirs = useMemo(
|
|
@@ -325,8 +356,8 @@ export function useMapModel(
|
|
|
325
356
|
}, [listing.data]);
|
|
326
357
|
const ready = dirs.length === 0 || listing.data !== undefined || listing.isError;
|
|
327
358
|
return useMemo(
|
|
328
|
-
() => ({ ...buildMapModel(files, touches, byDir, explored), ready }),
|
|
329
|
-
[files, touches, byDir, explored, ready],
|
|
359
|
+
() => ({ ...buildMapModel(files, touches, byDir, explored, areas), ready }),
|
|
360
|
+
[files, touches, byDir, explored, areas, ready],
|
|
330
361
|
);
|
|
331
362
|
}
|
|
332
363
|
|
package/client/pill.tsx
CHANGED
|
@@ -17,6 +17,7 @@ import { progressByKey } from "../shared/todo-sync";
|
|
|
17
17
|
import { currentAccess, onAccessChange, publishAccess } from "./access";
|
|
18
18
|
import { KeyScopeProvider } from "./key-scope";
|
|
19
19
|
import { MONO } from "./live-issues";
|
|
20
|
+
import { PluginIcon } from "./plugin-icon";
|
|
20
21
|
import { useIssue } from "./queries";
|
|
21
22
|
import { focusIssue, panelScope } from "./store";
|
|
22
23
|
import { Button, errorMessage, StateIcon, type Theme } from "./ui";
|
|
@@ -144,7 +145,7 @@ function LinkedIssue(props: ContentProps & { open: Open }) {
|
|
|
144
145
|
<Button
|
|
145
146
|
theme={theme}
|
|
146
147
|
size="xs"
|
|
147
|
-
icon=
|
|
148
|
+
icon={PluginIcon}
|
|
148
149
|
label="Open issue"
|
|
149
150
|
onPress={() => {
|
|
150
151
|
focusIssue(panelScope(workspaceId), identifier);
|
|
@@ -337,13 +338,13 @@ export function contributeComposerPills(client: PluginClientContext) {
|
|
|
337
338
|
button: identifier
|
|
338
339
|
? {
|
|
339
340
|
title: `Linear issue ${identifier}`,
|
|
340
|
-
icon:
|
|
341
|
+
icon: PluginIcon,
|
|
341
342
|
label: identifier,
|
|
342
343
|
behavior: { kind: "popover", Content: Linked },
|
|
343
344
|
}
|
|
344
345
|
: {
|
|
345
346
|
title: "Send a Linear issue to this agent",
|
|
346
|
-
icon:
|
|
347
|
+
icon: PluginIcon,
|
|
347
348
|
label: "Linear",
|
|
348
349
|
behavior: { kind: "popover", Content: Search },
|
|
349
350
|
},
|