@stevecastaneda/paseo-progress 1.13.0 → 1.14.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 CHANGED
@@ -1,12 +1,14 @@
1
- Progress gives each worktree a live dashboard for long agent jobs. The agent records its tickets or steps with a `paseo-progress` command, along with what's stuck, questions for you and finished work. The Progress panel in Explorer shows all of it. Needs Paseo 0.13 or later, and Node.js 22.18 or later on the computer running the Paseo daemon.
1
+ Progress gives each worktree a live dashboard for long agent jobs. The agent records its tickets or steps, along with what's stuck, questions for you and finished work, with the Progress tools or the `paseo-progress` command. The Progress panel in Explorer shows all of it. Needs Paseo 0.13 or later, and Node.js 22.18 or later on the computer running the Paseo daemon.
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, PDFs and text files the agent records open in a preview inside Paseo, with Markdown formatted, and a folder opens as a list of its files. Videos and PDFs show in the preview only in the desktop and web apps. A PDF in the Deliverables tab also has a button beside Copy that opens it in a Paseo tab, which is where it shows on an iPhone or iPad. Any PDF you open in a Paseo tab shows there too. 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
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
- 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:
7
+ On Paseo 0.14 and later, agents get the Progress tools once you turn on **Settings → your host → Agents → Enable Paseo tools**. That needs nothing installed, and every update names the agent that sent it. The setting is off by default and also lets agents start, message and stop other agents, so Progress leaves it to you. Without it, agents run the `paseo-progress` command. In the chat, each Progress tool call shows as one line of what it recorded, and opens to the full result.
8
8
 
9
- - Install command writes `~/.local/bin/paseo-progress` on the daemon host.
9
+ Nothing updates unless the agent records it, 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:
10
+
11
+ - Install command writes `~/.local/bin/paseo-progress` on the daemon host. With Paseo tools on, the command is only a fallback.
10
12
  - Install skill links the plugin's `paseo-progress` skill into `~/.agents/skills`, `~/.claude/skills` and `~/.codex/skills`. New agent sessions pick it up.
11
13
 
12
14
  Both work from files inside the plugin and download nothing. Neither one replaces a file you made.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Progress
4
4
 
5
- A plugin for Paseo 0.13 and later that gives each worktree a live progress dashboard. Agents record progress with the `paseo-progress` command, which adds one line per change to the worktree's progress file. That file lives outside the repo, so git never sees it. The **Progress** panel draws the dashboard from that file and updates by itself.
5
+ A plugin for Paseo 0.13 and later that gives each worktree a live progress dashboard. Agents record progress with the Progress tools Paseo 0.14 hands them, or with the `paseo-progress` command. Each change adds one line to the worktree's progress file. That file lives outside the repo, so git never sees it. The **Progress** panel draws the dashboard from that file and updates by itself.
6
6
 
7
7
  <img src="images/1-panel.png" alt="The Progress panel in Explorer" width="440">
8
8
 
@@ -18,7 +18,7 @@ The dashboard shows:
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
- <img src="images/5-settings.png" alt="Progress settings in Paseo: Questions set to Work around, Language set to System, and the Agent Skills section with View skill" width="600">
21
+ <img src="images/5-settings.png" alt="Progress settings in Paseo: Questions set to Work around, Language set to System, the Agent Skills section with View skill, and Agent tools showing Paseo tools off with how to turn them on" width="600">
22
22
 
23
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 once, including any versions you skipped. 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 an open question, its choices and their Copy buttons, and what is stuck, so you can answer without leaving the chat. With more than one question, the arrows beside **Question 1 of 3** step through them. After you copy an answer, the popover opens on the next question. **Open Progress** at the bottom opens the panel in Explorer.
24
24
 
@@ -38,7 +38,9 @@ Or from Paseo's plugin registry: `paseo plugin add stevecastaneda/progress`.
38
38
 
39
39
  Enable plugins under **Settings → Plugins** on the Paseo host. You need Node.js 22.18 or later on the daemon host.
