@yannelli/paseo-linear-plugin 1.1.0-beta.0 → 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 +3 -3
- package/README.md +26 -10
- package/client/graph-camera.tsx +219 -0
- package/client/graph-labels.ts +14 -4
- package/client/launch-guidance.tsx +86 -0
- package/client/launch-plan.ts +50 -2
- package/client/launch.tsx +10 -1
- package/client/live-agents.tsx +138 -23
- package/client/live-cursor.tsx +3 -1
- package/client/live-graph.tsx +159 -75
- package/client/live-header.tsx +29 -0
- package/client/live-panel.tsx +29 -8
- package/client/live-timeline.ts +38 -3
- package/client/settings-live.tsx +25 -1
- package/client/tool-buttons.tsx +68 -0
- package/client/web.ts +45 -0
- package/index.server.ts +5 -1
- package/package.json +1 -1
- package/server/agent-hooks.ts +37 -0
- package/server/agent-runs.ts +15 -2
- package/server/agent-tools.ts +240 -0
- package/server/init.ts +1 -0
- package/server/live.ts +27 -1
- package/server/mcp-http.ts +152 -0
- package/server/stores.ts +8 -1
- package/server/subagent-logs.ts +146 -0
- package/shared/activity.ts +5 -1
- package/shared/agent-hooks.ts +68 -0
- package/shared/agent-tools.ts +130 -0
- package/shared/graph-camera.ts +93 -0
- package/shared/live.ts +43 -0
- package/shared/prompts.ts +49 -1
- package/shared/settings.ts +31 -0
package/OVERVIEW.md
CHANGED
|
@@ -23,11 +23,11 @@ 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
27
|
|
|
28
28
|
## Linear Live
|
|
29
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. 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.
|
|
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
31
|
|
|
32
32
|
On agents not started from an issue, the Linear pill searches Linear and sends an issue to the agent.
|
|
33
33
|
|
|
@@ -49,4 +49,4 @@ Choose **Attach Linear issue** in the composer attachment menu. The agent gets t
|
|
|
49
49
|
|
|
50
50
|
## Data and permissions
|
|
51
51
|
|
|
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. 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.
|
|
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
|
@@ -120,7 +120,15 @@ Select **Start agent** or **Start review** in the issue view. The agent setup pa
|
|
|
120
120
|
|
|
121
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.
|
|
122
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.
|
|
123
|
-
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**.
|
|
124
132
|
|
|
125
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**.
|
|
126
134
|
|
|
@@ -149,14 +157,17 @@ Linear Live is an agent panel. Open it from the Linear pill, or with **Linear: O
|
|
|
149
157
|
|
|
150
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.
|
|
151
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.
|
|
152
|
-
- **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. 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.
|
|
153
|
-
- **
|
|
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.
|
|
154
163
|
- **Activity:** the latest reads, edits, searches, and commands, with line counts and exit codes.
|
|
155
164
|
|
|
156
165
|
The map comes from one of two sources. Choose it in **Settings → Plugins → Linear → Live**:
|
|
157
166
|
|
|
158
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.
|
|
159
|
-
- **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 in the Live settings. A faster, cheaper model is usually enough.
|
|
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.
|
|
160
171
|
|
|
161
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.
|
|
162
173
|
|
|
@@ -188,7 +199,9 @@ In **Settings → Plugins → Linear → Projects**, select **Start** under **Se
|
|
|
188
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`.
|
|
189
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.
|
|
190
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.
|
|
191
|
-
- **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.
|
|
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.
|
|
192
205
|
- **Todo sync:** when it is on, the daemon changes issue statuses in Linear with the key of the project that loaded the issue.
|
|
193
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.
|
|
194
207
|
|
|
@@ -205,7 +218,7 @@ Linear errors, such as a rejected key or a rate limit, show in the panel or as a
|
|
|
205
218
|
| `client/issue-detail.tsx`, `client/issue-sections.tsx`, `client/breadcrumbs.tsx` | App | Issue view: properties, breadcrumbs, sub-issues, links, agents, and comments |
|
|
206
219
|
| `client/issue-description.tsx`, `client/autosave.ts` | App | Description editor, task list checkboxes, and debounced saves |
|
|
207
220
|
| `client/markdown.tsx` | App | Renders Markdown |
|
|
208
|
-
| `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 |
|
|
209
222
|
| `client/model-browser.tsx`, `client/provider-icon.tsx` | App | Model picker and provider icons |
|
|
210
223
|
| `client/create-issue.tsx`, `client/pickers.tsx` | App | New issue form and option pickers |
|
|
211
224
|
| `client/connect.tsx`, `client/settings-*.tsx` | App | Key form and settings screens |
|
|
@@ -213,7 +226,8 @@ Linear errors, such as a rejected key or a rate limit, show in the panel or as a
|
|
|
213
226
|
| `client/compat.ts` | App | Registers the screen on Paseo 0.10 and on later releases |
|
|
214
227
|
| `client/timeline-card.tsx`, `client/ui.tsx`, `client/glyphs.tsx` | App | Timeline card, shared controls, and icons |
|
|
215
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 |
|
|
216
|
-
| `client/live-map.tsx`, `client/live-graph.tsx`, `client/graph-labels.ts`, `client/live-cursor.tsx`, `client/live-agents.tsx` | App | File map, file graph
|
|
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 |
|
|
217
231
|
| `client/pill.tsx` | App | Linear pill in each agent's composer |
|
|
218
232
|
| `client/project-init.tsx` | App | Project setup proposal in the Projects settings |
|
|
219
233
|
| `shared/linear.ts` | Both | RPC contracts and issue schemas |
|
|
@@ -223,14 +237,16 @@ Linear errors, such as a rejected key or a rate limit, show in the panel or as a
|
|
|
223
237
|
| `shared/markdown.ts` | Both | Markdown parser and task list toggle |
|
|
224
238
|
| `shared/live.ts`, `shared/activity.ts`, `shared/subagents.ts` | Both | Linear Live contracts, the ticket text map, timeline activity, and subagent runs |
|
|
225
239
|
| `shared/shell-lexer.ts`, `shared/shell-activity.ts` | Both | Shell command parsing for reads, edits, and searches |
|
|
226
|
-
| `shared/map-model.ts`, `shared/graph-model.ts`, `shared/graph-geometry.ts`, `shared/links.ts` | Both | Map folders, the file graph with its layout and paths, label placement, and the parsers that find links in files |
|
|
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 |
|
|
227
243
|
| `shared/todo-sync.ts`, `shared/project-setup.ts` | Both | Todo keys, status moves, and setup proposals |
|
|
228
244
|
| `server/handlers.ts` | Daemon | RPC handlers, key lookup, and cache use |
|
|
229
245
|
| `server/credentials.ts` | Daemon | Key file and key order |
|
|
230
246
|
| `server/cache.ts` | Daemon | Response cache in memory |
|
|
231
247
|
| `server/graphql.ts`, `server/queries.ts` | Daemon | Linear GraphQL client, queries, and changes |
|
|
232
248
|
| `server/linear.ts` | Daemon | Search and snapshot text for the composer attachment |
|
|
233
|
-
| `server/live.ts`, `server/links.ts`, `server/agent-runs.ts` | Daemon | File listing, links between files, explore runs, and the saved maps |
|
|
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 |
|
|
234
250
|
| `server/sync.ts`, `server/init.ts` | Daemon | Todo sync after each turn, and project setup runs |
|
|
235
251
|
|
|
236
252
|
The client bundle holds no credentials and makes no calls to Linear.
|
|
@@ -247,7 +263,7 @@ npm run check
|
|
|
247
263
|
paseo plugin add "$PWD"
|
|
248
264
|
```
|
|
249
265
|
|
|
250
|
-
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 links between files in many languages, the graph layout, 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.
|
|
251
267
|
|
|
252
268
|
## Graphics
|
|
253
269
|
|
|
@@ -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
|
+
}
|
package/client/graph-labels.ts
CHANGED
|
@@ -50,6 +50,8 @@ export interface LabelInput {
|
|
|
50
50
|
cursors: readonly { at: Point; caption: number; flip: boolean }[];
|
|
51
51
|
width: number;
|
|
52
52
|
height: number;
|
|
53
|
+
/** Zoomed in, so files no agent touched get labels too when they fit. */
|
|
54
|
+
every?: boolean;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
function linesOf(node: GraphNode, input: LabelInput): readonly [string, string | null] {
|
|
@@ -68,10 +70,18 @@ const textWidth = (text: string, perChar: number) => Math.min(MAX_LABEL, text.le
|
|
|
68
70
|
export function graphLabels(input: LabelInput): { nodes: Map<string, Label>; folders: FolderLabel[] } {
|
|
69
71
|
const { graph, positions, selected, working } = input;
|
|
70
72
|
const order = (node: GraphNode) =>
|
|
71
|
-
node.id === selected
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
node.id === selected
|
|
74
|
+
? 0
|
|
75
|
+
: node.kind === "issue"
|
|
76
|
+
? 1
|
|
77
|
+
: node.kind === "subissue"
|
|
78
|
+
? 2
|
|
79
|
+
: working.has(node.id)
|
|
80
|
+
? 4
|
|
81
|
+
: (node.tile?.touch ?? "none") !== "none"
|
|
82
|
+
? 5
|
|
83
|
+
: 6;
|
|
84
|
+
const wanted = graph.nodes.filter((node) => input.every || order(node) < 6).sort((a, b) => order(a) - order(b));
|
|
75
85
|
const lines = new Map<string, readonly [string, string | null]>();
|
|
76
86
|
const requests: LabelRequest[] = [];
|
|
77
87
|
const push = (node: GraphNode) => {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useSettings } from "@getpaseo/plugin/client";
|
|
2
|
+
import { useCallback, useMemo, useState } from "react";
|
|
3
|
+
import { Switch, Text, View } from "react-native";
|
|
4
|
+
import {
|
|
5
|
+
GUIDANCE_KEYS,
|
|
6
|
+
type Guidance,
|
|
7
|
+
GuidanceSchema,
|
|
8
|
+
linearSettings,
|
|
9
|
+
type ProjectConfig,
|
|
10
|
+
withProjectGuidance,
|
|
11
|
+
} from "../shared/settings";
|
|
12
|
+
import type { PaseoProjectOption } from "./queries";
|
|
13
|
+
import type { Theme } from "./ui";
|
|
14
|
+
|
|
15
|
+
// Launch options that add instructions to the prompt. Each project keeps its own choices, so
|
|
16
|
+
// the next launch in the project starts with them.
|
|
17
|
+
|
|
18
|
+
const DEFAULT_GUIDANCE = GuidanceSchema.parse({});
|
|
19
|
+
|
|
20
|
+
const COPY: Record<(typeof GUIDANCE_KEYS)[number], { label: string; hint: string }> = {
|
|
21
|
+
updateLinear: {
|
|
22
|
+
label: "Keep Linear updated",
|
|
23
|
+
hint: "The agent edits the issue description and checks off task list items as it works, with the plugin's Linear tools. It posts no comments. Todos start with issue keys, so status sync can move each sub-issue.",
|
|
24
|
+
},
|
|
25
|
+
subagentKeys: {
|
|
26
|
+
label: "Subagents state their issue key",
|
|
27
|
+
hint: "Subagents open with the key of the issue they work on and state the new key when it changes. Claude agents also get a hook that tells each subagent.",
|
|
28
|
+
},
|
|
29
|
+
paseoSubagents: {
|
|
30
|
+
label: "Hand off work to Paseo agents",
|
|
31
|
+
hint: "The agent starts one Paseo agent for each sub-issue with the plugin's tools, on its own provider and model. Each one checks off its own items, and Linear Live shows its work.",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** The project's guidance, changed locally at once and saved to the project in the background. */
|
|
36
|
+
export function useProjectGuidance(project: PaseoProjectOption | null, config: ProjectConfig | null) {
|
|
37
|
+
const settings = useSettings(linearSettings);
|
|
38
|
+
const [edits, setEdits] = useState<Record<string, Guidance>>({});
|
|
39
|
+
const guidance = (project ? edits[project.projectId] : undefined) ?? config?.guidance ?? DEFAULT_GUIDANCE;
|
|
40
|
+
const change = useCallback(
|
|
41
|
+
(key: (typeof GUIDANCE_KEYS)[number], value: boolean) => {
|
|
42
|
+
if (!project) return;
|
|
43
|
+
const next = { ...guidance, [key]: value };
|
|
44
|
+
setEdits((current) => ({ ...current, [project.projectId]: next }));
|
|
45
|
+
if (settings.status === "ready") void settings.save(withProjectGuidance(settings.values, project, next), settings.revision);
|
|
46
|
+
},
|
|
47
|
+
[project, guidance, settings],
|
|
48
|
+
);
|
|
49
|
+
return { guidance, change };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function GuidanceToggles(props: {
|
|
53
|
+
theme: Theme;
|
|
54
|
+
guidance: Guidance;
|
|
55
|
+
onChange(key: (typeof GUIDANCE_KEYS)[number], value: boolean): void;
|
|
56
|
+
}) {
|
|
57
|
+
const { colors } = props.theme;
|
|
58
|
+
const styles = useMemo(
|
|
59
|
+
() => ({
|
|
60
|
+
row: { flexDirection: "row" as const, alignItems: "center" as const, gap: 10, paddingVertical: 4 },
|
|
61
|
+
body: { flex: 1, gap: 2 },
|
|
62
|
+
label: { color: colors.foreground, fontSize: 14 },
|
|
63
|
+
hint: { color: colors.foregroundMuted, fontSize: 12 },
|
|
64
|
+
track: { true: colors.accent, false: colors.surface2 },
|
|
65
|
+
}),
|
|
66
|
+
[colors],
|
|
67
|
+
);
|
|
68
|
+
return (
|
|
69
|
+
<View>
|
|
70
|
+
{GUIDANCE_KEYS.map((key) => (
|
|
71
|
+
<View key={key} style={styles.row}>
|
|
72
|
+
<View style={styles.body}>
|
|
73
|
+
<Text style={styles.label}>{COPY[key].label}</Text>
|
|
74
|
+
<Text style={styles.hint}>{COPY[key].hint}</Text>
|
|
75
|
+
</View>
|
|
76
|
+
<Switch
|
|
77
|
+
accessibilityLabel={COPY[key].label}
|
|
78
|
+
value={props.guidance[key]}
|
|
79
|
+
onValueChange={(value) => props.onChange(key, value)}
|
|
80
|
+
trackColor={styles.track}
|
|
81
|
+
/>
|
|
82
|
+
</View>
|
|
83
|
+
))}
|
|
84
|
+
</View>
|
|
85
|
+
);
|
|
86
|
+
}
|
package/client/launch-plan.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RpcOutput } from "@getpaseo/plugin";
|
|
1
2
|
import { usePaseo, useRpc } from "@getpaseo/plugin/client";
|
|
2
3
|
import { useMutation } from "@tanstack/react-query";
|
|
3
4
|
import {
|
|
@@ -7,9 +8,11 @@ import {
|
|
|
7
8
|
updateIssueRpc,
|
|
8
9
|
type WorkflowState,
|
|
9
10
|
} from "../shared/linear";
|
|
11
|
+
import { agentHooksRpc } from "../shared/agent-hooks";
|
|
12
|
+
import { agentToolsRpc, TOOLS_SERVER } from "../shared/agent-tools";
|
|
10
13
|
import { launchAfterExploreRpc } from "../shared/live";
|
|
11
14
|
import { AGENT_LABELS, agentTitle } from "../shared/prompts";
|
|
12
|
-
import type { AgentAction, Isolation, LinearSettings } from "../shared/settings";
|
|
15
|
+
import type { AgentAction, Guidance, Isolation, LinearSettings } from "../shared/settings";
|
|
13
16
|
import { type AgentSelection, agentConfig } from "./agent-options";
|
|
14
17
|
import { rememberLaunch } from "./agent-preferences";
|
|
15
18
|
import type { PaseoProjectOption } from "./queries";
|
|
@@ -144,6 +147,25 @@ export interface LaunchRequest {
|
|
|
144
147
|
keyScope: string | null;
|
|
145
148
|
/** Maps the files with the read-only explore agent first, then starts the agent. */
|
|
146
149
|
explore: boolean;
|
|
150
|
+
/** Guidance options; for Claude some also add hooks. */
|
|
151
|
+
guidance: Guidance;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
type AgentTools = RpcOutput<typeof agentToolsRpc>;
|
|
155
|
+
|
|
156
|
+
/** The config with the plugin's MCP server, whose token only reaches this issue tree. */
|
|
157
|
+
export function withLinearTools<C extends object>(config: C, tools: Pick<AgentTools, "server" | "toolPolicy"> | null) {
|
|
158
|
+
if (!tools) return config;
|
|
159
|
+
return { ...config, mcpServers: { [TOOLS_SERVER]: tools.server }, toolPolicy: tools.toolPolicy };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The issue's guidance needs the MCP tools: Linear edits, or agents started on sub-issues. */
|
|
163
|
+
export const needsLinearTools = (guidance: Guidance) => guidance.updateLinear || guidance.paseoSubagents;
|
|
164
|
+
|
|
165
|
+
/** The config with the issue's Claude Code hooks, loaded as a plugin with --plugin-dir. */
|
|
166
|
+
export function withHookPlugin<C extends object>(config: C, pluginDir: string | null) {
|
|
167
|
+
if (!pluginDir) return config;
|
|
168
|
+
return { ...config, providerOptions: { extraArgs: { "plugin-dir": pluginDir } } };
|
|
147
169
|
}
|
|
148
170
|
|
|
149
171
|
export type Paseo = ReturnType<typeof usePaseo>;
|
|
@@ -217,6 +239,8 @@ export function useLaunchAgent() {
|
|
|
217
239
|
const updateIssue = useRpc(updateIssueRpc);
|
|
218
240
|
const attachCard = useRpc(attachIssueCardRpc);
|
|
219
241
|
const launchAfterExplore = useRpc(launchAfterExploreRpc);
|
|
242
|
+
const prepareHooks = useRpc(agentHooksRpc);
|
|
243
|
+
const prepareTools = useRpc(agentToolsRpc);
|
|
220
244
|
return useMutation({
|
|
221
245
|
mutationFn: async (request: LaunchRequest) => {
|
|
222
246
|
const { issue, action } = request;
|
|
@@ -230,14 +254,38 @@ export function useLaunchAgent() {
|
|
|
230
254
|
stateColor: state.color,
|
|
231
255
|
action,
|
|
232
256
|
};
|
|
257
|
+
// Hooks are a Claude Code feature; a failure to write them never blocks the launch.
|
|
258
|
+
const hooks =
|
|
259
|
+
request.agent.agent.id === "claude"
|
|
260
|
+
? await prepareHooks({
|
|
261
|
+
issue: {
|
|
262
|
+
identifier: issue.identifier,
|
|
263
|
+
title: issue.title,
|
|
264
|
+
children: issue.children.map(({ identifier, title }) => ({ identifier, title })),
|
|
265
|
+
},
|
|
266
|
+
guidance: request.guidance,
|
|
267
|
+
}).catch(() => null)
|
|
268
|
+
: null;
|
|
269
|
+
const config = agentConfig(request.agent);
|
|
270
|
+
// Without the tools the agent still starts; its prompt then names tools it does not have.
|
|
271
|
+
const tools = needsLinearTools(request.guidance)
|
|
272
|
+
? await prepareTools({
|
|
273
|
+
root: issue.identifier,
|
|
274
|
+
home: issue.identifier,
|
|
275
|
+
keyScope: request.keyScope,
|
|
276
|
+
workspaceId: workspace.id,
|
|
277
|
+
config,
|
|
278
|
+
}).catch(() => null)
|
|
279
|
+
: null;
|
|
233
280
|
const agent = {
|
|
234
|
-
config:
|
|
281
|
+
config: withLinearTools(withHookPlugin(config, hooks?.pluginDir ?? null), tools),
|
|
235
282
|
title: agentTitle(action, issue),
|
|
236
283
|
prompt: request.prompt,
|
|
237
284
|
labels: {
|
|
238
285
|
[AGENT_LABELS.issue]: issue.identifier,
|
|
239
286
|
[AGENT_LABELS.action]: action,
|
|
240
287
|
...(request.keyScope ? { [AGENT_LABELS.project]: request.keyScope } : {}),
|
|
288
|
+
...(tools ? { [AGENT_LABELS.tools]: tools.grantId } : {}),
|
|
241
289
|
},
|
|
242
290
|
};
|
|
243
291
|
// The daemon starts the agent once the map is ready, so agentId is null until then.
|
package/client/launch.tsx
CHANGED
|
@@ -15,6 +15,7 @@ import { modeIcon, optionLabel } from "./agent-options";
|
|
|
15
15
|
import { useKeyScope } from "./key-scope";
|
|
16
16
|
import { type LaunchChoices, useLaunchChoices } from "./launch-choices";
|
|
17
17
|
import { Chip, Field, Toggle } from "./launch-fields";
|
|
18
|
+
import { GuidanceToggles, useProjectGuidance } from "./launch-guidance";
|
|
18
19
|
import { linearPatch, type TargetWorkspace, useLaunchAgent } from "./launch-plan";
|
|
19
20
|
import { ModelBrowser } from "./model-browser";
|
|
20
21
|
import { PickerModal } from "./pickers";
|
|
@@ -157,6 +158,7 @@ function LaunchComposer(props: LaunchPageProps & { issue: IssueDetail; settings:
|
|
|
157
158
|
// Null until the user types. Until then the prompt follows the template and options.
|
|
158
159
|
const [customPrompt, setCustomPrompt] = useState<string | null>(null);
|
|
159
160
|
const { projectConfig } = choices;
|
|
161
|
+
const { guidance, change: changeGuidance } = useProjectGuidance(choices.project, projectConfig);
|
|
160
162
|
const generated = useMemo(
|
|
161
163
|
() =>
|
|
162
164
|
composePrompt({
|
|
@@ -166,8 +168,9 @@ function LaunchComposer(props: LaunchPageProps & { issue: IssueDetail; settings:
|
|
|
166
168
|
project: projectConfig,
|
|
167
169
|
includeComments,
|
|
168
170
|
extraInstructions: "",
|
|
171
|
+
guidance,
|
|
169
172
|
}),
|
|
170
|
-
[action, issue, settings, projectConfig, includeComments],
|
|
173
|
+
[action, issue, settings, projectConfig, includeComments, guidance],
|
|
171
174
|
);
|
|
172
175
|
const prompt = customPrompt ?? generated;
|
|
173
176
|
const styles = useMemo(() => {
|
|
@@ -271,6 +274,7 @@ function LaunchComposer(props: LaunchPageProps & { issue: IssueDetail; settings:
|
|
|
271
274
|
patch,
|
|
272
275
|
started,
|
|
273
276
|
keyScope,
|
|
277
|
+
guidance,
|
|
274
278
|
explore: settings.live.enabled && settings.live.mapping === "explore",
|
|
275
279
|
},
|
|
276
280
|
{
|
|
@@ -301,6 +305,7 @@ function LaunchComposer(props: LaunchPageProps & { issue: IssueDetail; settings:
|
|
|
301
305
|
issue,
|
|
302
306
|
prompt,
|
|
303
307
|
keyScope,
|
|
308
|
+
guidance,
|
|
304
309
|
queries,
|
|
305
310
|
toast,
|
|
306
311
|
onStarted,
|
|
@@ -443,6 +448,10 @@ function LaunchComposer(props: LaunchPageProps & { issue: IssueDetail; settings:
|
|
|
443
448
|
/>
|
|
444
449
|
) : null}
|
|
445
450
|
</View>
|
|
451
|
+
<View style={styles.group}>
|
|
452
|
+
<SectionLabel theme={theme}>Agent guidance</SectionLabel>
|
|
453
|
+
<GuidanceToggles theme={theme} guidance={guidance} onChange={changeGuidance} />
|
|
454
|
+
</View>
|
|
446
455
|
{launch.error ? (
|
|
447
456
|
<Text accessibilityRole="alert" style={styles.error}>
|
|
448
457
|
{errorMessage(launch.error)}
|