@yannelli/paseo-linear-plugin 1.0.1 → 1.1.0-beta.1
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 +20 -2
- package/README.md +91 -5
- package/client/access.tsx +66 -0
- package/client/browser.tsx +12 -9
- package/client/graph-camera.tsx +219 -0
- package/client/graph-labels.ts +147 -0
- package/client/launch-choices.ts +15 -4
- package/client/launch-guidance.tsx +86 -0
- package/client/launch-plan.ts +86 -16
- package/client/launch.tsx +19 -5
- package/client/live-agents.tsx +522 -0
- package/client/live-cursor.tsx +293 -0
- package/client/live-feed.tsx +151 -0
- package/client/live-graph.tsx +518 -0
- package/client/live-header.tsx +341 -0
- package/client/live-issues.tsx +345 -0
- package/client/live-map.tsx +345 -0
- package/client/live-panel.tsx +465 -0
- package/client/live-timeline.ts +376 -0
- package/client/pill.tsx +393 -0
- package/client/project-init.tsx +168 -0
- package/client/settings-access.tsx +56 -0
- package/client/settings-live.tsx +145 -0
- package/client/settings-projects.tsx +9 -0
- package/client/tool-buttons.tsx +68 -0
- package/client/web.ts +45 -0
- package/index.client.tsx +39 -1
- package/index.server.ts +29 -5
- package/package.json +1 -1
- package/server/agent-hooks.ts +37 -0
- package/server/agent-runs.ts +312 -0
- package/server/agent-tools.ts +240 -0
- package/server/handlers.ts +12 -1
- package/server/init.ts +65 -0
- package/server/links.ts +153 -0
- package/server/live.ts +360 -0
- package/server/mcp-http.ts +152 -0
- package/server/stores.ts +126 -0
- package/server/subagent-logs.ts +146 -0
- package/server/sync.ts +127 -0
- package/shared/activity.ts +312 -0
- package/shared/agent-hooks.ts +68 -0
- package/shared/agent-tools.ts +130 -0
- package/shared/graph-camera.ts +93 -0
- package/shared/graph-geometry.ts +122 -0
- package/shared/graph-model.ts +319 -0
- package/shared/links.ts +252 -0
- package/shared/live.ts +243 -0
- package/shared/map-model.ts +192 -0
- package/shared/project-setup.ts +55 -0
- package/shared/prompts.ts +65 -2
- package/shared/settings.ts +88 -1
- package/shared/shell-activity.ts +309 -0
- package/shared/shell-lexer.ts +180 -0
- package/shared/subagents.ts +106 -0
- package/shared/todo-sync.ts +158 -0
package/OVERVIEW.md
CHANGED
|
@@ -23,7 +23,25 @@ To use a different Linear key for one Paseo project, add it under **Project keys
|
|
|
23
23
|
|
|
24
24
|
Select **Start agent** or **Start review** on an issue. The setup page shows the prompt and the composer controls for the prompt template, model, thinking level, and mode. The model picker lists providers first, and its search covers all models. Choose the project and where the agent runs: a new worktree, a pull request checkout, the issue branch, an existing agent workspace, or the current workspace or project folder. You can include comments in the prompt. For Implement, you can also move the issue to In Progress and assign it to you.
|
|
25
25
|
|
|
26
|
-
The prompt follows the template until you edit it. **Reset prompt** restores it. Edit the templates in **Prompts**, and add project instructions in **Projects**.
|
|
26
|
+
**Agent guidance** switches add instructions to the prompt: keep Linear updated, have subagents state their issue key, and hand off work to Paseo agents. Each project keeps its choices. With the first and last switches, the agent gets the plugin's Linear tools. It edits the issue description, checks off task list items, and starts one Paseo agent for each sub-issue. It posts no comments. For Claude agents, the switches also add hooks that give subagents the issue keys. The prompt follows the template until you edit it. **Reset prompt** restores it. Edit the templates in **Prompts**, and add project instructions in **Projects**.
|
|
27
|
+
|
|
28
|
+
## Linear Live
|
|
29
|
+
|
|
30
|
+
Open **Linear Live** from the Linear pill in an agent's composer. It shows the issue's sub-issues with their statuses and the agent's todos, a map of the files the agent reads and edits, or a graph of those files joined by their imports and links, with the agent gliding to what it works on, the agents it starts, and its latest commands. It works with Claude, Codex, and other providers. The map comes from paths in the ticket text, or, if you choose it in **Settings → Plugins → Linear → Live**, from an explore agent that reads the repository before the agent starts. The implement agent waits for the map. The explore agent uses provider usage. Buttons on the map reload it, build it again with the explore agent, or clear it. On the graph, a button follows the running agents and zooms to their files. You can also zoom with the buttons, the wheel, or a pinch, and lock the view so the page scrolls. Agents in worktrees of the same project show on the map and the graph. Explore and setup agents are told to only read. The plugin starts Claude in Always Ask and answers the permission requests these agents raise: it allows reads and search commands in the project folder and denies the rest. Codex has no read-only mode, so it runs in its default mode, and the plugin answers only the requests Codex raises.
|
|
31
|
+
|
|
32
|
+
On agents not started from an issue, the Linear pill searches Linear and sends an issue to the agent.
|
|
33
|
+
|
|
34
|
+
## Update Linear from todos
|
|
35
|
+
|
|
36
|
+
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
|
+
|
|
38
|
+
## Set up project prompts
|
|
39
|
+
|
|
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.
|
|
41
|
+
|
|
42
|
+
## Choose projects
|
|
43
|
+
|
|
44
|
+
In **Projects**, **Use Linear in** turns Linear on or off for all projects, and each project has its own switch. In a project where Linear is off, its panels show that Linear is off, its agents get no Linear pill, and the agent setup page does not offer it. Todo sync and the explore agent skip it.
|
|
27
45
|
|
|
28
46
|
## Attach an issue
|
|
29
47
|
|
|
@@ -31,4 +49,4 @@ Choose **Attach Linear issue** in the composer attachment menu. The agent gets t
|
|
|
31
49
|
|
|
32
50
|
## Data and permissions
|
|
33
51
|
|
|
34
|
-
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. 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.
|
|
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.
|
package/README.md
CHANGED
|
@@ -13,6 +13,11 @@ A Paseo plugin for Linear. It adds a Linear screen and a workspace panel where y
|
|
|
13
13
|
- Use a different Linear key for a Paseo project.
|
|
14
14
|
- See saved results at once while the plugin gets new data from Linear.
|
|
15
15
|
- Attach a Linear issue to a message in the composer.
|
|
16
|
+
- Follow an agent in **Linear Live**: sub-issues, todos, and a map of the files it reads and edits.
|
|
17
|
+
- Open the issue from a **Linear** pill in each agent's composer, or send an issue to any agent.
|
|
18
|
+
- Let agent todos move sub-issues to In Progress and Done. This is off until you turn it on.
|
|
19
|
+
- Let an agent read a project's guidelines and propose its instructions and steps.
|
|
20
|
+
- Turn Linear on or off for all projects, or project by project.
|
|
16
21
|
|
|
17
22
|

