@stevecastaneda/paseo-progress 1.3.0 → 1.4.0
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 +1 -1
- package/README.md +34 -5
- package/client/launcher.tsx +2 -2
- package/client/panel.tsx +30 -10
- package/client/questions.tsx +20 -4
- package/client/settings.tsx +40 -0
- package/client/surfaces.ts +20 -8
- package/client/ticket-dialog.tsx +2 -1
- package/client/whats-new.tsx +120 -0
- package/client/write-up.tsx +3 -2
- package/dist/server/cli.js +140 -37
- package/images/1-panel.png +0 -0
- package/images/3-question.png +0 -0
- package/images/5-settings.png +0 -0
- package/index.client.tsx +12 -1
- package/index.server.ts +11 -2
- package/package.json +2 -1
- package/paseo-plugin.json +2 -1
- package/server/cli.ts +28 -11
- package/server/launcher.ts +81 -21
- package/server/question-mode.ts +61 -0
- package/server/skill.ts +15 -6
- package/server/store.ts +21 -4
- package/server/whats-new.ts +127 -0
- package/shared/dashboard.ts +9 -2
- package/shared/events.ts +9 -4
- package/shared/question-modes.ts +45 -0
- package/shared/rpc.ts +18 -0
- package/shared/settings.ts +10 -0
- package/shared/show.ts +3 -1
- package/shared/whats-new.ts +16 -0
- package/shared/write-up.ts +9 -8
- package/skills/paseo-progress/SKILL.md +26 -68
package/OVERVIEW.md
CHANGED
|
@@ -2,7 +2,7 @@ Progress gives each worktree a live dashboard for long agent jobs. The agent rec
|
|
|
2
2
|
|
|
3
3
|
The panel shows how much of the estimated work is done, what the agent is doing right now, and where each ticket stands. A ticket turns stuck when it's blocked or runs past its estimate without an update. Press a ticket to see its timeline, activity, questions and deliverables. Images, videos and text files the agent records open in a preview inside Paseo, and a folder opens as a list of its files. Videos play only in the desktop app. Any other file opens in an app on the computer running the Paseo daemon, and so does a previewed file when you press Open. Open is hidden when that computer can't open files, like a Linux server with no desktop. That's true from your phone too. The app opens on that computer, not on the phone.
|
|
4
4
|
|
|
5
|
-
When the agent needs a decision, it asks a numbered question with lettered options and says which one it recommends. A pill above the message box counts open questions and stuck work. Each option has a Copy button. Paste the answer into the chat and the agent records it.
|
|
5
|
+
When the agent needs a decision, it asks a numbered question with lettered options and says which one it recommends. A pill above the message box counts open questions and stuck work. Each option has a Copy button. Paste the answer into the chat and the agent records it. In Settings you choose what the agent does while it waits: stop, work around the question, or keep building on its recommendation.
|
|
6
6
|
|
|
7
7
|
Nothing updates unless the agent runs the command, so agents need the plugin's skill. The panel has two setup buttons, and each one shows where it will write before you press it:
|
|
8
8
|
|
package/README.md
CHANGED
|
@@ -13,12 +13,14 @@ The dashboard shows:
|
|
|
13
13
|
- percent of estimated work done, time done of time estimated (like "1h 30m of 2h"; once everything is done, how long it really took), and a bar with one segment per ticket
|
|
14
14
|
- **Stuck:** blocked tickets, working tickets with no update for longer than their estimate, and blockers the agent flags
|
|
15
15
|
- **Tickets:** estimate, status, and the working ticket's stage. A ticket that waits for another (`--waits-for T03`) shows "Waits for Ticket 03" under its title until that one is done; waiting is the order of work, so it never counts as stuck, even if the agent also marked it blocked. Press a ticket for its story in a dialog. A summary on top shows its status and stage, time worked against the estimate as a bar that turns red past it, the agent's latest status line while it's working, and the agent on it. Tabs below hold the rest, each only when it has something: the ticket's write-up when it has a `--source` file (the start of it, with the rest a press away), a timeline of its stages and statuses with how long each took, and the activity, questions and deliverables that belong to it. The agent on a ticket shows by its chat title; pressing it opens that chat. Items tagged with `--ticket` belong exactly; older, untagged ones are matched by when they happened and marked "by time"
|
|
16
|
-
- **Questions:** each with a permanent reference (Q1, Q2, ...), lettered options, and the option the agent recommends.
|
|
16
|
+
- **Questions:** each with a permanent reference (Q1, Q2, ...), lettered options, and the option the agent recommends. You pick what the agent does while you decide in Paseo's Settings (Settings, Plugins, Progress, Settings). **Stop**: it stops all work until you answer. **Work around**, the default: it holds what your answer affects and keeps working on the rest. **Keep building**: it builds its recommendation and changes course if you pick another, except for big questions, which wait. A question is big when building ahead would send something off your computer, when the answer sets something people or programs outside the repo rely on, or when most of the remaining work would be built on it. Each open question shows what the agent is doing: "Agent stopped until you answer", "Waiting for you" (with the reason when it's big), or "Building with B: <its label>". Press a question to open it in a dialog, then press **Copy Q2 B** to copy `Q2 (title): B` for your reply. Files and links the agent attached (`--file`) open like deliverables: images, videos and text files preview in Paseo (with **Back** to the question), a folder lists its files there, links open in Paseo's browser, and other files open in their default app. From the pill's popover, the preview opens over the popover, which stays open so you can go back to it. Answers stay listed under the same reference.
|
|
17
17
|
- **Activity**, **Answered** questions and **Deliverables** (press one to open it), sharing one card with a tab for each; the panel remembers the tab you picked. Each shows the newest 10, with **Show 10 older** below
|
|
18
18
|
|
|
19
19
|
<img src="images/2-ticket.png" alt="A ticket's story: its status, time against the estimate, the agent on it, and its timeline" width="400"> <img src="images/3-question.png" alt="A question waiting for an answer, with lettered options, the agent's recommendation and Copy buttons" width="400">
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
<img src="images/5-settings.png" alt="The Questions setting in Paseo's Settings, set to Work around" width="600">
|
|
22
|
+
|
|
23
|
+
The panel lives in Explorer, beside the agent chat, so you can watch both. Open it from Command Center with **Open Progress**. The cog at the top right of the panel opens the plugin's Settings. After an update, the panel shows what's new in that version once. When questions are waiting or something is stuck, a pill above the message box says so, for example `3 questions`. With something stuck too, it shortens to `3Q · 1 stuck` to fit. Pressing it opens a popover with each open question, its choices and their Copy buttons, and what is stuck, so you can answer without leaving the chat. **Open Progress** at the bottom opens the panel in Explorer.
|
|
22
24
|
|
|
23
25
|
<img src="images/4-pill.png" alt="The pill above the message box, showing two questions waiting" width="600">
|
|
24
26
|
|
|
@@ -36,7 +38,7 @@ Or from Paseo's plugin registry: `paseo plugin add stevecastaneda/progress`.
|
|
|
36
38
|
|
|
37
39
|
Enable plugins under **Settings → Plugins** on the Paseo host. You need Node.js 22.18 or later on the daemon host.
|
|
38
40
|
|
|
39
|
-
Then install the `paseo-progress` command agents run. Open **Progress** from Command Center and press **Install command** in the banner. It adds one file, `~/.local/bin/paseo-progress`, on the machine running the Paseo daemon, and the banner disappears once agents can run it. The plugin writes nothing until you press it, and it never replaces a file something else put at that path.
|
|
41
|
+
Then install the `paseo-progress` command agents run. Open **Progress** from Command Center and press **Install command** in the banner. It adds one file, `~/.local/bin/paseo-progress`, on the machine running the Paseo daemon, and the banner disappears once agents can run it. On Windows it also adds `paseo-progress.cmd` next to it, because PowerShell and cmd only run files with an extension like `.cmd`. The plugin writes nothing until you press it, and it never replaces a file something else put at that path.
|
|
40
42
|
|
|
41
43
|
To install from a terminal instead, or to put the command somewhere else:
|
|
42
44
|
|
|
@@ -47,6 +49,15 @@ node "$([ -f "$dir/dist/server/cli.js" ] && echo "$dir/dist/server/cli.js" || ec
|
|
|
47
49
|
|
|
48
50
|
If `paseo` isn't on your `PATH`, use `/Applications/Paseo.app/Contents/Resources/bin/paseo`. The command doesn't name a plugin folder: each run asks Paseo (`paseo plugin ls`) which copy of the plugin it is running and runs that copy, so it keeps working after updates and `npm run dev` switches. The npm package carries the command as one JavaScript file in `dist/`, built with esbuild when publishing, because Node won't run TypeScript from `node_modules`. That file includes zod (MIT), so installing the plugin downloads no other packages.
|
|
49
51
|
|
|
52
|
+
The command, and **Install skill** in the panel, look for Paseo's own command in this order:
|
|
53
|
+
|
|
54
|
+
1. `PASEO_BIN`, if you set it.
|
|
55
|
+
2. `PASEO_CLI`, which Paseo sets for the agents it starts.
|
|
56
|
+
3. `paseo` on your `PATH`.
|
|
57
|
+
4. The app's default install: `/Applications/Paseo.app/Contents/Resources/bin/paseo` on macOS, `%LOCALAPPDATA%\Programs\Paseo\resources\bin\paseo.cmd` on Windows.
|
|
58
|
+
|
|
59
|
+
Setting `PASEO_PROGRESS_PLUGIN_DIR` to a plugin folder makes the command run that copy without asking Paseo. Use it only for testing, because an installed plugin's folder changes with every update.
|
|
60
|
+
|
|
50
61
|
## Where progress is kept
|
|
51
62
|
|
|
52
63
|
Each worktree's progress lives outside the repo, in its own folder under `~/.local/state/paseo-progress/` on the daemon host. There's nothing to set up and nothing to hide: git never sees these files, and the plugin never edits a `.gitignore` or git's exclude file. The panel's empty state shows the exact path.
|
|
@@ -63,7 +74,7 @@ Earlier versions kept progress in `.scratch/` in each worktree, hidden by a `.sc
|
|
|
63
74
|
|
|
64
75
|
## Agent skill
|
|
65
76
|
|
|
66
|
-
The plugin ships an agent skill, `paseo-progress`, that tells agents when to run each command during a multi-ticket job: set up the run, move tickets through their stages, ask questions
|
|
77
|
+
The plugin ships an agent skill, `paseo-progress`, that tells agents when to run each command during a multi-ticket job: set up the run, move tickets through their stages, ask questions, and finish. It stays short and leaves the details to `paseo-progress --help`. Press **Install skill** in the Progress panel. It links `paseo-progress` into `~/.agents/skills`, `~/.claude/skills`, and `~/.codex/skills` (the folders Paseo installs its own skills into), pointing at the copy of the plugin Paseo runs, so plugin updates reach agents. On Windows the links are junctions, which don't need admin rights or Developer Mode. If Paseo later runs the plugin from somewhere else, the button changes to **Update skill**. It never replaces a skill it didn't link; that folder is skipped.
|
|
67
78
|
|
|
68
79
|
From a terminal instead:
|
|
69
80
|
|
|
@@ -99,7 +110,8 @@ paseo-progress finish "Snapshots ship for all four tables"
|
|
|
99
110
|
- Statuses are `not_started`, `working`, `blocked`, `done`, and `skipped`. Skipped tickets leave the totals.
|
|
100
111
|
- `ticket add` and `ticket update` take an optional `--source <path or link>[=<label>]`: where the ticket is written up, like its spec file or issue. It shows under the ticket's status in its dialog and opens like a deliverable. `--source ""` clears it. Tickets without one are fine.
|
|
101
112
|
- A working ticket is stuck once it goes longer than its estimate without an update, timed from its last status, stage or note change, or activity tagged to it. The ticket's dialog still shows total time worked against the estimate.
|
|
102
|
-
- `question ask` needs `--recommend`, the agent's pick. The question dialog tags that option "Recommended".
|
|
113
|
+
- `question ask` needs `--recommend`, the agent's pick. The question dialog tags that option "Recommended". Its output tells the agent the question mode and what to do until the answer. `--big "<why>"` marks a big question, which waits for the answer even in Keep building. `start` and `show` also print the mode.
|
|
114
|
+
- Paseo keeps the question mode in its own settings, which only the plugin's server can read, so the server copies it to `~/.local/state/paseo-progress/settings.json` for the command. Without that file, for example while the plugin is off, the command uses Work around.
|
|
103
115
|
- Deliverable paths are stored relative to the worktree root. Web links open in Paseo's browser. Pressing an image (PNG, JPEG, GIF, WebP, up to 10 MB) or text deliverable (Markdown, text, logs, JSON, YAML, CSV; the first 256 KB) previews it in a dialog inside Paseo, which also works from a phone; **Open** there opens the file in its default app. **Open** shows only when the computer running Paseo can open files. A Linux server with no desktop can't, so there a file Paseo can't preview copies its path and says why. A video (MP4, MOV, WebM, up to 200 MB) plays in the same dialog in the desktop app. When there are several files, **Previous** and **Next** step through them, and so do the left and right arrow keys. Pressing a folder lists its files in the same dialog. Press a file to preview it, or a folder inside to list that one, and **Back** returns to the list. Pressing any other local deliverable opens it with its default app on the machine running the Paseo daemon (for example HTML in your browser). On macOS, **Open** sends text files to your default browser. Only recorded deliverables inside the worktree open this way; otherwise the path is copied. The copy icon on each row copies the path.
|
|
104
116
|
- Several agents can share one run. When another session adds a ticket, each agent sees it at the end of its next command's output (and in `show`), and the skill has it work that ticket in order after its current one. An agent only hears about tickets added since its own last update in the run, and only once.
|
|
105
117
|
- Subagents record their own progress. Since Paseo 0.11, an agent waiting on background subagents shows as idle, so only the subagents' updates show the work is still going. The skill has the agent give each subagent the worktree path and ticket id, and the subagent records its stages and a ticker line on that worktree's dashboard.
|
|
@@ -125,6 +137,23 @@ The progress file, `progress.jsonl`, holds one JSON event per line, each with a
|
|
|
125
137
|
|
|
126
138
|
## Changelog
|
|
127
139
|
|
|
140
|
+
### 1.4.0
|
|
141
|
+
|
|
142
|
+
- [New] **Choose what agents do while a question waits.** Open Settings with the cog at the top right of the Progress panel. **Stop** halts all work until you answer. **Work around**, the default, holds only what your answer affects, which is how questions already worked. **Keep building** builds the agent's pick and changes course if you choose another.
|
|
143
|
+
- [New] **Big questions always wait.** In Keep building, the agent still waits when building ahead would send something off your computer, set something people outside the repo rely on, or carry most of the remaining work. The question says which.
|
|
144
|
+
- [Improved] **Every open question shows what the agent is doing.** It reads "Agent stopped until you answer", "Waiting for you", or "Building with B" and the name of option B.
|
|
145
|
+
- [Improved] **A shorter agent skill.** It's about half as long, and agents get the rule for your mode each time they ask.
|
|
146
|
+
- [New] **What's new, once per update.** After an update, a banner in the panel shows that version's changes. Closing it hides it until the next version.
|
|
147
|
+
- [Fixed] **Bold text in ticket write-ups.** Words marked bold in a ticket's write-up used to show as plain text.
|
|
148
|
+
|
|
149
|
+
### 1.3.1
|
|
150
|
+
|
|
151
|
+
- Progress works on Windows (#84). Before, `paseo-progress` stopped with "The progress plugin is not installed in Paseo", and **Install skill** couldn't find the plugin. On Windows, Paseo's command is `paseo.cmd`, and Node can start a `.cmd` file only through cmd. Both now go through cmd on Windows, and both also check where Paseo installs its command.
|
|
152
|
+
- On Windows, **Install command** also adds `paseo-progress.cmd`, so agents in PowerShell or cmd can run `paseo-progress`, not just those in Git Bash.
|
|
153
|
+
- On Windows, **Install skill** makes junctions instead of symlinks, so it works without admin rights or Developer Mode.
|
|
154
|
+
- The banner no longer says the command's folder is missing from `PATH` on Windows when it's there.
|
|
155
|
+
- If you worked around this by hand, delete your own `paseo-progress.cmd`, then press **Update command**. Also remove `PASEO_PROGRESS_PLUGIN_DIR`, or the command keeps running the folder it names after the plugin updates. Skill links you made yourself keep working, and the panel offers **Update skill** once the plugin folder changes.
|
|
156
|
+
|
|
128
157
|
### 1.3.0
|
|
129
158
|
|
|
130
159
|
- The file preview has a **Copy** button beside the path. It copies the full path, so you can paste it into your message to the agent without closing the preview. Before, the path was cut off when it was long, and you had to close the preview and copy it from the list.
|
package/client/launcher.tsx
CHANGED
|
@@ -22,7 +22,7 @@ export function skillQueryKey(hostId: string) {
|
|
|
22
22
|
export function launcherNotice(status: LauncherStatus): { text: string; action?: string } | null {
|
|
23
23
|
switch (status.state) {
|
|
24
24
|
case "missing":
|
|
25
|
-
return { text: `Agents record progress with the paseo-progress command, which isn't installed yet. Installing adds
|
|
25
|
+
return { text: `Agents record progress with the paseo-progress command, which isn't installed yet. Installing adds it at ${status.path}`, action: "Install command" };
|
|
26
26
|
case "outdated":
|
|
27
27
|
return { text: `An older paseo-progress command is installed at ${status.path}.`, action: "Update command" };
|
|
28
28
|
case "foreign":
|
|
@@ -34,7 +34,7 @@ export function launcherNotice(status: LauncherStatus): { text: string; action?:
|
|
|
34
34
|
|
|
35
35
|
// What the skill banner says, or null when every agent folder has the skill.
|
|
36
36
|
export function skillNotice(status: SkillStatus): { text: string; action?: string } | null {
|
|
37
|
-
const folders = (state: string) => status.targets.filter((target) => target.state === state).map((target) => target.path.replace(
|
|
37
|
+
const folders = (state: string) => status.targets.filter((target) => target.state === state).map((target) => target.path.replace(/[\\/]paseo-progress$/, ""));
|
|
38
38
|
const skipped = folders("foreign").length ? ` Something else is already at ${folders("foreign").join(", ")}, so the plugin leaves it alone.` : "";
|
|
39
39
|
switch (status.state) {
|
|
40
40
|
case "missing":
|
package/client/panel.tsx
CHANGED
|
@@ -2,10 +2,11 @@ import { type PluginWorkspacePanelProps, useRpc, useWorkspace } from "@getpaseo/
|
|
|
2
2
|
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
3
3
|
import { DeliverablesList } from "./deliverables";
|
|
4
4
|
import { LauncherBanner, SkillBanner } from "./launcher";
|
|
5
|
+
import { WhatsNewBanner } from "./whats-new";
|
|
5
6
|
import { HistoryRow } from "./history";
|
|
6
7
|
import { AnsweredQuestionsList, QuestionsSection } from "./questions";
|
|
7
8
|
import { Spinner, StalledPulse, WaitingDot } from "./spinner";
|
|
8
|
-
import { IconSwap, Presence, StaggerRoot, nativeDriver } from "./motion";
|
|
9
|
+
import { IconSwap, PressScale, Presence, StaggerRoot, nativeDriver } from "./motion";
|
|
9
10
|
import { SectionTitle } from "./section-title";
|
|
10
11
|
import { raised } from "./surfaces";
|
|
11
12
|
import { unbroken, When } from "./when";
|
|
@@ -30,11 +31,13 @@ type Colors = PluginWorkspacePanelProps["theme"]["colors"];
|
|
|
30
31
|
|
|
31
32
|
const mono = () => (Platform.OS === "ios" ? "Menlo" : "monospace");
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
type PanelProps = PluginWorkspacePanelProps & { openSettings(): void };
|
|
35
|
+
|
|
36
|
+
export function ProgressPanel(props: PanelProps) {
|
|
34
37
|
return <WorkspaceProgress key={`${props.host.id}:${props.workspaceId}`} {...props} />;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
function WorkspaceProgress({ theme, workspaceId, host, layout, navigation }:
|
|
40
|
+
function WorkspaceProgress({ theme, workspaceId, host, layout, navigation, openSettings }: PanelProps) {
|
|
38
41
|
const colors = theme.colors;
|
|
39
42
|
const directory = useWorkspace(workspaceId, (workspace) => workspace.directory);
|
|
40
43
|
// Paseo marks the workspace running while any of its agents is. Since 0.11 a Claude
|
|
@@ -56,12 +59,13 @@ function WorkspaceProgress({ theme, workspaceId, host, layout, navigation }: Plu
|
|
|
56
59
|
Could not read progress: {errorText(query.error, "something went wrong")}
|
|
57
60
|
</Text>
|
|
58
61
|
) : null}
|
|
62
|
+
<WhatsNewBanner colors={colors} host={host} />
|
|
59
63
|
<LauncherBanner colors={colors} host={host} />
|
|
60
64
|
<SkillBanner colors={colors} host={host} />
|
|
61
65
|
{/* Fades in like the dashboard's sections instead of popping in after the spinner. */}
|
|
62
|
-
<Presence show={Boolean(result && !result.configured)}>{result && !result.configured ? <EmptyState colors={colors} file={result.file} /> : null}</Presence>
|
|
66
|
+
<Presence show={Boolean(result && !result.configured)}>{result && !result.configured ? <EmptyState colors={colors} file={result.file} openSettings={openSettings} /> : null}</Presence>
|
|
63
67
|
{/* Keyed by run: a new run starts with fresh tabs, pages and dialogs. */}
|
|
64
|
-
{result?.configured ? <DashboardView key={result.dashboard.run?.startedAt ?? "none"} colors={colors} dashboard={result.dashboard} agentRunning={agentRunning} compact={layout.compact}
|
|
68
|
+
{result?.configured ? <DashboardView key={result.dashboard.run?.startedAt ?? "none"} colors={colors} openSettings={openSettings} dashboard={result.dashboard} agentRunning={agentRunning} compact={layout.compact}
|
|
65
69
|
workspaceId={workspaceId} workspaceDirectory={result.root} file={result.file} navigation={navigation} /> : null}
|
|
66
70
|
{result?.inRepo && directory ? <HistoryRow colors={colors} workspaceId={workspaceId} directory={directory} savedTo={result.savedTo} /> : null}
|
|
67
71
|
</ScrollView>
|
|
@@ -84,7 +88,17 @@ function useMarkPanelOpened(workspaceId: string, directory: string | null, neede
|
|
|
84
88
|
}, [needed, directory, workspaceId, mark]);
|
|
85
89
|
}
|
|
86
90
|
|
|
87
|
-
|
|
91
|
+
// Optical: the 24px button centers on a 20 to 21px title line.
|
|
92
|
+
function SettingsButton({ colors, onPress }: { colors: Colors; onPress(): void }) {
|
|
93
|
+
return (
|
|
94
|
+
<PressScale accessibilityRole="button" accessibilityLabel="Open Progress settings" hitSlop={6} outerStyle={{ marginVertical: -2 }} onPress={onPress}
|
|
95
|
+
style={({ pressed, hovered }) => ({ padding: 4, borderRadius: 4, backgroundColor: pressed ? colors.border : hovered ? colors.surface1 : "transparent" })}>
|
|
96
|
+
<Icon name="Settings" size={16} color={colors.foregroundMuted} />
|
|
97
|
+
</PressScale>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function EmptyState({ colors, file, openSettings }: { colors: Colors; file: string; openSettings(): void }) {
|
|
88
102
|
return (
|
|
89
103
|
<View style={{ padding: 12, paddingTop: 20, gap: 10 }}>
|
|
90
104
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
@@ -92,6 +106,8 @@ function EmptyState({ colors, file }: { colors: Colors; file: string }) {
|
|
|
92
106
|
<Text accessibilityRole="header" style={{ color: colors.foreground, fontSize: 14, lineHeight: 20, fontWeight: "500" }}>
|
|
93
107
|
No progress recorded yet
|
|
94
108
|
</Text>
|
|
109
|
+
<View style={{ flex: 1 }} />
|
|
110
|
+
<SettingsButton colors={colors} onPress={openSettings} />
|
|
95
111
|
</View>
|
|
96
112
|
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
|
|
97
113
|
An agent working in this worktree records progress with the <Text style={{ color: colors.foreground }}>paseo-progress</Text> command. It shows up here as soon as the first run starts.
|
|
@@ -132,7 +148,8 @@ function useNow(intervalMs: number): number {
|
|
|
132
148
|
|
|
133
149
|
const WIDE_MIN = 760;
|
|
134
150
|
|
|
135
|
-
function DashboardView({ colors, dashboard, agentRunning, compact, workspaceId, workspaceDirectory, file, navigation }: {
|
|
151
|
+
function DashboardView({ colors, dashboard, agentRunning, compact, workspaceId, workspaceDirectory, file, navigation, openSettings }: {
|
|
152
|
+
openSettings(): void;
|
|
136
153
|
colors: Colors;
|
|
137
154
|
dashboard: Dashboard;
|
|
138
155
|
agentRunning: boolean;
|
|
@@ -155,9 +172,12 @@ function DashboardView({ colors, dashboard, agentRunning, compact, workspaceId,
|
|
|
155
172
|
<StaggerRoot>
|
|
156
173
|
<Presence show order={0}>
|
|
157
174
|
<View style={{ paddingHorizontal: 12, paddingTop: 12, paddingBottom: 10, gap: 4, borderBottomWidth: 1, borderBottomColor: colors.border }}>
|
|
158
|
-
<
|
|
159
|
-
{
|
|
160
|
-
|
|
175
|
+
<View style={{ flexDirection: "row", alignItems: "flex-start", gap: 8 }}>
|
|
176
|
+
<Text accessibilityRole="header" style={{ flex: 1, color: colors.foreground, fontSize: 15, lineHeight: 21, fontWeight: "600" }}>
|
|
177
|
+
{dashboard.run?.title ?? "Progress"}
|
|
178
|
+
</Text>
|
|
179
|
+
<SettingsButton colors={colors} onPress={openSettings} />
|
|
180
|
+
</View>
|
|
161
181
|
{dashboard.run?.subtitle ? (
|
|
162
182
|
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>{dashboard.run.subtitle}</Text>
|
|
163
183
|
) : null}
|
package/client/questions.tsx
CHANGED
|
@@ -153,7 +153,10 @@ function QuestionRow({ colors, question, first, onOpen, onCopy }: {
|
|
|
153
153
|
</PressScale>
|
|
154
154
|
<PressableRow colors={colors} accessibilityRole="button" accessibilityLabel={`${question.id} choices`} onPress={onOpen}
|
|
155
155
|
style={{ flex: 1, minWidth: 0, flexDirection: "row", alignItems: "flex-start", gap: 8, borderRadius: 4, margin: -4, padding: 4 }}>
|
|
156
|
-
<
|
|
156
|
+
<View style={{ flex: 1, minWidth: 0, gap: 2 }}>
|
|
157
|
+
<Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 19 }}>{question.question}</Text>
|
|
158
|
+
<QuestionState colors={colors} question={question} />
|
|
159
|
+
</View>
|
|
157
160
|
{/* Optical: centers the 14px chevron on the 19px first line. */}
|
|
158
161
|
<View style={{ paddingTop: 2.5 }}><Icon name="ChevronRight" size={14} color={colors.foregroundMuted} /></View>
|
|
159
162
|
</PressableRow>
|
|
@@ -183,10 +186,23 @@ export function QuestionView({ colors, question, now, onCopy, onOpenAttachment,
|
|
|
183
186
|
);
|
|
184
187
|
}
|
|
185
188
|
|
|
186
|
-
//
|
|
189
|
+
// Under an open question: whether the agent stopped, waits, or builds its pick,
|
|
190
|
+
// and its pick when there are no lettered options to tag.
|
|
187
191
|
export function QuestionState({ colors, question }: { colors: Colors; question: Question }) {
|
|
188
|
-
|
|
189
|
-
|
|
192
|
+
const value = { color: colors.foreground };
|
|
193
|
+
// The letter alone means nothing in the rows that don't list the options.
|
|
194
|
+
const pick = question.options.find((option) => option.letter === question.recommended);
|
|
195
|
+
return (
|
|
196
|
+
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
|
|
197
|
+
{question.state === "building" ? <>Building with <Text style={value}>{question.recommended}{pick ? `: ${pick.label}` : ""}</Text></> : (
|
|
198
|
+
<>
|
|
199
|
+
{!question.options.length && question.recommended ? <>Recommended: <Text style={value}>{question.recommended}</Text>. </> : null}
|
|
200
|
+
{question.state === "stopped" ? "Agent stopped until you answer" : "Waiting for you"}
|
|
201
|
+
{question.big ? <>. <Text style={value}>Big</Text>: {question.big}</> : null}
|
|
202
|
+
</>
|
|
203
|
+
)}
|
|
204
|
+
</Text>
|
|
205
|
+
);
|
|
190
206
|
}
|
|
191
207
|
|
|
192
208
|
// `onSurface1` when drawn on a surface1 background (Paseo's dialogs), so the
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type PluginSurfaceProps, useSettings } from "@getpaseo/plugin/client";
|
|
2
|
+
import { SettingsAction, SettingsCard, SettingsSection, SettingsSelect } from "@getpaseo/plugin/client/ui";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Text } from "react-native";
|
|
5
|
+
import { DEFAULT_QUESTION_MODE, QUESTION_MODE_RULES, QUESTION_MODES } from "../shared/question-modes";
|
|
6
|
+
import { questionSettings } from "../shared/settings";
|
|
7
|
+
|
|
8
|
+
const OPTIONS = QUESTION_MODES.map((value) => ({ label: QUESTION_MODE_RULES[value].label, value }));
|
|
9
|
+
|
|
10
|
+
export function QuestionSettings({ theme }: PluginSurfaceProps) {
|
|
11
|
+
const settings = useSettings(questionSettings);
|
|
12
|
+
const muted = { color: theme.colors.foregroundMuted, fontSize: 13, lineHeight: 19 };
|
|
13
|
+
if (settings.status === "loading") return <Text style={muted}>Loading settings…</Text>;
|
|
14
|
+
if (settings.status !== "ready") {
|
|
15
|
+
return (
|
|
16
|
+
<SettingsSection title="Questions">
|
|
17
|
+
<Text style={muted}>{settings.error}</Text>
|
|
18
|
+
<SettingsCard>
|
|
19
|
+
<SettingsAction label="Try again" actionLabel="Reload" onPress={settings.reload} />
|
|
20
|
+
{settings.status === "invalid" ? <SettingsAction label={`Go back to ${QUESTION_MODE_RULES[DEFAULT_QUESTION_MODE].label}`} actionLabel="Reset" onPress={settings.reset} /> : null}
|
|
21
|
+
</SettingsCard>
|
|
22
|
+
</SettingsSection>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return (
|
|
26
|
+
<SettingsSection title="Questions">
|
|
27
|
+
<SettingsCard>
|
|
28
|
+
<SettingsSelect
|
|
29
|
+
label="When an agent asks you a question"
|
|
30
|
+
hint={QUESTION_MODE_RULES[settings.values.mode].hint}
|
|
31
|
+
error={settings.saveError}
|
|
32
|
+
value={settings.values.mode}
|
|
33
|
+
options={OPTIONS}
|
|
34
|
+
disabled={settings.saving}
|
|
35
|
+
onValueChange={(mode) => void settings.save({ ...settings.values, mode }, settings.revision)}
|
|
36
|
+
/>
|
|
37
|
+
</SettingsCard>
|
|
38
|
+
</SettingsSection>
|
|
39
|
+
);
|
|
40
|
+
}
|
package/client/surfaces.ts
CHANGED
|
@@ -3,17 +3,20 @@ import type { ViewStyle } from "react-native";
|
|
|
3
3
|
|
|
4
4
|
type Colors = PluginWorkspacePanelProps["theme"]["colors"];
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
function
|
|
8
|
-
let rgb: number[] | null = null;
|
|
6
|
+
// The red, green and blue of a #rgb/#rrggbb or rgb() color, or null when unreadable.
|
|
7
|
+
function channels(color: string): number[] | null {
|
|
9
8
|
const hex = color.trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
|
|
10
9
|
if (hex) {
|
|
11
10
|
const digits = hex[1].length === 3 ? [...hex[1]].map((d) => d + d) : hex[1].match(/../g)!;
|
|
12
|
-
|
|
13
|
-
} else {
|
|
14
|
-
const fn = color.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i);
|
|
15
|
-
if (fn) rgb = fn.slice(1, 4).map(Number);
|
|
11
|
+
return digits.map((d) => parseInt(d, 16));
|
|
16
12
|
}
|
|
13
|
+
const fn = color.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i);
|
|
14
|
+
return fn ? fn.slice(1, 4).map(Number) : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Relative luminance, or null when the color is unreadable.
|
|
18
|
+
function luminance(color: string): number | null {
|
|
19
|
+
const rgb = channels(color);
|
|
17
20
|
if (!rgb) return null;
|
|
18
21
|
const [r, g, b] = rgb.map((v) => {
|
|
19
22
|
const c = v / 255;
|
|
@@ -22,7 +25,7 @@ function luminance(color: string): number | null {
|
|
|
22
25
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
function isLight(colors: Colors): boolean | null {
|
|
28
|
+
export function isLight(colors: Colors): boolean | null {
|
|
26
29
|
const value = luminance(colors.surface0);
|
|
27
30
|
return value === null ? null : value > 0.4;
|
|
28
31
|
}
|
|
@@ -43,3 +46,12 @@ export function raised(colors: Colors): ViewStyle {
|
|
|
43
46
|
: "0px 0px 0px 1px rgba(255, 255, 255, 0.08)",
|
|
44
47
|
};
|
|
45
48
|
}
|
|
49
|
+
|
|
50
|
+
// A color at low opacity, for a tag's fill: the same hue as its text, never a
|
|
51
|
+
// drifting second color. Paseo's own status badges use 12% in light themes and
|
|
52
|
+
// 16% in dark ones, where the surface swallows more of it.
|
|
53
|
+
export function tint(color: string, colors: Colors): string {
|
|
54
|
+
const rgb = channels(color);
|
|
55
|
+
if (!rgb) return colors.surface2;
|
|
56
|
+
return `rgba(${rgb.join(", ")}, ${isLight(colors) ? 0.12 : 0.16})`;
|
|
57
|
+
}
|
package/client/ticket-dialog.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import { type Link, ticketStory, type TimelineStep } from "../shared/ticket-stor
|
|
|
8
8
|
import { type Attachment, AttachmentList, deliverableAttachment, ticketSourceAttachment } from "./attachments";
|
|
9
9
|
import { StackedDialog } from "./dialog-stack";
|
|
10
10
|
import { IconSwap } from "./motion";
|
|
11
|
+
import { QuestionState } from "./questions";
|
|
11
12
|
import { PressableRow } from "./row";
|
|
12
13
|
import { Spinner, StalledPulse, WaitingDot } from "./spinner";
|
|
13
14
|
import { StatusBadge } from "./status-badge";
|
|
@@ -98,7 +99,7 @@ export function TicketStoryView({ colors, dashboard, ticket, now, live, context,
|
|
|
98
99
|
<Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 18 }}>{question.title}<LinkNote colors={colors} link={question.link} /></Text>
|
|
99
100
|
{question.answer
|
|
100
101
|
? <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>Answered <Text style={{ color: colors.statusSuccess, fontWeight: "600" }}>{question.answer.choice}</Text>{chosen ? <Text style={{ color: colors.foreground }}>: {chosen.label}</Text> : null}</Text>
|
|
101
|
-
:
|
|
102
|
+
: <QuestionState colors={colors} question={question} />}
|
|
102
103
|
</View>
|
|
103
104
|
<View style={{ paddingTop: 2 }}><Icon name="ChevronRight" size={14} color={colors.foregroundMuted} /></View>
|
|
104
105
|
</PressableRow>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { type PluginWorkspacePanelProps, useRpc } from "@getpaseo/plugin/client";
|
|
2
|
+
import { Icon, Modal } from "@getpaseo/plugin/client/react-native";
|
|
3
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import React, { useState } from "react";
|
|
5
|
+
import { Text, View } from "react-native";
|
|
6
|
+
import { dismissWhatsNew, getWhatsNew } from "../shared/rpc";
|
|
7
|
+
import { NOTE_KINDS, type NoteKind, type NoteTone, type WhatsNew, type WhatsNewNote } from "../shared/whats-new";
|
|
8
|
+
import { spans } from "../shared/write-up";
|
|
9
|
+
import { PressScale, Presence } from "./motion";
|
|
10
|
+
import { isLight, raised, tint } from "./surfaces";
|
|
11
|
+
import { BlockView, Spans } from "./write-up";
|
|
12
|
+
|
|
13
|
+
type Colors = PluginWorkspacePanelProps["theme"]["colors"];
|
|
14
|
+
|
|
15
|
+
// Once after an update: this version's headline, with its full notes a press
|
|
16
|
+
// away. Closing either hides both for this version.
|
|
17
|
+
export function WhatsNewBanner({ colors, host }: { colors: Colors; host: PluginWorkspacePanelProps["host"] }) {
|
|
18
|
+
const fetchWhatsNew = useRpc(getWhatsNew);
|
|
19
|
+
const dismiss = useRpc(dismissWhatsNew);
|
|
20
|
+
const queryClient = useQueryClient();
|
|
21
|
+
const queryKey = ["progress", "whats-new", host.id];
|
|
22
|
+
const query = useQuery({ queryKey, queryFn: () => fetchWhatsNew({}) });
|
|
23
|
+
const whatsNew = query.data ?? null;
|
|
24
|
+
// Paseo's Modal keeps drawing its content while it fades out, so the notes outlive the dismissal.
|
|
25
|
+
const [shown, setShown] = useState<WhatsNew | null>(null);
|
|
26
|
+
const [open, setOpen] = useState(false);
|
|
27
|
+
const close = (version: string) => {
|
|
28
|
+
queryClient.setQueryData(queryKey, null);
|
|
29
|
+
dismiss({ version }).catch(() => queryClient.invalidateQueries({ queryKey }));
|
|
30
|
+
};
|
|
31
|
+
const closeDialog = () => {
|
|
32
|
+
if (!shown) return;
|
|
33
|
+
setOpen(false);
|
|
34
|
+
close(shown.version);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<Presence show={Boolean(whatsNew)}>{whatsNew ? (
|
|
40
|
+
<View style={{ flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 10, margin: 12, marginBottom: 0, padding: 8, borderRadius: 12, ...raised(colors), backgroundColor: colors.surface1 }}>
|
|
41
|
+
<View style={{ alignSelf: "flex-start", height: 17, justifyContent: "center" }}><Icon name="Gift" size={14} color={colors.foregroundMuted} /></View>
|
|
42
|
+
<Text selectable style={{ flex: 1, minWidth: 200, color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
|
|
43
|
+
<Text style={{ color: colors.foreground, fontWeight: "500" }}>New in {whatsNew.version}:</Text> {whatsNew.headline}
|
|
44
|
+
</Text>
|
|
45
|
+
{/* 8px keeps the close button's 6px hit slop off the button beside it. */}
|
|
46
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
47
|
+
<PressScale accessibilityRole="button" onPress={() => { setShown(whatsNew); setOpen(true); }}
|
|
48
|
+
style={({ pressed, hovered }) => ({ paddingHorizontal: 10, paddingVertical: 5, borderRadius: 4, backgroundColor: colors.accent, opacity: pressed ? 0.7 : hovered ? 0.85 : 1 })}>
|
|
49
|
+
<Text style={{ color: colors.accentForeground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>See what’s new</Text>
|
|
50
|
+
</PressScale>
|
|
51
|
+
<PressScale accessibilityRole="button" accessibilityLabel="Dismiss what's new" hitSlop={6} onPress={() => close(whatsNew.version)}
|
|
52
|
+
style={({ pressed, hovered }) => ({ padding: 5, borderRadius: 4, backgroundColor: pressed ? colors.border : hovered ? colors.surface2 : "transparent" })}>
|
|
53
|
+
<Icon name="X" size={16} color={colors.foregroundMuted} />
|
|
54
|
+
</PressScale>
|
|
55
|
+
</View>
|
|
56
|
+
</View>
|
|
57
|
+
) : null}</Presence>
|
|
58
|
+
<Modal title={shown ? `What’s new in Progress ${shown.version}` : ""} icon={<Icon name="Gift" size={16} color={colors.foregroundMuted} />}
|
|
59
|
+
open={open} onOpenChange={(next) => { if (!next) closeDialog(); }}>
|
|
60
|
+
<Modal.Content>
|
|
61
|
+
{shown ? <Notes colors={colors} notes={shown.notes} /> : null}
|
|
62
|
+
<View style={{ flexDirection: "row", justifyContent: "flex-end", marginTop: 8 }}>
|
|
63
|
+
<PressScale accessibilityRole="button" onPress={closeDialog}
|
|
64
|
+
style={({ pressed, hovered }) => ({ paddingHorizontal: 12, paddingVertical: 6, borderRadius: 6, backgroundColor: colors.accent, opacity: pressed ? 0.7 : hovered ? 0.85 : 1 })}>
|
|
65
|
+
<Text style={{ color: colors.accentForeground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>Got it</Text>
|
|
66
|
+
</PressScale>
|
|
67
|
+
</View>
|
|
68
|
+
</Modal.Content>
|
|
69
|
+
</Modal>
|
|
70
|
+
</>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Titled notes read as a short list of headings with their details; a changelog
|
|
75
|
+
// written without titles stays a bulleted list.
|
|
76
|
+
function Notes({ colors, notes }: { colors: Colors; notes: WhatsNewNote[] }) {
|
|
77
|
+
if (!notes.some((note) => note.title || note.kind)) {
|
|
78
|
+
return <View style={{ gap: 12 }}>{notes.map((note, index) => (
|
|
79
|
+
<BlockView key={index} colors={colors} block={{ kind: "item", marker: "•", depth: 0, spans: spans(note.body) }} first={index === 0} />
|
|
80
|
+
))}</View>;
|
|
81
|
+
}
|
|
82
|
+
return (
|
|
83
|
+
<View style={{ gap: 20 }}>
|
|
84
|
+
{notes.map((note, index) => (
|
|
85
|
+
<View key={index} style={{ gap: 4 }}>
|
|
86
|
+
{note.title || note.kind ? (
|
|
87
|
+
<View style={{ flexDirection: "row", alignItems: "flex-start", gap: 8 }}>
|
|
88
|
+
{note.kind ? <KindTag colors={colors} kind={note.kind} /> : null}
|
|
89
|
+
{note.title ? <Text accessibilityRole="header" selectable style={{ flex: 1, color: colors.foreground, fontSize: 14, lineHeight: 20, fontWeight: "600" }}>{note.title}</Text> : null}
|
|
90
|
+
</View>
|
|
91
|
+
) : null}
|
|
92
|
+
{note.body ? (
|
|
93
|
+
<Text selectable style={{ color: note.title ? colors.foregroundMuted : colors.foreground, fontSize: 13, lineHeight: 19 }}>
|
|
94
|
+
<Spans colors={colors} spans={spans(note.body)} />
|
|
95
|
+
</Text>
|
|
96
|
+
) : null}
|
|
97
|
+
</View>
|
|
98
|
+
))}
|
|
99
|
+
</View>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Paseo has no violet for plugins, so it's Paseo's own "merged" purple, which
|
|
104
|
+
// shares the status colors' lightness and chroma.
|
|
105
|
+
const TONE_COLOR: Record<NoteTone, (colors: Colors) => string> = {
|
|
106
|
+
violet: (colors) => isLight(colors) ? "#7347af" : "#a890d5",
|
|
107
|
+
green: (colors) => colors.statusSuccess,
|
|
108
|
+
amber: (colors) => colors.statusWarning,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Sits on the title's first line: 18px tall, 1px down in its 20px line. As wide
|
|
112
|
+
// as "Improved" at least, so the titles beside the tags start at one edge.
|
|
113
|
+
function KindTag({ colors, kind }: { colors: Colors; kind: NoteKind }) {
|
|
114
|
+
const color = TONE_COLOR[NOTE_KINDS[kind].tone](colors);
|
|
115
|
+
return (
|
|
116
|
+
<View style={{ marginTop: 1, minWidth: 64, borderRadius: 4, paddingHorizontal: 7, backgroundColor: tint(color, colors) }}>
|
|
117
|
+
<Text numberOfLines={1} style={{ textAlign: "center", color, fontSize: 11, lineHeight: 18, fontWeight: "500" }}>{NOTE_KINDS[kind].label}</Text>
|
|
118
|
+
</View>
|
|
119
|
+
);
|
|
120
|
+
}
|
package/client/write-up.tsx
CHANGED
|
@@ -62,7 +62,7 @@ function TextButton({ colors, icon, label, accessibilityLabel, onPress, grow = f
|
|
|
62
62
|
);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
function BlockView({ colors, block, first }: { colors: Colors; block: Block; first: boolean }) {
|
|
65
|
+
export function BlockView({ colors, block, first }: { colors: Colors; block: Block; first: boolean }) {
|
|
66
66
|
switch (block.kind) {
|
|
67
67
|
case "heading":
|
|
68
68
|
return (
|
|
@@ -89,8 +89,9 @@ function BlockView({ colors, block, first }: { colors: Colors; block: Block; fir
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
function Spans({ colors, spans }: { colors: Colors; spans: Span[] }) {
|
|
92
|
+
export function Spans({ colors, spans }: { colors: Colors; spans: Span[] }) {
|
|
93
93
|
return <>{spans.map((span, index) => span.code
|
|
94
94
|
? <Text key={index} style={{ fontFamily: MONO, fontSize: 12, color: colors.foreground }}>{span.text}</Text>
|
|
95
|
+
: span.strong ? <Text key={index} style={{ fontWeight: "600" }}>{span.text}</Text>
|
|
95
96
|
: <React.Fragment key={index}>{span.text}</React.Fragment>)}</>;
|
|
96
97
|
}
|