40
40
 
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.
41
+ On Paseo 0.14 and later you can skip the command: turn on Paseo tools and agents get the Progress tools instead. See [Agent tools](#agent-tools). The command is still worth installing as a fallback, because agents in a chat that started before you turned the setting on, or in a reply that was running while the plugin reloaded, don't have the tools.
42
+
43
+ To 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.
42
44
 
43
45
  To install from a terminal instead, or to put the command somewhere else:
44
46
 
@@ -80,9 +82,39 @@ The choice covers every worktree of the repo and is kept in the repo's local git
80
82
 
81
83
  Earlier versions kept progress in `.scratch/` in each worktree, hidden by a `.scratch/.gitignore` they wrote. The first time the command or the panel touches a worktree after the upgrade, the plugin moves its files out of `.scratch/` and removes that `.gitignore` if it's exactly the one the plugin wrote. If `.scratch/` is then empty, it goes too; anything else you keep there stays. If git tracks the old progress file (for example, an agent committed it), the plugin leaves it where it is and keeps using it, so nothing changes in that repo.
82
84
 
85
+ ## Agent tools
86
+
87
+ On Paseo 0.14 and later, Progress hands agents a tool for each common command, so recording progress needs nothing installed and every update names the agent that sent it. Paseo only gives plugin tools to agents while **Settings → your host → Agents → Enable Paseo tools** is on. It's off by default, and turning it on also gives agents Paseo's own tools for starting, messaging and stopping other agents. Progress never changes the setting. Once it's on, start a new chat or reload one to give it the tools.
88
+
89
+ | Tool | Does what this command does |
90
+ | --- | --- |
91
+ | `progress_show` | `show` |
92
+ | `progress_start` | `start` |
93
+ | `progress_ticket_add` | `ticket add` |
94
+ | `progress_ticket_update` | `ticket update` |
95
+ | `progress_question_ask` | `question ask` |
96
+ | `progress_question_answer` | `question answer` |
97
+ | `progress_deliverable_add` | `deliverable add` |
98
+ | `progress_activity_add` | `activity add` |
99
+ | `progress_ticker` | `ticker set`, or `ticker clear` when the text is empty |
100
+ | `progress_finish` | `finish` |
101
+ | `paseo_progress` | Any command, given as the list of words you'd type after `paseo-progress` |
102
+
103
+ Each tool runs the command inside the plugin, so it checks the same things and answers with the same words. Statuses and deliverable kinds are listed in the tool, so an agent can't send one the command doesn't know. Paths are relative to the agent's folder, like the command run there. Claude Code shows them as `mcp__paseo__progress_show` and so on, and holds them back until the agent searches for them. The skill tells agents to search.
104
+
105
+ In the chat, a Progress tool call shows as one line of what it recorded, like "Added T01: Export button (30m)", beside the Progress icon. While the call runs, the line says what the tool is doing, like "Adding ticket…". A failed call shows its error in red. Tap the line to see everything the tool returned, such as the dashboard text from `progress_show`.
106
+
107
+ <img src="images/6-chat-rows.png" alt="A chat with Progress tool calls as single lines: a started run, two added tickets, a failed update in red, and the dashboard text opened below Read the dashboard" width="600">
108
+
109
+ **Agent tools** in Progress settings shows whether the setting is on and, while it's off, how to turn it on. A new worktree's empty panel says the same.
110
+
111
+ <img src="images/7-empty-tools-note.png" alt="An empty Progress panel with the paseo-progress command example and a note on turning on Paseo tools" width="440">
112
+
113
+ While it's on, the banner stops asking you to install the command and only offers it as a fallback. On Paseo 0.13 the panel never mentions tools. Reloading the plugin takes the tools away from a reply that's already running. Paseo gives them back before the chat's next message, and until then the skill has agents use the command.
114
+
83
115
  ## Agent skill
84
116
 
85
- 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. Paseo puts each plugin update in a new folder, so when Progress starts it repoints the links it made to that folder. If it can't, the button changes to **Update skill**. It never replaces a skill it didn't link; that folder is skipped. To read what agents get, open **View skill** under Agent Skills in Progress settings.
117
+ 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 has agents use the Progress tools when they have them and the command otherwise, 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. Paseo puts each plugin update in a new folder, so when Progress starts it repoints the links it made to that folder. If it can't, the button changes to **Update skill**. It never replaces a skill it didn't link; that folder is skipped. To read what agents get, open **View skill** under Agent Skills in Progress settings.
86
118
 
87
119
  From a terminal instead:
88
120
 
@@ -122,7 +154,7 @@ paseo-progress finish "Snapshots ship for all four tables"
122
154
  - 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.
123
155
  - 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. Markdown shows formatted, with links and images relative to the file's own folder; **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, and a PDF (up to 100 MB) shows there too. 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.
124
156
  - 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.
125
- - 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.
157
+ - Subagents record their own progress. A Claude Code subagent can call the Progress tools too, and its updates carry its parent chat's agent id. 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.
126
158
  - `start` begins a fresh dashboard. Earlier runs stay in the file. Question references stay unique across runs.
127
159
  - `finish "<outcome>"` closes the run once every ticket is done or skipped, no question is open, and nothing is flagged stuck; otherwise it says what is left. The run stays on the dashboard, marked Finished with its outcome, and takes no more updates, so the next agent in the worktree starts a new run instead of adding to it.
128
160
 
@@ -133,7 +165,8 @@ The progress file, `progress.jsonl`, holds one JSON event per line, each with a
133
165
  ## Limitations
134
166
 
135
167
  - An agent hears about tickets other sessions added only when it next runs `paseo-progress`. One in the middle of a long step finds out when that step ends.
136
- - Nothing updates unless the agent runs `paseo-progress`. Agents need the skill, and an agent that skips the command leaves the dashboard behind.
168
+ - Nothing updates unless the agent records it, with a Progress tool or `paseo-progress`. Agents need the skill, and an agent that skips it leaves the dashboard behind.
169
+ - Agents get the Progress tools only on Paseo 0.14 and later, with Paseo tools turned on for the host. The setting also lets agents start, message and stop other agents, so Progress leaves it to you.
137
170
  - On Paseo 0.11 and later, an agent waiting on background subagents counts as idle. If those subagents record nothing, the dashboard shows "possibly stale" after 15 minutes while they're still working.
138
171
  - The command runs on the machine that hosts the Paseo daemon, and it needs Node.js 22.18 or later there.
139
172
  - Each worktree keeps its own dashboard. There's no view across worktrees.
@@ -145,6 +178,18 @@ The progress file, `progress.jsonl`, holds one JSON event per line, each with a
145
178
 
146
179
  ## Changelog
147
180
 
181
+ ### 1.14.0
182
+
183
+ - [New] **Agents can record progress with Paseo tools.** On Paseo 0.14 and later, Progress hands agents a tool for each common command, like `progress_ticket_update` and `progress_question_ask`. Nothing needs installing, and every update names the agent that sent it. Agents get them once you turn on **Settings → your host → Agents → Enable Paseo tools**, which also lets them start, message and stop other agents. **Agent tools** in Progress settings shows whether it's on and how to turn it on. The skill now uses the tools when an agent has them and the `paseo-progress` command otherwise, so Paseo 0.13 and agents without the tools work as before.
184
+ - [New] **Progress tool calls read plainly in the chat.** Instead of Paseo's generic tool row, each call shows the Progress icon and one line of what it recorded, like "Added T01: Export button (30m)". A failed call shows its error in red. Tap a row to see everything the tool returned.
185
+
186
+ ### 1.13.1
187
+
188
+ - [Fixed] **Ticket tabs fit on a phone.** On a phone, a ticket's tabs used to wrap and leave Deliverables alone on a second line. They now stay in one row that you swipe sideways. When some tabs are out of sight, that edge of the row fades and shows an arrow you can tap. The tab you pick scrolls into view, and so does the one you had open when you come back to the ticket.
189
+ - [Fixed] **Open chat stays on one line.** On a phone, the Open chat link beside a ticket's agent no longer wraps. The agent's name gets shorter instead.
190
+ - [Fixed] **Larger question arrows in a phone's browser.** In the pill's popover, the arrows that step through questions and the Open Progress button now have the same finger-sized buttons in a phone's web browser as in the Paseo phone apps.
191
+ - [Fixed] **Screen readers hear which tab is selected in the desktop and web apps.** They also hear whether the finished tickets and a write-up are shown or hidden. Before, only the phone apps passed this on.
192
+
148
193
  ### 1.13.0
149
194
 
150
195
  - [New] **PDF previews.** A PDF the agent records now opens in the preview in Paseo's desktop and web apps, with a loading percentage while it arrives. Any PDF you open in a Paseo tab now shows in that tab, up to 100 MB on desktop and 25 MB on a phone. A PDF deliverable also has a button beside Copy that opens it in a tab, which is how you see it on an iPhone or iPad.
@@ -0,0 +1,78 @@
1
+ import type { PluginTimelineItemProps } from "@getpaseo/plugin/client";
2
+ import { Icon, ScrollView } from "@getpaseo/plugin/client/ui";
3
+ import React, { useState } from "react";
4
+ import { Platform, Pressable, Text, View } from "react-native";
5
+ import { type ChatRow, chatRowText } from "./chat-row";
6
+ import { useProgressLanguage } from "./language";
7
+
8
+ const mono = () => (Platform.OS === "ios" ? "Menlo" : "monospace");
9
+
10
+ // Sized like Paseo's own tool rows (ExpandableBadge in the app): 14px label,
11
+ // 12px icon centered in a 22px slot, 8 by 4 padding. The -13 margin puts the
12
+ // icon's glyph on the chat's text edge, where Paseo's rows put theirs.
13
+ export function ChatRowView({ item, theme }: PluginTimelineItemProps<ChatRow>) {
14
+ const { colors } = theme;
15
+ const { messages: m } = useProgressLanguage();
16
+ const [expanded, setExpanded] = useState(false);
17
+ const [hovered, setHovered] = useState(false);
18
+ const row = item.data;
19
+ const { line, more } = chatRowText(row, m);
20
+ const failed = row.state === "failed";
21
+ const running = row.state === "running";
22
+ const open = expanded && more !== null;
23
+ // Only a row that opens lights up on hover, like Paseo's.
24
+ const active = more !== null && (hovered || expanded);
25
+ const iconColor = failed ? colors.statusDanger : active ? colors.foreground : colors.foregroundMuted;
26
+
27
+ const icon = active
28
+ ? <View style={{ marginLeft: -4, transform: [{ scale: 1.3 }, { rotate: expanded ? "90deg" : "0deg" }] }}><Icon name="ChevronRight" size={12} color={colors.foreground} /></View>
29
+ : <View style={{ marginLeft: -1, opacity: failed ? 0.8 : 1 }}><Icon name={failed ? "TriangleAlert" : "ListChecks"} size={12} color={iconColor} /></View>;
30
+
31
+ return (
32
+ <View style={{ marginHorizontal: -13 }}>
33
+ <Pressable
34
+ disabled={more === null}
35
+ onPress={() => setExpanded((value) => !value)}
36
+ onHoverIn={() => setHovered(true)}
37
+ onHoverOut={() => setHovered(false)}
38
+ accessibilityRole={more === null ? undefined : "button"}
39
+ aria-expanded={more === null ? undefined : expanded}
40
+ aria-busy={running}
41
+ style={({ pressed }) => ({
42
+ flexDirection: "row",
43
+ alignItems: "center",
44
+ borderRadius: 8,
45
+ borderWidth: 1,
46
+ borderColor: open ? colors.border : "transparent",
47
+ borderBottomLeftRadius: open ? 0 : 8,
48
+ borderBottomRightRadius: open ? 0 : 8,
49
+ backgroundColor: open ? colors.surface1 : "transparent",
50
+ paddingHorizontal: 8,
51
+ paddingVertical: 4,
52
+ opacity: pressed && more !== null ? 0.9 : 1,
53
+ })}
54
+ >
55
+ <View style={{ width: 22, height: 22, marginRight: 4, alignItems: "center", justifyContent: "center" }}>{icon}</View>
56
+ <Text
57
+ numberOfLines={1}
58
+ style={{
59
+ flexShrink: 1,
60
+ fontSize: 14,
61
+ color: failed ? colors.statusDanger : running || active ? colors.foreground : colors.foregroundMuted,
62
+ opacity: running ? 0.72 : 1,
63
+ }}
64
+ >
65
+ {line}
66
+ </Text>
67
+ </Pressable>
68
+ {open ? (
69
+ <View style={{ borderWidth: 1, borderTopWidth: 0, borderColor: colors.border, borderBottomLeftRadius: 8, borderBottomRightRadius: 8, overflow: "hidden" }}>
70
+ {/* Scrolling past the end stays in the output instead of moving the chat. */}
71
+ <ScrollView style={[{ maxHeight: 320 }, Platform.OS === "web" ? ({ overscrollBehavior: "contain" } as object) : null]} contentContainerStyle={{ paddingHorizontal: 12, paddingVertical: 8 }}>
72
+ <Text selectable style={{ color: failed ? colors.statusDanger : colors.foreground, fontFamily: mono(), fontSize: 12, lineHeight: 18 }}>{more}</Text>
73
+ </ScrollView>
74
+ </View>
75
+ ) : null}
76
+ </View>
77
+ );
78
+ }
@@ -0,0 +1,83 @@
1
+ import type { PluginTimelineTransformerContribution } from "@getpaseo/plugin/client";
2
+ import { z } from "zod";
3
+ import { PROGRESS_TOOL_NAMES, progressToolName } from "../shared/tools";
4
+ import type { ProgressMessages } from "./messages";
5
+
6
+ export const CHAT_ROW_KIND = "tool-call";
7
+ export const CHAT_ROW_VERSION = 1;
8
+
9
+ const tool = z.enum(PROGRESS_TOOL_NAMES);
10
+
11
+ export const chatRowSchema = z.discriminatedUnion("state", [
12
+ z.object({ tool, state: z.literal("running") }),
13
+ z.object({ tool, state: z.literal("done"), output: z.string() }),
14
+ z.object({ tool, state: z.literal("failed"), error: z.string() }),
15
+ z.object({ tool, state: z.literal("canceled") }),
16
+ ]);
17
+
18
+ export type ChatRow = z.output<typeof chatRowSchema>;
19
+
20
+ type ToolCall = Parameters<PluginTimelineTransformerContribution<"tool_call">["transform"]>[0]["item"];
21
+
22
+ function isRecord(value: unknown): value is Record<string, unknown> {
23
+ return typeof value === "object" && value !== null && !Array.isArray(value);
24
+ }
25
+
26
+ // The text a tool call carries, or undefined when its shape is unfamiliar.
27
+ // Claude Code stores the result as { output: text } and an error as
28
+ // { content: text or [{ text }] }; Codex keeps the MCP result { content: [{ text }] }.
29
+ function readText(value: unknown): string | undefined {
30
+ if (value === null || value === undefined) return "";
31
+ if (typeof value === "string") return value;
32
+ if (Array.isArray(value)) {
33
+ const parts = value.map((part) => (typeof part === "string" ? part : isRecord(part) && typeof part.text === "string" ? part.text : undefined));
34
+ return parts.every((part) => part !== undefined) ? parts.join("\n") : undefined;
35
+ }
36
+ if (!isRecord(value)) return undefined;
37
+ const key = ["output", "content", "text", "message"].find((name) => name in value);
38
+ return key ? readText(value[key]) : undefined;
39
+ }
40
+
41
+ // A call to a Progress tool becomes the row's data. Anything else, or a call
42
+ // whose detail can't be read, stays undefined so Paseo draws its own row.
43
+ export function chatRow(item: ToolCall): ChatRow | undefined {
44
+ const name = progressToolName(item.name);
45
+ if (!name) return undefined;
46
+ if (item.status === "running") return { tool: name, state: "running" };
47
+ if (item.status === "canceled") return { tool: name, state: "canceled" };
48
+ if (item.status === "failed") {
49
+ const error = readText(item.error);
50
+ return error === undefined ? undefined : { tool: name, state: "failed", error };
51
+ }
52
+ const detail: unknown = item.detail;
53
+ if (!isRecord(detail) || !("output" in detail)) return undefined;
54
+ const output = readText(detail.output);
55
+ if (output === undefined) return undefined;
56
+ // Codex reports a tool's own error inside a completed MCP result.
57
+ if (isRecord(detail.output) && detail.output.isError === true) return { tool: name, state: "failed", error: output };
58
+ return { tool: name, state: "done", output };
59
+ }
60
+
61
+ export function transformChatRow({ item }: { item: ToolCall }) {
62
+ const data = chatRow(item);
63
+ return data && { items: [{ type: "plugin" as const, kind: CHAT_ROW_KIND, version: CHAT_ROW_VERSION, data }] };
64
+ }
65
+
66
+ // What the row says: one line, and the full text the row opens to. The line
67
+ // truncates on a narrow chat, so any text at all can be opened.
68
+ export function chatRowText(row: ChatRow, m: ProgressMessages): { line: string; more: string | null } {
69
+ const labels = m.toolRow[row.tool];
70
+ const firstLine = (text: string) => text.split("\n").map((line) => line.trim()).find(Boolean) ?? "";
71
+ const withMore = (line: string, text: string) => ({ line, more: text.trim() || null });
72
+ switch (row.state) {
73
+ case "running":
74
+ return { line: labels.running, more: null };
75
+ case "canceled":
76
+ return { line: m.toolRowCanceled(labels.name), more: null };
77
+ case "failed":
78
+ return withMore(firstLine(row.error) || m.toolRowFailed(labels.name), row.error);
79
+ case "done":
80
+ // Show starts with the worktree's path; the dashboard is what the agent read.
81
+ return withMore(row.tool === "progress_show" ? labels.name : firstLine(row.output) || labels.name, row.output);
82
+ }
83
+ }
@@ -25,7 +25,7 @@ export type Step =
25
25
 
26
26
  // Paseo gives each dialog its own backdrop, so opening a second one darkens
27
27
  // the screen while the first fades out; steps swap content inside this one instead.
28
- export function StackedDialog({ colors, root, onClose, questions = [], dashboard, now, live = true, context }: {
28
+ export function StackedDialog({ colors, root, onClose, questions = [], dashboard, now, live = true, compact = false, context }: {
29
29
  colors: Colors;
30
30
  root: Step | null;
31
31
  onClose(): void;
@@ -34,6 +34,8 @@ export function StackedDialog({ colors, root, onClose, questions = [], dashboard
34
34
  dashboard?: Dashboard;
35
35
  now: number;
36
36
  live?: boolean;
37
+ // Paseo's phone-sized layout; the ticket's tabs scroll sideways there.
38
+ compact?: boolean;
37
39
  context: AttachmentContext;
38
40
  }) {
39
41
  // Paseo's Modal keeps drawing its content while it fades out, so closing keeps the steps.
@@ -103,7 +105,7 @@ export function StackedDialog({ colors, root, onClose, questions = [], dashboard
103
105
  ) : (
104
106
  <>
105
107
  {ticket && dashboard ? (
106
- <TicketStoryView colors={colors} dashboard={dashboard} ticket={ticket} now={now} live={live} context={context} onOpenAgent={openAgent}
108
+ <TicketStoryView colors={colors} dashboard={dashboard} ticket={ticket} now={now} live={live} compact={compact} context={context} onOpenAgent={openAgent}
107
109
  onOpenAttachment={openAttachment} onOpenQuestion={(id) => push({ kind: "question", id })} />
108
110
  ) : null}
109
111
  {question ? (
@@ -5,7 +5,7 @@ import React from "react";
5
5
  import { Text, View } from "react-native";
6
6
  import { PressScale, Presence } from "./motion";
7
7
  import { raised } from "./surfaces";
8
- import { getLauncherStatus, getSkillStatus, installLauncher, installSkill, type LauncherStatus, type SkillStatus } from "../shared/rpc";
8
+ import { getLauncherStatus, getSkillStatus, getToolsStatus, installLauncher, installSkill, type LauncherStatus, type SkillStatus, type ToolsState } from "../shared/rpc";
9
9
  import { messagesFor, type ProgressMessages } from "./i18n";
10
10
  import { errorText } from "./errors";
11
11
  import { useProgressLanguage } from "./language";
@@ -34,6 +34,22 @@ export function launcherNotice(status: LauncherStatus, m: ProgressMessages = mes
34
34
  }
35
35
  }
36
36
 
37
+ // With Paseo tools on, the command is only a fallback: the banner offers an
38
+ // install or update and stays quiet otherwise.
39
+ export function setupNotice(status: LauncherStatus, tools: ToolsState, m: ProgressMessages = messagesFor("en")): { text: string; action?: string } | null {
40
+ const notice = launcherNotice(status, m);
41
+ if (tools !== "on") return notice;
42
+ return notice?.action ? { text: m.toolsOn, action: notice.action } : null;
43
+ }
44
+
45
+ // Whether agents get the Progress tools, or null until known. A Paseo that
46
+ // can't say reads as 0.13: the command is how agents record progress.
47
+ export function useToolsState(hostId: string): ToolsState | null {
48
+ const fetchStatus = useRpc(getToolsStatus);
49
+ const status = useQuery({ queryKey: ["progress", "tools", hostId], queryFn: () => fetchStatus({}), staleTime: 60_000, retry: false });
50
+ return status.data?.state ?? (status.isError ? "unsupported" : null);
51
+ }
52
+
37
53
  // What the skill banner says, or null when every agent folder has the skill.
38
54
  export function skillNotice(status: SkillStatus, m: ProgressMessages = messagesFor("en")): { text: string; action?: string } | null {
39
55
  const folders = (state: string) => status.targets.filter((target) => target.state === state).map((target) => target.path.replace(/[\\/]paseo-progress$/, ""));
@@ -66,7 +82,8 @@ export function LauncherBanner({ colors, host }: { colors: Colors; host: PluginW
66
82
  },
67
83
  onError: (error) => toast.error(errorText(error, m.installCommandFailure)),
68
84
  });
69
- const notice = status.data ? launcherNotice(status.data, m) : null;
85
+ const tools = useToolsState(host.id);
86
+ const notice = status.data && tools ? setupNotice(status.data, tools, m) : null;
70
87
  return <SetupBanner colors={colors} icon="Terminal" notice={notice} pending={mutation.isPending} onPress={() => mutation.mutate()} />;
71
88
  }
72
89
 
@@ -33,6 +33,31 @@ export const messages: ProgressMessages = {
33
33
  openSettings: "Open Progress settings",
34
34
  noProgress: "No progress recorded yet",
35
35
  emptyIntro: ["An agent working in this worktree records progress with the ", " command. It shows up here as soon as the first run starts."],
36
+ emptyIntroTools: "An agent working in this worktree records progress with the Progress tools Paseo gives it. It shows up here as soon as the first run starts.",
37
+ toolsOn: "Agents record progress with Paseo tools. The paseo-progress command is a fallback for agents without them.",
38
+ toolsNote: [
39
+ "Let agents record progress without the command.",
40
+ "Paseo can hand agents the Progress tools directly. Nothing to install, and every update shows which agent sent it. Turning this on also lets agents start, message and stop other agents. To turn it on, open Settings → your host → Agents → Enable Paseo tools, then start a new chat or reload one.",
41
+ ],
42
+ agentTools: "Agent tools",
43
+ paseoTools: "Paseo tools",
44
+ toolsStateOn: "On",
45
+ toolsStateOff: "Off",
46
+ toolRow: {
47
+ progress_show: { running: "Reading the dashboard…", name: "Read the dashboard" },
48
+ progress_start: { running: "Starting run…", name: "Start run" },
49
+ progress_ticket_add: { running: "Adding ticket…", name: "Add ticket" },
50
+ progress_ticket_update: { running: "Updating ticket…", name: "Update ticket" },
51
+ progress_question_ask: { running: "Asking question…", name: "Ask question" },
52
+ progress_question_answer: { running: "Recording answer…", name: "Record answer" },
53
+ progress_deliverable_add: { running: "Adding deliverable…", name: "Add deliverable" },
54
+ progress_activity_add: { running: "Logging activity…", name: "Log activity" },
55
+ progress_ticker: { running: "Updating the now line…", name: "Update the now line" },
56
+ progress_finish: { running: "Finishing run…", name: "Finish run" },
57
+ paseo_progress: { running: "Recording progress…", name: "Record progress" },
58
+ },
59
+ toolRowCanceled: (name) => `${name}: canceled`,
60
+ toolRowFailed: (name) => `${name}: failed`,
36
61
  finished: "Finished",
37
62
  lastUpdated: "Last updated ",
38
63
  activity: "Activity",
@@ -33,6 +33,31 @@ export const messages: ProgressMessages = {
33
33
  openSettings: "打开 Progress 设置",
34
34
  noProgress: "尚未记录进度",
35
35
  emptyIntro: ["在此工作树中工作的智能体会通过 ", " 命令记录进度。首次运行开始后,进度就会显示在这里。"],
36
+ emptyIntroTools: "在此工作树中工作的智能体会通过 Paseo 提供的 Progress 工具记录进度。首次运行开始后,进度就会显示在这里。",
37
+ toolsOn: "智能体通过 Paseo tools 记录进度。没有这些工具的智能体会改用 paseo-progress 命令。",
38
+ toolsNote: [
39
+ "让智能体无需命令即可记录进度。",
40
+ "Paseo 可以直接把 Progress 工具交给智能体。无需安装,每条更新都会显示是哪个智能体发送的。开启后,智能体还能启动其他智能体、给它们发消息,或停止它们。开启方法:打开 设置 → 你的主机 → Agents → 启用 Paseo tools,然后新建对话或重新加载一个对话。",
41
+ ],
42
+ agentTools: "智能体工具",
43
+ paseoTools: "Paseo tools",
44
+ toolsStateOn: "已开启",
45
+ toolsStateOff: "已关闭",
46
+ toolRow: {
47
+ progress_show: { running: "正在读取进度面板…", name: "读取进度面板" },
48
+ progress_start: { running: "正在开始运行…", name: "开始运行" },
49
+ progress_ticket_add: { running: "正在添加工单…", name: "添加工单" },
50
+ progress_ticket_update: { running: "正在更新工单…", name: "更新工单" },
51
+ progress_question_ask: { running: "正在提问…", name: "提问" },
52
+ progress_question_answer: { running: "正在记录回答…", name: "记录回答" },
53
+ progress_deliverable_add: { running: "正在添加交付物…", name: "添加交付物" },
54
+ progress_activity_add: { running: "正在记录动态…", name: "记录动态" },
55
+ progress_ticker: { running: "正在更新当前动态…", name: "更新当前动态" },
56
+ progress_finish: { running: "正在结束运行…", name: "结束运行" },
57
+ paseo_progress: { running: "正在记录进度…", name: "记录进度" },
58
+ },
59
+ toolRowCanceled: (name) => `${name}:已取消`,
60
+ toolRowFailed: (name) => `${name}:失败`,
36
61
  finished: "已结束",
37
62
  lastUpdated: "最后更新于 ",
38
63
  activity: "动态",
@@ -2,6 +2,7 @@ import type { PiecedKind } from "../shared/attachments";
2
2
  import type { TicketStatus } from "../shared/events";
3
3
  import type { QuestionMode } from "../shared/question-modes";
4
4
  import type { NoteKind } from "../shared/whats-new";
5
+ import type { ProgressToolName } from "../shared/tools";
5
6
 
6
7
  // Text before and after a value the UI draws in its own style.
7
8
  export type Around = readonly [before: string, after: string];
@@ -16,6 +17,19 @@ export interface ProgressMessages {
16
17
  openSettings: string;
17
18
  noProgress: string;
18
19
  emptyIntro: Around;
20
+ // Paseo 0.14 can hand agents the Progress tools. The note offers it while the host's setting is off.
21
+ toolsOn: string;
22
+ emptyIntroTools: string;
23
+ toolsNote: readonly [lead: string, body: string];
24
+ agentTools: string;
25
+ paseoTools: string;
26
+ toolsStateOn: string;
27
+ toolsStateOff: string;
28
+ // A Progress tool call in the chat: the line while it runs, and the tool's
29
+ // name for a row whose output has nothing better to show.
30
+ toolRow: Record<ProgressToolName, { running: string; name: string }>;
31
+ toolRowCanceled: (name: string) => string;
32
+ toolRowFailed: (name: string) => string;
19
33
  finished: string;
20
34
  lastUpdated: string;
21
35
  activity: string;
package/client/panel.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type PluginWorkspacePanelProps, useRpc, useWorkspace } from "@getpaseo/plugin/client";
2
2
  import { Icon } from "@getpaseo/plugin/client/ui";
3
3
  import { DeliverablesList } from "./deliverables";
4
- import { LauncherBanner, SkillBanner } from "./launcher";
4
+ import { LauncherBanner, SkillBanner, useToolsState } from "./launcher";
5
5
  import { WhatsNewBanner } from "./whats-new";
6
6
  import { HistoryRow } from "./history";
7
7
  import { AnsweredQuestionsList, QuestionsSection } from "./questions";
@@ -65,7 +65,7 @@ function WorkspaceProgress({ theme, workspaceId, host, layout, navigation, openS
65
65
  <LauncherBanner colors={colors} host={host} />
66
66
  <SkillBanner colors={colors} host={host} />
67
67
  {/* Fades in like the dashboard's sections instead of popping in after the spinner. */}
68
- <Presence show={Boolean(result && !result.configured)}>{result && !result.configured ? <EmptyState colors={colors} file={result.file} openSettings={openSettings} /> : null}</Presence>
68
+ <Presence show={Boolean(result && !result.configured)}>{result && !result.configured ? <EmptyState colors={colors} hostId={host.id} file={result.file} openSettings={openSettings} /> : null}</Presence>
69
69
  {/* Keyed by run: a new run starts with fresh tabs, pages and dialogs. */}
70
70
  {result?.configured ? <DashboardView key={result.dashboard.run?.startedAt ?? "none"} colors={colors} openSettings={openSettings} dashboard={result.dashboard} agentRunning={agentRunning} compact={layout.compact}
71
71
  workspaceId={workspaceId} workspaceDirectory={result.root} file={result.file} navigation={navigation} /> : null}
@@ -101,8 +101,9 @@ function SettingsButton({ colors, onPress }: { colors: Colors; onPress(): void }
101
101
  );
102
102
  }
103
103
 
104
- function EmptyState({ colors, file, openSettings }: { colors: Colors; file: string; openSettings(): void }) {
104
+ function EmptyState({ colors, hostId, file, openSettings }: { colors: Colors; hostId: string; file: string; openSettings(): void }) {
105
105
  const { messages: m } = useProgressLanguage();
106
+ const tools = useToolsState(hostId);
106
107
  return (
107
108
  <View style={{ padding: 12, paddingTop: 20, gap: 10 }}>
108
109
  <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
@@ -113,14 +114,26 @@ function EmptyState({ colors, file, openSettings }: { colors: Colors; file: stri
113
114
  <View style={{ flex: 1 }} />
114
115
  <SettingsButton colors={colors} onPress={openSettings} />
115
116
  </View>
116
- <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
117
- {m.emptyIntro[0]}<Text style={{ color: colors.foreground }}>paseo-progress</Text>{m.emptyIntro[1]}
118
- </Text>
119
- <View style={{ backgroundColor: colors.surface1, borderRadius: 6, padding: 10 }}>
120
- <Text selectable style={{ color: colors.foreground, fontFamily: mono(), fontSize: 12, lineHeight: 18 }}>
121
- {`paseo-progress start "Build the export feature"\npaseo-progress ticket add "Ticket 01: Export button" --estimate 60`}
117
+ {tools === "on" ? (
118
+ <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>{m.emptyIntroTools}</Text>
119
+ ) : (
120
+ <>
121
+ <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
122
+ {m.emptyIntro[0]}<Text style={{ color: colors.foreground }}>paseo-progress</Text>{m.emptyIntro[1]}
123
+ </Text>
124
+ <View style={{ backgroundColor: colors.surface1, borderRadius: 6, padding: 10 }}>
125
+ <Text selectable style={{ color: colors.foreground, fontFamily: mono(), fontSize: 12, lineHeight: 18 }}>
126
+ {`paseo-progress start "Build the export feature"\npaseo-progress ticket add "Ticket 01: Export button" --estimate 60`}
127
+ </Text>
128
+ </View>
129
+ </>
130
+ )}
131
+ {/* The other way to start, next to the command it replaces. */}
132
+ {tools === "off" ? (
133
+ <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
134
+ <Text style={{ color: colors.foreground, fontWeight: "600" }}>{m.toolsNote[0]}</Text> {m.toolsNote[1]}
122
135
  </Text>
123
- </View>
136
+ ) : null}
124
137
  {/* So people know nothing lands in the repo. */}
125
138
  <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
126
139
  {m.savedOutside[0]}<Text selectable style={{ color: colors.foreground }}>{file}</Text>{m.savedOutside[1]}
@@ -253,7 +266,7 @@ function DashboardView({ colors, dashboard, agentRunning, compact, workspaceId,
253
266
  ) : (
254
267
  <TicketList colors={colors} dashboard={dashboard} live={live} now={now} workspaceId={workspaceId} onOpen={setOpenTicket} />
255
268
  )}
256
- <TicketDialogs colors={colors} dashboard={dashboard} openId={openTicket} setOpenId={setOpenTicket} now={now} live={live} context={attachmentContext} />
269
+ <TicketDialogs colors={colors} dashboard={dashboard} openId={openTicket} setOpenId={setOpenTicket} now={now} live={live} compact={compact} context={attachmentContext} />
257
270
  </Card>
258
271
  </Presence>
259
272
  </View>
@@ -468,7 +481,7 @@ function FinishedRow({ colors, tickets, shown, onPress }: { colors: Colors; tick
468
481
  const chevron = shown ? "ChevronUp" : "ChevronDown";
469
482
  const narrow = useNarrow();
470
483
  return (
471
- <PressableRow colors={colors} accessibilityRole="button" accessibilityState={{ expanded: shown }}
484
+ <PressableRow colors={colors} accessibilityRole="button" aria-expanded={shown}
472
485
  accessibilityLabel={m.toggleFinished(shown, label)} onPress={onPress}
473
486
  style={{ flexDirection: "row", alignItems: "flex-start", gap: 10, paddingHorizontal: 10, paddingVertical: 8, borderTopWidth: 1, borderTopColor: colors.border }}>
474
487
  <View style={{ width: 16, paddingTop: 2, alignItems: "center" }}>
@@ -32,8 +32,8 @@ export function AttentionPopover({ theme, host, layout, workspaceId, close, open
32
32
  const colors = theme.colors;
33
33
  // Paseo draws the desktop popover on surface1 and the phone sheet on surface0.
34
34
  const onSurface1 = !layout.compact;
35
- // The phone apps are pressed with a finger.
36
- const touch = layout.platform !== "web";
35
+ // Pressed with a finger: the phone apps, and any phone-sized screen, browsers included.
36
+ const touch = layout.compact || layout.platform !== "web";
37
37
  const directory = useWorkspace(workspaceId, (workspace) => workspace.directory);
38
38
  const query = useDashboard(host.id, workspaceId, directory);
39
39
  const dashboard = query.data?.configured ? query.data.dashboard : null;
@@ -210,7 +210,7 @@ function StepButton({ colors, touch, onSurface1, icon, label, target, onPress }:
210
210
  const button = onSurface1 ? { rest: colors.surface2, pressed: colors.surface1 } : { rest: colors.surface1, pressed: colors.surface2 };
211
211
  const size = stepSize(touch);
212
212
  return (
213
- <PressScale accessibilityRole="button" accessibilityLabel={label} accessibilityState={{ disabled: !target }} disabled={!target}
213
+ <PressScale accessibilityRole="button" accessibilityLabel={label} disabled={!target}
214
214
  onPress={target ? () => onPress(target) : undefined} hitSlop={touch ? stepSlop : undefined}
215
215
  style={({ pressed }) => ({ width: size, height: size, alignItems: "center", justifyContent: "center", borderRadius: touch ? 8 : 4, opacity: target ? 1 : 0.35, ...raised(colors),
216
216
  backgroundColor: pressed ? button.pressed : button.rest })}>
@@ -1,6 +1,6 @@
1
1
  import { type PluginSurfaceProps, useRpc, useSettings } from "@getpaseo/plugin/client";
2
2
  import { Icon, Modal } from "@getpaseo/plugin/client/ui";
3
- import { SettingsAction, SettingsCard, SettingsSelect } from "@getpaseo/plugin/client/ui";
3
+ import { SettingsAction, SettingsCard, SettingsRow, SettingsSelect } from "@getpaseo/plugin/client/ui";
4
4
  import { useQuery } from "@tanstack/react-query";
5
5
  import React, { type ReactNode, useState } from "react";
6
6
  import { Text, View } from "react-native";
@@ -10,11 +10,11 @@ import { questionSettings, LANGUAGE_PREFERENCES } from "../shared/settings";
10
10
  import { parseBlocks, withoutTitle } from "../shared/blocks";
11
11
  import { errorText } from "./errors";
12
12
  import { useProgressLanguage } from "./language";
13
- import { useSkill } from "./launcher";
13
+ import { useSkill, useToolsState } from "./launcher";
14
14
  import { BlockView } from "./blocks";
15
15
 
16
16
  export function QuestionSettings({ theme, host }: PluginSurfaceProps) {
17
- return <><QuestionModeSettings theme={theme} /><DisplaySettings theme={theme} /><SkillSettings colors={theme.colors} hostId={host.id} /></>;
17
+ return <><QuestionModeSettings theme={theme} /><DisplaySettings theme={theme} /><SkillSettings colors={theme.colors} hostId={host.id} /><ToolsSettings colors={theme.colors} hostId={host.id} /></>;
18
18
  }
19
19
 
20
20
  type Colors = PluginSurfaceProps["theme"]["colors"];
@@ -76,6 +76,22 @@ function SkillSettings({ colors, hostId }: { colors: Colors; hostId: string }) {
76
76
  );
77
77
  }
78
78
 
79
+ // Paseo 0.14 and later only: whether agents get the Progress tools, and how to turn them on.
80
+ function ToolsSettings({ colors, hostId }: { colors: Colors; hostId: string }) {
81
+ const { messages: m } = useProgressLanguage();
82
+ const tools = useToolsState(hostId);
83
+ if (tools !== "on" && tools !== "off") return null;
84
+ return (
85
+ <Section colors={colors} icon="Wrench" title={m.agentTools}>
86
+ <SettingsCard>
87
+ <SettingsRow label={m.paseoTools} hint={tools === "on" ? m.toolsOn : m.toolsNote[1]}>
88
+ <Text style={{ color: colors.foregroundMuted, fontSize: 13 }}>{tools === "on" ? m.toolsStateOn : m.toolsStateOff}</Text>
89
+ </SettingsRow>
90
+ </SettingsCard>
91
+ </Section>
92
+ );
93
+ }
94
+
79
95
  function QuestionModeSettings({ theme }: Pick<PluginSurfaceProps, "theme">) {
80
96
  const { messages: m } = useProgressLanguage();
81
97
  const settings = useSettings(questionSettings);