|
|
18
23
|
|
|
@@ -115,7 +120,15 @@ Select **Start agent** or **Start review** in the issue view. The agent setup pa
|
|
|
115
120
|
|
|
116
121
|
The worktree and checkout options show only for a Git project. When agents run in worktrees (the default), a review uses the pull request, then the implementation workspace, then the issue branch.
|
|
117
122
|
5. Set the options: **Include comments in the prompt**, **Move the issue to In Progress**, and **Assign the issue to me**. The last two show only for Implement, and only when they would change the issue.
|
|
118
|
-
6.
|
|
123
|
+
6. Set the **Agent guidance** switches. Each one adds instructions to the prompt. The project keeps your choices for its next launch.
|
|
124
|
+
- **Keep Linear updated:** the agent edits the issue description as it works. It checks off each task list item it finishes and updates the plan when it changes. It posts no comments. Todos start with their issue key, so todo sync can move each sub-issue.
|
|
125
|
+
- **Subagents state their issue key:** each subagent's description and prompt start with the key of the issue it works on. The subagent states the key again when its work moves to another issue.
|
|
126
|
+
- **Hand off work to Paseo agents:** the agent starts one Paseo agent for each sub-issue instead of its built-in subagents. Each one runs in the same workspace on the same provider and model, with thinking that fits its task, and checks off its own items. Linear Live shows their work.
|
|
127
|
+
|
|
128
|
+
**Keep Linear updated** and **Hand off work to Paseo agents** give the agent the plugin's `linear` MCP server. It has four tools: `read_issue` and `edit_issue` for the issue and its sub-issues, and `start_agent` and `wait_agent` for agents on the sub-issues. Agents that `start_agent` starts get only `read_issue` and `edit_issue`. Paseo allows these tools without a permission prompt.
|
|
129
|
+
|
|
130
|
+
For Claude agents, these switches also add Claude Code hooks. With **Subagents state their issue key** on, each subagent gets the issue keys when it starts. With any switch on, the agent gets the issue again after Claude compacts the conversation. The hooks only print that text.
|
|
131
|
+
7. Select **Start agent** or **Start review**.
|
|
119
132
|
|
|
120
133
|
The plugin starts the agent and then updates Linear. If the Linear update fails, the agent keeps running and a message shows the error. The agent's timeline gets a card that links to the issue. The issue view lists the agents started from it under **Agents on this issue**.
|
|
121
134
|
|
|
@@ -125,11 +138,71 @@ Set the default model, where agents run, the default options, and the Implement
|
|
|
125
138
|
|
|
126
139
|
In the composer, open the attachment menu and choose **Attach Linear issue**. Type an identifier such as `ENG-123`, or words from a title. An empty search lists the 20 most recently updated issues. The agent gets the issue text with your message. The plugin takes this snapshot when you select the issue.
|
|
127
140
|
|
|
141
|
+
### Turn Linear on or off per project
|
|
142
|
+
|
|
143
|
+
In **Settings → Plugins → Linear → Projects**, **Use Linear in** has an **All projects** switch and one switch per Paseo project. A project switch wins over **All projects**. A project without its own switch follows **All projects**. Save to apply.
|
|
144
|
+
|
|
145
|
+
In a project where Linear is off:
|
|
146
|
+
|
|
147
|
+
- The workspace **Linear** panel and **Linear Live** show that Linear is off.
|
|
148
|
+
- Agents get no Linear pill.
|
|
149
|
+
- The agent setup page does not offer the project.
|
|
150
|
+
- Todo sync and the explore agent skip the project's agents.
|
|
151
|
+
|
|
152
|
+
The **Linear** screen in the sidebar is not tied to a project, so it stays available. To turn off the whole plugin, use **Settings → Plugins**.
|
|
153
|
+
|
|
154
|
+
### Linear Live
|
|
155
|
+
|
|
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
|
+
|
|
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 and how many files it links to. Switch between **Map** and **Graph** on the card, or set the default in **Show files as** in the Live settings.
|
|
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
|
+
- **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
|
+
- **Activity:** the latest reads, edits, searches, and commands, with line counts and exit codes.
|
|
164
|
+
|
|
165
|
+
The map comes from one of two sources. Choose it in **Settings → Plugins → Linear → Live**:
|
|
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.
|
|
169
|
+
|
|
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
|
+
|
|
172
|
+
The daemon starts the waiting agent, so you can close the app while the map is made. If the plugin reloads or the daemon restarts during the run, the plugin finishes it when the explore agent's turn ends. A waiting agent that has not started after one hour is dropped.
|
|
173
|
+
|
|
174
|
+
Explore and setup agents are told to only read. The plugin starts them in a mode that asks before tool use when the provider has one: **Always Ask** for Claude, never a bypass mode. When such an agent asks for permission, the plugin answers. It allows file reads inside the project folder and these commands: `ls`, `find`, `rg`, `grep`, `cat`, `head`, `tail`, `wc`, `git ls-files`, and `git grep`. It denies other commands, edits, web requests, redirection, and paths outside the folder. The plugin sees only the requests the provider raises: Claude runs some read commands without asking, and Codex has no read-only mode, so it runs in its default mode and its own sandbox decides what needs a request.
|
|
175
|
+
|
|
176
|
+
For implement agents on an issue with sub-issues, the prompt asks the agent to start each todo with its sub-issue key, such as `ENG-124: add the queue`. That is how todos map to sub-issues.
|
|
177
|
+
|
|
178
|
+
### Linear pill
|
|
179
|
+
|
|
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
|
+
|
|
182
|
+
### Update Linear from agent todos
|
|
183
|
+
|
|
184
|
+
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:
|
|
185
|
+
|
|
186
|
+
- A sub-issue with a todo in progress or done moves to the team's first started status, such as In Progress.
|
|
187
|
+
- A sub-issue whose todos are all done moves to the team's first completed status, such as Done.
|
|
188
|
+
- The parent moves to In Progress when work starts. When every sub-issue is done, it moves to a started status whose name has "review", such as In Review. It never moves to Done by itself.
|
|
189
|
+
- Statuses only move forward. Canceled issues and issues outside the parent and its sub-issues never change.
|
|
190
|
+
|
|
191
|
+
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
|
+
|
|
193
|
+
### Set up project prompts with an agent
|
|
194
|
+
|
|
195
|
+
In **Settings → Plugins → Linear → Projects**, select **Start** under **Set up with an agent**. An 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.
|
|
196
|
+
|
|
128
197
|
## Data and permissions
|
|
129
198
|
|
|
130
199
|
- **Your key stays on the daemon host.** When you save a key, the app sends it to the daemon once. The daemon never sends a key back to the app. The app gets only the last four characters, to show which key is in use. Only the daemon sends requests to `https://api.linear.app/graphql`.
|
|
131
200
|
- **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.
|
|
132
201
|
- **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
|
+
- **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.
|
|
203
|
+
- **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
|
+
- **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
|
+
- **Todo sync:** when it is on, the daemon changes issue statuses in Linear with the key of the project that loaded the issue.
|
|
133
206
|
- **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.
|
|
134
207
|
|
|
135
208
|
Linear errors, such as a rejected key or a rate limit, show in the panel or as a message.
|
|
@@ -138,30 +211,43 @@ Linear errors, such as a rejected key or a rate limit, show in the panel or as a
|
|
|
138
211
|
|
|
139
212
|
| File | Runtime | Role |
|
|
140
213
|
| --- | --- | --- |
|
|
141
|
-
| `index.client.tsx` | App | Registers the Linear screen, the workspace panel, the Account, Prompts, and Projects settings, the command center items, the `/linear` command, the timeline card,
|
|
142
|
-
| `index.server.ts` | Daemon | Registers the settings
|
|
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 |
|
|
215
|
+
| `index.server.ts` | Daemon | Registers the settings, the RPC handlers, and the todo sync hook |
|
|
143
216
|
| `client/browser.tsx` | App | Screen and panel: picks the key, then shows the list, the issue, or the agent setup page |
|
|
144
217
|
| `client/issue-list.tsx`, `client/issue-row.tsx`, `client/issue-tree.ts` | App | Search, filters, sort, rows, status groups, and sub-issue nesting |
|
|
145
218
|
| `client/issue-detail.tsx`, `client/issue-sections.tsx`, `client/breadcrumbs.tsx` | App | Issue view: properties, breadcrumbs, sub-issues, links, agents, and comments |
|
|
146
219
|
| `client/issue-description.tsx`, `client/autosave.ts` | App | Description editor, task list checkboxes, and debounced saves |
|
|
147
220
|
| `client/markdown.tsx` | App | Renders Markdown |
|
|
148
|
-
| `client/launch.tsx`, `client/launch-fields.tsx`, `client/launch-choices.ts`, `client/launch-plan.ts`, `client/agent-options.ts` | App | Agent setup page, Run in options, and agent start |
|
|
221
|
+
| `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 |
|
|
149
222
|
| `client/model-browser.tsx`, `client/provider-icon.tsx` | App | Model picker and provider icons |
|
|
150
223
|
| `client/create-issue.tsx`, `client/pickers.tsx` | App | New issue form and option pickers |
|
|
151
224
|
| `client/connect.tsx`, `client/settings-*.tsx` | App | Key form and settings screens |
|
|
152
225
|
| `client/queries.ts`, `client/store.ts`, `client/key-scope.tsx` | App | Data hooks with saved results, browser state, and the key in use |
|
|
153
226
|
| `client/compat.ts` | App | Registers the screen on Paseo 0.10 and on later releases |
|
|
154
227
|
| `client/timeline-card.tsx`, `client/ui.tsx`, `client/glyphs.tsx` | App | Timeline card, shared controls, and icons |
|
|
228
|
+
| `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
|
+
| `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 |
|
|
231
|
+
| `client/pill.tsx` | App | Linear pill in each agent's composer |
|
|
232
|
+
| `client/project-init.tsx` | App | Project setup proposal in the Projects settings |
|
|
155
233
|
| `shared/linear.ts` | Both | RPC contracts and issue schemas |
|
|
156
234
|
| `shared/issues.ts` | Both | The `issues.search` RPC and the composer attachment source |
|
|
157
235
|
| `shared/settings.ts` | Both | Settings schema: templates, agent defaults, and project settings |
|
|
158
236
|
| `shared/prompts.ts` | Both | Default templates and the issue snapshot |
|
|
159
237
|
| `shared/markdown.ts` | Both | Markdown parser and task list toggle |
|
|
238
|
+
| `shared/live.ts`, `shared/activity.ts`, `shared/subagents.ts` | Both | Linear Live contracts, the ticket text map, timeline activity, and subagent runs |
|
|
239
|
+
| `shared/shell-lexer.ts`, `shared/shell-activity.ts` | Both | Shell command parsing for reads, edits, and searches |
|
|
240
|
+
| `shared/map-model.ts`, `shared/graph-model.ts`, `shared/graph-geometry.ts`, `shared/graph-camera.ts`, `shared/links.ts` | Both | Map folders, the file graph with its layout and paths, label placement, camera math, and the parsers that find links in files |
|
|
241
|
+
| `shared/agent-hooks.ts`, `server/agent-hooks.ts` | Both, Daemon | Claude Code hooks for agent guidance, and the plugin folder the daemon writes |
|
|
242
|
+
| `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
|
+
| `shared/todo-sync.ts`, `shared/project-setup.ts` | Both | Todo keys, status moves, and setup proposals |
|
|
160
244
|
| `server/handlers.ts` | Daemon | RPC handlers, key lookup, and cache use |
|
|
161
245
|
| `server/credentials.ts` | Daemon | Key file and key order |
|
|
162
246
|
| `server/cache.ts` | Daemon | Response cache in memory |
|
|
163
247
|
| `server/graphql.ts`, `server/queries.ts` | Daemon | Linear GraphQL client, queries, and changes |
|
|
164
248
|
| `server/linear.ts` | Daemon | Search and snapshot text for the composer attachment |
|
|
249
|
+
| `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
|
+
| `server/sync.ts`, `server/init.ts` | Daemon | Todo sync after each turn, and project setup runs |
|
|
165
251
|
|
|
166
252
|
The client bundle holds no credentials and makes no calls to Linear.
|
|
167
253
|
|
|
@@ -177,7 +263,7 @@ npm run check
|
|
|
177
263
|
paseo plugin add "$PWD"
|
|
178
264
|
```
|
|
179
265
|
|
|
180
|
-
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, and the release scripts. They use a local GraphQL server and do not call Linear.
|
|
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.
|
|
181
267
|
|
|
182
268
|
## Graphics
|
|
183
269
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type PluginAgentPanelProps, useSettings, useWorkspace } from "@getpaseo/plugin/client";
|
|
2
|
+
import { type ComponentType, type ReactNode, useEffect } from "react";
|
|
3
|
+
import { linearSettings, type ProjectAccess, projectEnabled } from "../shared/settings";
|
|
4
|
+
import { EmptyState, type Theme } from "./ui";
|
|
5
|
+
|
|
6
|
+
// Composer pills live outside React, so components that read settings share the project
|
|
7
|
+
// switches here, and the pill code listens for changes.
|
|
8
|
+
let current: ProjectAccess | null = null;
|
|
9
|
+
const listeners = new Set<(access: ProjectAccess) => void>();
|
|
10
|
+
|
|
11
|
+
export function publishAccess(access: ProjectAccess): void {
|
|
12
|
+
if (current && JSON.stringify(current) === JSON.stringify(access)) return;
|
|
13
|
+
current = access;
|
|
14
|
+
for (const listener of listeners) listener(access);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function currentAccess(): ProjectAccess | null {
|
|
18
|
+
return current;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function onAccessChange(listener: (access: ProjectAccess) => void): () => void {
|
|
22
|
+
listeners.add(listener);
|
|
23
|
+
return () => listeners.delete(listener);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Null while settings load or when the project is unknown, so callers can wait. */
|
|
27
|
+
export function useProjectEnabled(projectId: string | null | undefined): boolean | null {
|
|
28
|
+
const settings = useSettings(linearSettings);
|
|
29
|
+
const access = settings.status === "ready" ? settings.values.access : null;
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (access) publishAccess(access);
|
|
32
|
+
}, [access]);
|
|
33
|
+
if (!access || projectId === undefined) return null;
|
|
34
|
+
return projectEnabled(access, projectId);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function ProjectGate(props: {
|
|
38
|
+
theme: Theme;
|
|
39
|
+
projectId: string | null | undefined;
|
|
40
|
+
children: ReactNode;
|
|
41
|
+
}) {
|
|
42
|
+
const enabled = useProjectEnabled(props.projectId);
|
|
43
|
+
if (enabled !== false) return <>{props.children}</>;
|
|
44
|
+
return (
|
|
45
|
+
<EmptyState
|
|
46
|
+
theme={props.theme}
|
|
47
|
+
icon="SquareKanban"
|
|
48
|
+
title="Linear is off for this project"
|
|
49
|
+
detail="Turn it on in Linear settings, under Projects."
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const selectProjectId = (workspace: { projectId: string }) => workspace.projectId;
|
|
55
|
+
|
|
56
|
+
/** Wraps an agent panel so it shows the off state in projects where Linear is off. */
|
|
57
|
+
export function gateAgentPanel(Panel: ComponentType<PluginAgentPanelProps>) {
|
|
58
|
+
return function GatedAgentPanel(props: PluginAgentPanelProps) {
|
|
59
|
+
const projectId = useWorkspace(props.workspaceId, selectProjectId);
|
|
60
|
+
return (
|
|
61
|
+
<ProjectGate theme={props.theme} projectId={projectId}>
|
|
62
|
+
<Panel {...props} />
|
|
63
|
+
</ProjectGate>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
}
|
package/client/browser.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import { ConnectCard } from "./connect";
|
|
|
11
11
|
import { CreateIssueModal } from "./create-issue";
|
|
12
12
|
import { IssueDetailView } from "./issue-detail";
|
|
13
13
|
import { IssueList } from "./issue-list";
|
|
14
|
+
import { ProjectGate } from "./access";
|
|
14
15
|
import { effectiveKeyScope, KeyScopeProvider } from "./key-scope";
|
|
15
16
|
import { LaunchPage, type LaunchTarget } from "./launch";
|
|
16
17
|
import { useAuthStatus, useCatalog, useProjects } from "./queries";
|
|
@@ -262,14 +263,16 @@ export function LinearPanel({ theme, layout, navigation, workspaceId }: PluginWo
|
|
|
262
263
|
if (teamId) update({ teamId, assignee: "anyone" });
|
|
263
264
|
}, [settings, projectId, update]);
|
|
264
265
|
return (
|
|
265
|
-
<
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
266
|
+
<ProjectGate theme={theme} projectId={projectId}>
|
|
267
|
+
<IssueBrowser
|
|
268
|
+
theme={theme}
|
|
269
|
+
compact={layout.compact}
|
|
270
|
+
navigation={navigation}
|
|
271
|
+
scope={scope}
|
|
272
|
+
target={target}
|
|
273
|
+
keyProjectId={projectId ?? null}
|
|
274
|
+
canSwitchKey={false}
|
|
275
|
+
/>
|
|
276
|
+
</ProjectGate>
|
|
274
277
|
);
|
|
275
278
|
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { type RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { Animated, Easing, type GestureResponderHandlers, PanResponder, View } from "react-native";
|
|
3
|
+
import { type Box, type Camera, clampCamera, FIT, frameBox, MAX_ZOOM, MIN_ZOOM, zoomAt } from "../shared/graph-camera";
|
|
4
|
+
import { NATIVE_DRIVER } from "./live-timeline";
|
|
5
|
+
import { ToolButton, ToolGroup } from "./tool-buttons";
|
|
6
|
+
import type { Theme } from "./ui";
|
|
7
|
+
import { listenWheel } from "./web";
|
|
8
|
+
|
|
9
|
+
// Animated values move the graph canvas, so a wheel or a drag never renders the graph again.
|
|
10
|
+
// Nodes, edges, and cursors scale by the inverse zoom, so they keep their size on screen.
|
|
11
|
+
|
|
12
|
+
export interface CameraOptions {
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
/** Where auto mode looks; null shows the whole graph. */
|
|
16
|
+
focus: Box | null;
|
|
17
|
+
auto: boolean;
|
|
18
|
+
/** Wheel and drag move the page, not the camera. */
|
|
19
|
+
locked: boolean;
|
|
20
|
+
reduce: boolean;
|
|
21
|
+
/** The user moved the camera, so auto mode stops. */
|
|
22
|
+
onManual(): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type ZoomLimit = "min" | "max" | null;
|
|
26
|
+
|
|
27
|
+
export interface CameraControl {
|
|
28
|
+
ref: RefObject<View | null>;
|
|
29
|
+
handlers: GestureResponderHandlers;
|
|
30
|
+
transform: [{ translateX: Animated.Value }, { translateY: Animated.Value }, { scale: Animated.Value }];
|
|
31
|
+
/** One over the zoom: the scale that keeps a canvas item its own size on screen. */
|
|
32
|
+
inverse: Animated.AnimatedInterpolation<number>;
|
|
33
|
+
limit: ZoomLimit;
|
|
34
|
+
/** The camera once it stops moving; labels are placed for it. */
|
|
35
|
+
settled: Camera;
|
|
36
|
+
zoomBy(factor: number): void;
|
|
37
|
+
fit(): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ZOOM_STEP = 1.5;
|
|
41
|
+
const DRAG_START = 6;
|
|
42
|
+
const SETTLE_MS = 160;
|
|
43
|
+
|
|
44
|
+
type Touch = { pageX: number; pageY: number };
|
|
45
|
+
const spreadOf = (touches: readonly Touch[]) => {
|
|
46
|
+
const [a, b] = touches;
|
|
47
|
+
return a && b ? Math.hypot(a.pageX - b.pageX, a.pageY - b.pageY) : null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function useGraphCamera(options: CameraOptions): CameraControl {
|
|
51
|
+
const [values] = useState(() => ({ x: new Animated.Value(0), y: new Animated.Value(0), zoom: new Animated.Value(1) }));
|
|
52
|
+
const [inverse] = useState(() => Animated.divide<number>(new Animated.Value(1), values.zoom));
|
|
53
|
+
const [limit, setLimit] = useState<ZoomLimit>("min");
|
|
54
|
+
const [settled, setSettled] = useState<Camera>(FIT);
|
|
55
|
+
const pause = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
56
|
+
const camera = useRef<Camera>(FIT);
|
|
57
|
+
const ref = useRef<View>(null);
|
|
58
|
+
const latest = useRef(options);
|
|
59
|
+
latest.current = options;
|
|
60
|
+
|
|
61
|
+
const apply = useCallback(
|
|
62
|
+
(next: Camera, animate: boolean) => {
|
|
63
|
+
const { width, height, reduce } = latest.current;
|
|
64
|
+
const target = clampCamera(next, width, height);
|
|
65
|
+
camera.current = target;
|
|
66
|
+
setLimit(target.zoom <= MIN_ZOOM ? "min" : target.zoom >= MAX_ZOOM ? "max" : null);
|
|
67
|
+
for (const value of [values.x, values.y, values.zoom]) value.stopAnimation();
|
|
68
|
+
if (pause.current) clearTimeout(pause.current);
|
|
69
|
+
if (!animate || reduce) {
|
|
70
|
+
values.x.setValue(target.x);
|
|
71
|
+
values.y.setValue(target.y);
|
|
72
|
+
values.zoom.setValue(target.zoom);
|
|
73
|
+
pause.current = setTimeout(() => setSettled(target), SETTLE_MS);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const glide = (value: Animated.Value, toValue: number) =>
|
|
77
|
+
Animated.timing(value, { toValue, duration: 450, easing: Easing.inOut(Easing.cubic), useNativeDriver: NATIVE_DRIVER });
|
|
78
|
+
Animated.parallel([glide(values.x, target.x), glide(values.y, target.y), glide(values.zoom, target.zoom)]).start(
|
|
79
|
+
({ finished }) => {
|
|
80
|
+
if (finished) setSettled(target);
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
},
|
|
84
|
+
[values],
|
|
85
|
+
);
|
|
86
|
+
const move = useCallback(
|
|
87
|
+
(next: Camera, animate: boolean) => {
|
|
88
|
+
if (latest.current.auto) latest.current.onManual();
|
|
89
|
+
apply(next, animate);
|
|
90
|
+
},
|
|
91
|
+
[apply],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
useEffect(
|
|
95
|
+
() => () => {
|
|
96
|
+
if (pause.current) clearTimeout(pause.current);
|
|
97
|
+
},
|
|
98
|
+
[],
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const { width, height, auto, locked, focus } = options;
|
|
102
|
+
const focusKey = focus ? [focus.left, focus.top, focus.right, focus.bottom].map(Math.round).join(",") : "";
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (width === 0 || !auto) return;
|
|
105
|
+
const box = latest.current.focus;
|
|
106
|
+
apply(box ? frameBox(box, width, height) : FIT, true);
|
|
107
|
+
}, [auto, focusKey, width, height, apply]);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (width > 0 && !latest.current.auto) apply(camera.current, false);
|
|
110
|
+
}, [width, height, apply]);
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (locked) return;
|
|
114
|
+
return listenWheel(ref.current, (point, factor) => {
|
|
115
|
+
const size = latest.current;
|
|
116
|
+
move(zoomAt(camera.current, point, factor, size.width, size.height), false);
|
|
117
|
+
});
|
|
118
|
+
}, [locked, move]);
|
|
119
|
+
|
|
120
|
+
// A drag pans once zoomed in; at full view it scrolls the page. Two fingers pinch to zoom.
|
|
121
|
+
const handlers = useMemo(() => {
|
|
122
|
+
let start = FIT;
|
|
123
|
+
let origin = { x: 0, y: 0 };
|
|
124
|
+
let fingers = 0;
|
|
125
|
+
let spread: number | null = null;
|
|
126
|
+
let lead = { x: 0, y: 0 };
|
|
127
|
+
const follow = (touches: readonly Touch[], gesture: { dx: number; dy: number }) => {
|
|
128
|
+
if (Math.min(2, touches.length) !== fingers) {
|
|
129
|
+
fingers = Math.min(2, touches.length);
|
|
130
|
+
start = camera.current;
|
|
131
|
+
origin = { x: gesture.dx, y: gesture.dy };
|
|
132
|
+
spread = spreadOf(touches);
|
|
133
|
+
}
|
|
134
|
+
const { width: w, height: h } = latest.current;
|
|
135
|
+
const now = spreadOf(touches);
|
|
136
|
+
const base = now && spread ? zoomAt(start, { x: w / 2, y: h / 2 }, now / spread, w, h) : start;
|
|
137
|
+
move({ ...base, x: base.x + gesture.dx - origin.x, y: base.y + gesture.dy - origin.y }, false);
|
|
138
|
+
};
|
|
139
|
+
return PanResponder.create({
|
|
140
|
+
onMoveShouldSetPanResponder: (_event, gesture) => {
|
|
141
|
+
if (latest.current.locked) return false;
|
|
142
|
+
lead = { x: gesture.dx, y: gesture.dy };
|
|
143
|
+
if (gesture.numberActiveTouches >= 2) return true;
|
|
144
|
+
return camera.current.zoom > MIN_ZOOM && Math.hypot(gesture.dx, gesture.dy) > DRAG_START;
|
|
145
|
+
},
|
|
146
|
+
// The grant restarts dx at zero, so the move before it is added back.
|
|
147
|
+
onPanResponderGrant: (event) => {
|
|
148
|
+
const touches = event.nativeEvent.touches;
|
|
149
|
+
fingers = Math.min(2, touches.length);
|
|
150
|
+
start = camera.current;
|
|
151
|
+
origin = { x: -lead.x, y: -lead.y };
|
|
152
|
+
spread = spreadOf(touches);
|
|
153
|
+
},
|
|
154
|
+
onPanResponderMove: (event, gesture) => follow(event.nativeEvent.touches, gesture),
|
|
155
|
+
onPanResponderTerminationRequest: () => false,
|
|
156
|
+
}).panHandlers;
|
|
157
|
+
}, [move]);
|
|
158
|
+
|
|
159
|
+
const transform = useMemo(
|
|
160
|
+
(): CameraControl["transform"] => [{ translateX: values.x }, { translateY: values.y }, { scale: values.zoom }],
|
|
161
|
+
[values],
|
|
162
|
+
);
|
|
163
|
+
const zoomBy = useCallback(
|
|
164
|
+
(factor: number) => {
|
|
165
|
+
const size = latest.current;
|
|
166
|
+
move(zoomAt(camera.current, { x: size.width / 2, y: size.height / 2 }, factor, size.width, size.height), true);
|
|
167
|
+
},
|
|
168
|
+
[move],
|
|
169
|
+
);
|
|
170
|
+
const fit = useCallback(() => move(FIT, true), [move]);
|
|
171
|
+
return { ref, handlers, transform, inverse, limit, settled, zoomBy, fit };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function CameraButtons(props: {
|
|
175
|
+
theme: Theme;
|
|
176
|
+
camera: CameraControl;
|
|
177
|
+
auto: boolean;
|
|
178
|
+
locked: boolean;
|
|
179
|
+
onAuto(): void;
|
|
180
|
+
onLock(): void;
|
|
181
|
+
}) {
|
|
182
|
+
const { theme, camera, auto, locked } = props;
|
|
183
|
+
const style = useMemo(() => ({ position: "absolute", top: 8, right: 8 }) as const, []);
|
|
184
|
+
return (
|
|
185
|
+
<View style={style}>
|
|
186
|
+
<ToolGroup theme={theme}>
|
|
187
|
+
<ToolButton
|
|
188
|
+
theme={theme}
|
|
189
|
+
icon="LocateFixed"
|
|
190
|
+
label={auto ? "Stop following the running agents" : "Follow the running agents"}
|
|
191
|
+
active={auto}
|
|
192
|
+
onPress={props.onAuto}
|
|
193
|
+
/>
|
|
194
|
+
<ToolButton theme={theme} icon="Maximize" label="Show the whole graph" onPress={camera.fit} />
|
|
195
|
+
<ToolButton
|
|
196
|
+
theme={theme}
|
|
197
|
+
icon="Minus"
|
|
198
|
+
label="Zoom out"
|
|
199
|
+
disabled={camera.limit === "min"}
|
|
200
|
+
onPress={() => camera.zoomBy(1 / ZOOM_STEP)}
|
|
201
|
+
/>
|
|
202
|
+
<ToolButton
|
|
203
|
+
theme={theme}
|
|
204
|
+
icon="Plus"
|
|
205
|
+
label="Zoom in"
|
|
206
|
+
disabled={camera.limit === "max"}
|
|
207
|
+
onPress={() => camera.zoomBy(ZOOM_STEP)}
|
|
208
|
+
/>
|
|
209
|
+
<ToolButton
|
|
210
|
+
theme={theme}
|
|
211
|
+
icon={locked ? "Lock" : "LockOpen"}
|
|
212
|
+
label={locked ? "Let the wheel and drags move the graph" : "Let the wheel and drags scroll the page"}
|
|
213
|
+
active={locked}
|
|
214
|
+
onPress={props.onLock}
|
|
215
|
+
/>
|
|
216
|
+
</ToolGroup>
|
|
217
|
+
</View>
|
|
218
|
+
);
|
|
219
|
+
}
|