@stevecastaneda/paseo-progress 1.10.0 → 1.12.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 +2 -2
- package/README.md +14 -3
- package/client/attachments.tsx +1 -1
- package/client/blocks.tsx +41 -0
- package/client/deliverables.tsx +1 -1
- package/client/dialog-stack.tsx +2 -2
- package/client/history.tsx +1 -1
- package/client/launcher.tsx +1 -1
- package/client/panel.tsx +1 -1
- package/client/pill-popover.tsx +1 -1
- package/client/pills.tsx +4 -13
- package/client/preview.tsx +50 -17
- package/client/questions.tsx +1 -1
- package/client/section-title.tsx +1 -1
- package/client/settings.tsx +4 -4
- package/client/show-more.tsx +1 -1
- package/client/spinner.tsx +1 -1
- package/client/tabs.tsx +1 -1
- package/client/ticket-dialog.tsx +1 -1
- package/client/whats-new.tsx +3 -3
- package/client/write-up.tsx +25 -44
- package/dist/server/cli.js +29 -16
- package/index.client.tsx +1 -2
- package/package.json +3 -3
- package/paseo-plugin.json +1 -1
- package/server/cli.ts +28 -13
- package/server/open.ts +30 -24
- package/server/whats-new.ts +1 -1
- package/shared/attachments.ts +19 -4
- package/shared/{write-up.ts → blocks.ts} +1 -21
- package/shared/dashboard.ts +1 -0
- package/shared/events.ts +1 -1
- package/shared/markdown.ts +483 -0
- package/shared/rpc.ts +4 -2
- package/skills/paseo-progress/SKILL.md +1 -1
package/OVERVIEW.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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.
|
|
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.
|
|
2
2
|
|
|
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.
|
|
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, with Markdown formatted, 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
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
|
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Progress
|
|
4
4
|
|
|
5
|
-
A plugin for Paseo 0.
|
|
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.
|
|
6
6
|
|
|
7
7
|
<img src="images/1-panel.png" alt="The Progress panel in Explorer" width="440">
|
|
8
8
|
|
|
@@ -24,7 +24,7 @@ The panel lives in Explorer, beside the agent chat, so you can watch both. Open
|
|
|
24
24
|
|
|
25
25
|
<img src="images/4-pill.png" alt="The pill above the message box, showing two questions waiting" width="600">
|
|
26
26
|
|
|
27
|
-
On phones
|
|
27
|
+
On phones, Progress opens in Explorer too. On any phone, the pill stays above the message box while a run is open, so there's always a way in. SVG files and videos preview on desktop; on phones they open on the daemon host.
|
|
28
28
|
|
|
29
29
|
The first time a run starts in a worktree whose Progress panel has never been opened, the pill shows a spinner and reads **Progress** instead; pressing it opens the panel. Once the panel has been opened there, the plugin writes an empty `progress-panel-opened` file next to the progress file and the nudge never comes back, even for later runs. It doesn't open the panel by itself because Paseo switches to a workspace to open its panel, which would pull you away from whatever you're looking at.
|
|
30
30
|
|
|
@@ -120,7 +120,7 @@ paseo-progress finish "Snapshots ship for all four tables"
|
|
|
120
120
|
- 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.
|
|
121
121
|
- `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.
|
|
122
122
|
- 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
|
-
- 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.
|
|
123
|
+
- 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. 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
124
|
- 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
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.
|
|
126
126
|
- `start` begins a fresh dashboard. Earlier runs stay in the file. Question references stay unique across runs.
|
|
@@ -145,6 +145,17 @@ The progress file, `progress.jsonl`, holds one JSON event per line, each with a
|
|
|
145
145
|
|
|
146
146
|
## Changelog
|
|
147
147
|
|
|
148
|
+
### 1.12.0
|
|
149
|
+
|
|
150
|
+
- [Improved] **Markdown deliverables show formatted.** A `.md` or `.markdown` file now opens in the preview with its headings, lists, tables, code blocks and images drawn by Paseo's own Markdown view. An image with a relative path loads from the file's own folder. A link to another file in the worktree opens that file in Paseo, except on a phone, where Paseo doesn't open file links from a plugin's dialog. MDX files still show as plain text, because Paseo would hide their components.
|
|
151
|
+
- [Improved] **Ticket write-ups show formatted.** A ticket's write-up is drawn the same way. Its headings stay smaller than the ticket's title, and front matter stays out of the short version.
|
|
152
|
+
- [Fixed] **Long previews end on a whole line.** A text file over 256 KB used to stop wherever the 256 KB ran out, sometimes in the middle of a character. Now it stops at the last whole line, above the note that says the file goes on.
|
|
153
|
+
- [Improved] **Needs Paseo 0.13 or later.** Progress now uses Paseo 0.13's plugin interface and its Markdown view. On an older Paseo, the version you already have keeps working.
|
|
154
|
+
|
|
155
|
+
### 1.11.0
|
|
156
|
+
|
|
157
|
+
- [Improved] **Agents can change their recommendation.** When you ask an agent about an open question and explaining it changes the agent's mind, it can now change its recommendation, and the question card shows the new one. In Keep building mode, the agent also switches the work it already built to the new recommendation.
|
|
158
|
+
|
|
148
159
|
### 1.10.0
|
|
149
160
|
|
|
150
161
|
- [Improved] **Progress opens in Explorer on phones.** On Paseo 0.13 or later, the phone app opens Progress in Explorer, as the desktop app does, and lists it in Explorer's New tab menu. If a Progress tab from an earlier version is still open on your phone, you can close it.
|
package/client/attachments.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { openExternalUrl, type PluginWorkspacePanelProps, useRpc } from "@getpaseo/plugin/client";
|
|
2
|
-
import { copyText, Icon, useToast } from "@getpaseo/plugin/client/
|
|
2
|
+
import { copyText, Icon, useToast } from "@getpaseo/plugin/client/ui";
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { Platform, Text, View } from "react-native";
|
|
5
5
|
import { type Attachment, attachmentIcon, canPreview, opensAsPage, shortName } from "../shared/attachments";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Platform, Text, View } from "react-native";
|
|
4
|
+
import { type Block, type Span } from "../shared/blocks";
|
|
5
|
+
|
|
6
|
+
type Colors = PluginWorkspacePanelProps["theme"]["colors"];
|
|
7
|
+
const MONO = Platform.OS === "ios" ? "Menlo" : "monospace";
|
|
8
|
+
|
|
9
|
+
export function BlockView({ colors, block, first }: { colors: Colors; block: Block; first: boolean }) {
|
|
10
|
+
switch (block.kind) {
|
|
11
|
+
case "heading":
|
|
12
|
+
return (
|
|
13
|
+
// A step under the section's own 13px title: smaller and muted, set apart from the body by weight.
|
|
14
|
+
<Text accessibilityRole="header" selectable style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17, fontWeight: "600", marginTop: first ? 0 : 6 }}>
|
|
15
|
+
<Spans colors={colors} spans={block.spans} />
|
|
16
|
+
</Text>
|
|
17
|
+
);
|
|
18
|
+
case "item":
|
|
19
|
+
return (
|
|
20
|
+
<View style={{ flexDirection: "row", gap: 6, paddingStart: block.depth * 14 }}>
|
|
21
|
+
<Text style={{ minWidth: 10, color: colors.foregroundMuted, fontSize: 13, lineHeight: 19, fontVariant: ["tabular-nums"] }}>{block.marker}</Text>
|
|
22
|
+
<Text selectable style={{ flex: 1, color: colors.foreground, fontSize: 13, lineHeight: 19 }}><Spans colors={colors} spans={block.spans} /></Text>
|
|
23
|
+
</View>
|
|
24
|
+
);
|
|
25
|
+
case "code":
|
|
26
|
+
return (
|
|
27
|
+
<View style={{ padding: 8, borderRadius: 6, backgroundColor: colors.surface0 }}>
|
|
28
|
+
<Text selectable style={{ color: colors.foreground, fontFamily: MONO, fontSize: 12, lineHeight: 18 }}>{block.text}</Text>
|
|
29
|
+
</View>
|
|
30
|
+
);
|
|
31
|
+
default:
|
|
32
|
+
return <Text selectable style={{ color: colors.foreground, fontSize: 13, lineHeight: 19 }}><Spans colors={colors} spans={block.spans} /></Text>;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function Spans({ colors, spans }: { colors: Colors; spans: Span[] }) {
|
|
37
|
+
return <>{spans.map((span, index) => span.code
|
|
38
|
+
? <Text key={index} style={{ fontFamily: MONO, fontSize: 12, color: colors.foreground }}>{span.text}</Text>
|
|
39
|
+
: span.strong ? <Text key={index} style={{ fontWeight: "600" }}>{span.text}</Text>
|
|
40
|
+
: <React.Fragment key={index}>{span.text}</React.Fragment>)}</>;
|
|
41
|
+
}
|
package/client/deliverables.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
|
|
2
|
-
import { Icon } from "@getpaseo/plugin/client/
|
|
2
|
+
import { Icon } from "@getpaseo/plugin/client/ui";
|
|
3
3
|
import React, { useState } from "react";
|
|
4
4
|
import { Text, View } from "react-native";
|
|
5
5
|
import type { Deliverable } from "../shared/dashboard";
|
package/client/dialog-stack.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
|
|
2
|
-
import { Icon, Modal } from "@getpaseo/plugin/client/
|
|
2
|
+
import { Icon, Modal } from "@getpaseo/plugin/client/ui";
|
|
3
3
|
import React, { useEffect, useRef, useState } from "react";
|
|
4
4
|
import { Platform } from "react-native";
|
|
5
5
|
import { opensAsPage } from "../shared/attachments";
|
|
@@ -98,7 +98,7 @@ export function StackedDialog({ colors, root, onClose, questions = [], dashboard
|
|
|
98
98
|
onOpenOnHost={() => { onClose(); void opener.openOnHost(preview.attachment); }}
|
|
99
99
|
onCopyPath={() => void opener.copyPath(preview.attachment)} onBack={back}
|
|
100
100
|
gallery={{ items: preview.group, onSelect: (attachment) => replaceTop({ ...preview, attachment, fullScreen: preview.fullScreen && previewKind(attachment.path ?? "") === "image", leftFullScreen: false }) }}
|
|
101
|
-
onLong={setPreviewLong} fullScreen={preview.fullScreen} leftFullScreen={preview.leftFullScreen === true} onFullScreen={setFullScreen}
|
|
101
|
+
onLong={setPreviewLong} ownScroll={previewLong || preview.fullScreen} fullScreen={preview.fullScreen} leftFullScreen={preview.leftFullScreen === true} onFullScreen={setFullScreen}
|
|
102
102
|
onOpenEntry={openAttachment} />
|
|
103
103
|
) : (
|
|
104
104
|
<>
|
package/client/history.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type PluginWorkspacePanelProps, useRpc } from "@getpaseo/plugin/client";
|
|
2
|
-
import { Icon, Modal, useToast } from "@getpaseo/plugin/client/
|
|
2
|
+
import { Icon, Modal, useToast } from "@getpaseo/plugin/client/ui";
|
|
3
3
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
4
|
import React, { useState } from "react";
|
|
5
5
|
import { ActivityIndicator, ScrollView, Text, TextInput, View } from "react-native";
|
package/client/launcher.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type PluginWorkspacePanelProps, useRpc } from "@getpaseo/plugin/client";
|
|
2
|
-
import { Icon, useToast } from "@getpaseo/plugin/client/
|
|
2
|
+
import { Icon, useToast } from "@getpaseo/plugin/client/ui";
|
|
3
3
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
4
|
import React from "react";
|
|
5
5
|
import { Text, View } from "react-native";
|
package/client/panel.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type PluginWorkspacePanelProps, useRpc, useWorkspace } from "@getpaseo/plugin/client";
|
|
2
|
-
import { Icon } from "@getpaseo/plugin/client/
|
|
2
|
+
import { Icon } from "@getpaseo/plugin/client/ui";
|
|
3
3
|
import { DeliverablesList } from "./deliverables";
|
|
4
4
|
import { LauncherBanner, SkillBanner } from "./launcher";
|
|
5
5
|
import { WhatsNewBanner } from "./whats-new";
|
package/client/pill-popover.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type PluginButtonContentProps, useWorkspace } from "@getpaseo/plugin/client";
|
|
2
|
-
import { Icon, ScrollView } from "@getpaseo/plugin/client/
|
|
2
|
+
import { Icon, ScrollView } from "@getpaseo/plugin/client/ui";
|
|
3
3
|
import React, { useRef, useState } from "react";
|
|
4
4
|
import { Text, View } from "react-native";
|
|
5
5
|
import type { Dashboard } from "../shared/dashboard";
|
package/client/pills.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PluginButtonContentProps, PluginButtonIconProps, PluginButtonRegistration, PluginClientContext } from "@getpaseo/plugin/client";
|
|
2
|
-
import { Icon } from "@getpaseo/plugin/client/
|
|
2
|
+
import { Icon } from "@getpaseo/plugin/client/ui";
|
|
3
3
|
import React, { useEffect, useSyncExternalStore } from "react";
|
|
4
4
|
import { Platform } from "react-native";
|
|
5
5
|
import { type AttentionResult, getAttention } from "../shared/rpc";
|
|
@@ -18,17 +18,8 @@ export const PILL_POLL_MS = 5_000;
|
|
|
18
18
|
// still trigger a check, at most once a poll interval.
|
|
19
19
|
export const IDLE_POLL_EVERY = 6;
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// The tab has its own id: Paseo 0.10.1 reopens an existing tab where it
|
|
24
|
-
// already is, and earlier versions of this plugin left a "progress" tab in the
|
|
25
|
-
// phone's hidden Explorer pane, which a plugin can't move or close.
|
|
26
|
-
type ProgressPanelPlacement = { id: "progress"; location: "explorer" } | { id: "progress-tab"; location: "workspace" };
|
|
27
|
-
|
|
28
|
-
export function progressPanel(client: PluginClientContext): ProgressPanelPlacement {
|
|
29
|
-
return Platform.OS === "web" || "addFilePanel" in client
|
|
30
|
-
? { id: "progress", location: "explorer" }
|
|
31
|
-
: { id: "progress-tab", location: "workspace" };
|
|
21
|
+
export function progressPanel(): { id: "progress"; location: "explorer" } {
|
|
22
|
+
return { id: "progress", location: "explorer" };
|
|
32
23
|
}
|
|
33
24
|
|
|
34
25
|
type Workspace = { id: string; workspaceDirectory?: string; projectRootPath: string };
|
|
@@ -87,7 +78,7 @@ export function contributePills(client: PluginClientContext) {
|
|
|
87
78
|
);
|
|
88
79
|
}
|
|
89
80
|
|
|
90
|
-
const panel = progressPanel(
|
|
81
|
+
const panel = progressPanel();
|
|
91
82
|
const openPanel = (workspaceId: string) => {
|
|
92
83
|
client.openPanel(panel.id, { workspaceId, location: panel.location });
|
|
93
84
|
};
|
package/client/preview.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type PluginWorkspacePanelProps, useRpc } from "@getpaseo/plugin/client";
|
|
2
|
-
import { Icon, ScrollView } from "@getpaseo/plugin/client/
|
|
2
|
+
import { Icon, Markdown, ScrollView } from "@getpaseo/plugin/client/ui";
|
|
3
3
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
-
import React, { useEffect, useRef, useState } from "react";
|
|
5
|
-
import { ActivityIndicator, Image, Platform, Pressable, Text, useWindowDimensions, View, type ViewStyle } from "react-native";
|
|
4
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { ActivityIndicator, Image, type LayoutChangeEvent, Platform, Pressable, Text, useWindowDimensions, View, type ViewStyle } from "react-native";
|
|
6
6
|
import { ATTACHMENT_ROW, type Attachment, AttachmentList, isPreviewable } from "./attachments";
|
|
7
7
|
import { errorText } from "./errors";
|
|
8
8
|
import { folderAttachments, isFolder, previewKind } from "../shared/attachments";
|
|
@@ -39,8 +39,9 @@ type PreviewProps = {
|
|
|
39
39
|
|
|
40
40
|
// `onLong` says when the file is too tall for the dialog, so the host turns off
|
|
41
41
|
// its own scrolling and the file scrolls with the actions pinned below.
|
|
42
|
-
export function PreviewContent({ colors, attachment, workspaceId, workspaceDirectory, onOpenOnHost, onCopyPath, onBack, gallery, onOpenEntry, onLong, fullScreen, leftFullScreen, onFullScreen }: PreviewProps & {
|
|
42
|
+
export function PreviewContent({ colors, attachment, workspaceId, workspaceDirectory, onOpenOnHost, onCopyPath, onBack, gallery, onOpenEntry, onLong, ownScroll, fullScreen, leftFullScreen, onFullScreen }: PreviewProps & {
|
|
43
43
|
onLong(long: boolean): void;
|
|
44
|
+
ownScroll: boolean;
|
|
44
45
|
fullScreen: boolean;
|
|
45
46
|
leftFullScreen: boolean;
|
|
46
47
|
onFullScreen(on: boolean): void;
|
|
@@ -146,22 +147,40 @@ export function PreviewContent({ colors, attachment, workspaceId, workspaceDirec
|
|
|
146
147
|
const room = roomForContent(windowHeight);
|
|
147
148
|
const maxMediaHeight = preview.data?.kind === "video" ? Math.min(640, Math.max(200, room)) : 640;
|
|
148
149
|
const mediaStyle = { width: "100%", aspectRatio, maxHeight: maxMediaHeight, borderRadius: 6, ...imageOutline(colors), backgroundColor: colors.surface1, opacity: stepping ? 0.6 : 1 } as const;
|
|
150
|
+
// Markdown's height depends on the user's text size, which a plugin can't read,
|
|
151
|
+
// so only a file far too tall for any size is long before it's drawn. Anything
|
|
152
|
+
// else is measured, and once it's too tall it stays long: Paseo remounting the
|
|
153
|
+
// content when the dialog stops scrolling, an image that fails and shrinks, or a
|
|
154
|
+
// diagram that grows can't flip it back. Kept in the query cache to survive the remount.
|
|
155
|
+
const longKey = ["progress", "preview-long", workspaceId, attachment.ref, attachment.path];
|
|
156
|
+
const latched = useQuery({ queryKey: longKey, queryFn: () => false, enabled: false }).data === true;
|
|
157
|
+
const data = preview.data;
|
|
158
|
+
const markdown = data?.kind === "markdown" ? data.markdown : null;
|
|
159
|
+
const floor = useMemo(() => (markdown === null ? 0 : markdownFloor(markdown)), [markdown]);
|
|
160
|
+
const certainlyLong = floor > 2 * room;
|
|
161
|
+
const lastLong = useRef(false);
|
|
149
162
|
// A long file gets its own scroll area so the actions below it stay in view.
|
|
150
163
|
// Short ones keep the dialog sized to them.
|
|
151
|
-
const long = estimatedHeight(
|
|
164
|
+
const long = data?.kind === "markdown" ? (stepping ? lastLong.current : certainlyLong || latched) : estimatedHeight(data, aspectRatio, maxMediaHeight) > room;
|
|
165
|
+
lastLong.current = long;
|
|
152
166
|
useEffect(() => onLong(long), [long]);
|
|
167
|
+
const measure = !long && !stepping ? (event: LayoutChangeEvent) => {
|
|
168
|
+
if (event.nativeEvent.layout.height > room) queryClient.setQueryData(longKey, true);
|
|
169
|
+
} : undefined;
|
|
153
170
|
// How far a video has loaded: under the spinner, or in place of the path while
|
|
154
171
|
// the last file stays on screen, so nothing moves.
|
|
155
172
|
const videoLoading = preview.isFetching && loading !== undefined && loading < 1 ? m.loadingVideo(Math.floor(loading * 100)) : null;
|
|
156
173
|
const caption = { color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 } as const;
|
|
174
|
+
const loadingState = (
|
|
175
|
+
<View style={{ alignItems: "center", gap: 8, padding: 24 }}>
|
|
176
|
+
<ActivityIndicator color={colors.foregroundMuted} accessibilityLabel={m.loadingPreview} />
|
|
177
|
+
{videoLoading ? <Text style={[caption, { fontVariant: ["tabular-nums"] }]}>{videoLoading}</Text> : null}
|
|
178
|
+
</View>
|
|
179
|
+
);
|
|
180
|
+
const truncatedNote = (paddingTop: number) => <Text style={{ color: colors.foregroundMuted, fontSize: 11, lineHeight: 16, paddingTop }}>{m.truncatedText}{canOpen ? m.openRest : ""}</Text>;
|
|
157
181
|
const content = (
|
|
158
182
|
<>
|
|
159
|
-
{preview.isPending ?
|
|
160
|
-
<View style={{ alignItems: "center", gap: 8, padding: 24 }}>
|
|
161
|
-
<ActivityIndicator color={colors.foregroundMuted} accessibilityLabel={m.loadingPreview} />
|
|
162
|
-
{videoLoading ? <Text style={[caption, { fontVariant: ["tabular-nums"] }]}>{videoLoading}</Text> : null}
|
|
163
|
-
</View>
|
|
164
|
-
) : null}
|
|
183
|
+
{preview.isPending ? loadingState : null}
|
|
165
184
|
{preview.error ? (
|
|
166
185
|
<Text accessibilityRole="alert" selectable style={{ color: colors.statusDanger, fontSize: 12, lineHeight: 18 }}>
|
|
167
186
|
{m.previewError(preview.error instanceof VideoChangedError ? m.videoChanged : errorText(preview.error, m.unknownError))}
|
|
@@ -172,13 +191,16 @@ export function PreviewContent({ colors, attachment, workspaceId, workspaceDirec
|
|
|
172
191
|
<Text selectable style={{ color: colors.foreground, fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace", fontSize: 12, lineHeight: 18 }}>
|
|
173
192
|
{preview.data.text}
|
|
174
193
|
</Text>
|
|
175
|
-
{preview.data.truncated ? (
|
|
176
|
-
<Text style={{ color: colors.foregroundMuted, fontSize: 11, lineHeight: 16, paddingTop: 8 }}>
|
|
177
|
-
{m.truncatedText}{canOpen ? m.openRest : ""}
|
|
178
|
-
</Text>
|
|
179
|
-
) : null}
|
|
194
|
+
{preview.data.truncated ? truncatedNote(8) : null}
|
|
180
195
|
</View>
|
|
181
196
|
) : null}
|
|
197
|
+
{/* Straight on the dialog: Paseo's code blocks and tables bring their own fills. */}
|
|
198
|
+
{preview.data?.kind === "markdown" ? (certainlyLong && !ownScroll ? loadingState : (
|
|
199
|
+
<View onLayout={measure} style={{ gap: 12, opacity: stepping ? 0.6 : 1 }}>
|
|
200
|
+
<Markdown>{preview.data.markdown}</Markdown>
|
|
201
|
+
{preview.data.truncated ? truncatedNote(0) : null}
|
|
202
|
+
</View>
|
|
203
|
+
)) : null}
|
|
182
204
|
{preview.data?.kind === "folder" ? (
|
|
183
205
|
<FolderList colors={colors} folder={attachment} entries={folderAttachments(attachment, preview.data.entries)} truncated={preview.data.truncated}
|
|
184
206
|
dimmed={stepping} onOpen={onOpenEntry} />
|
|
@@ -364,6 +386,17 @@ function estimatedHeight(data: { kind: "image" | "video" | "html" } | { kind: "t
|
|
|
364
386
|
return lines * 18 + 20;
|
|
365
387
|
}
|
|
366
388
|
|
|
389
|
+
// Paseo's smallest text sizes draw no line under 13px: 9px code at 1.45, 10px
|
|
390
|
+
// prose at 1.4. Paseo draws every line break in Markdown, so each non-blank line
|
|
391
|
+
// takes at least one line.
|
|
392
|
+
const MIN_MARKDOWN_LINE = 12;
|
|
393
|
+
|
|
394
|
+
export function markdownFloor(markdown: string): number {
|
|
395
|
+
let lines = 0;
|
|
396
|
+
for (const line of markdown.split("\n")) if (line.trim()) lines++;
|
|
397
|
+
return lines * MIN_MARKDOWN_LINE;
|
|
398
|
+
}
|
|
399
|
+
|
|
367
400
|
function FolderList({ colors, folder, entries, truncated, dimmed, onOpen }: { colors: Colors; folder: Attachment; entries: Attachment[]; truncated: boolean; dimmed: boolean; onOpen(attachment: Attachment, group: Attachment[]): void }) {
|
|
368
401
|
const { messages: m } = useProgressLanguage();
|
|
369
402
|
const caption = { color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 } as const;
|
|
@@ -388,7 +421,7 @@ function imageSize(uri: string): Promise<{ width: number | null; ratio: number |
|
|
|
388
421
|
// Height the dialog has for the preview before it must scroll: Paseo caps the card
|
|
389
422
|
// at 85% of the window (inside a 24px margin), less its header and padding, the
|
|
390
423
|
// path caption (17px after an 8px gap) and the footer (30px after a 20px gap).
|
|
391
|
-
function roomForContent(windowHeight: number): number {
|
|
424
|
+
export function roomForContent(windowHeight: number): number {
|
|
392
425
|
return (windowHeight - 48) * 0.85 - 57 - 48 - (8 + 17) - (20 + 30);
|
|
393
426
|
}
|
|
394
427
|
|
package/client/questions.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
|
|
2
|
-
import { copyText, Icon, useToast } from "@getpaseo/plugin/client/
|
|
2
|
+
import { copyText, Icon, useToast } from "@getpaseo/plugin/client/ui";
|
|
3
3
|
import React, { useState } from "react";
|
|
4
4
|
import { Text, View } from "react-native";
|
|
5
5
|
import type { Question } from "../shared/dashboard";
|
package/client/section-title.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
|
|
2
|
-
import { Icon } from "@getpaseo/plugin/client/
|
|
2
|
+
import { Icon } from "@getpaseo/plugin/client/ui";
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { Text, View, type ViewStyle } from "react-native";
|
|
5
5
|
|
package/client/settings.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type PluginSurfaceProps, useRpc, useSettings } from "@getpaseo/plugin/client";
|
|
2
|
-
import { Icon, Modal } from "@getpaseo/plugin/client/
|
|
2
|
+
import { Icon, Modal } from "@getpaseo/plugin/client/ui";
|
|
3
3
|
import { SettingsAction, SettingsCard, SettingsSelect } from "@getpaseo/plugin/client/ui";
|
|
4
4
|
import { useQuery } from "@tanstack/react-query";
|
|
5
5
|
import React, { type ReactNode, useState } from "react";
|
|
@@ -7,11 +7,11 @@ import { Text, View } from "react-native";
|
|
|
7
7
|
import { DEFAULT_QUESTION_MODE, QUESTION_MODES } from "../shared/question-modes";
|
|
8
8
|
import { readSkill } from "../shared/rpc";
|
|
9
9
|
import { questionSettings, LANGUAGE_PREFERENCES } from "../shared/settings";
|
|
10
|
-
import {
|
|
10
|
+
import { parseBlocks, withoutTitle } from "../shared/blocks";
|
|
11
11
|
import { errorText } from "./errors";
|
|
12
12
|
import { useProgressLanguage } from "./language";
|
|
13
13
|
import { useSkill } from "./launcher";
|
|
14
|
-
import { BlockView } from "./
|
|
14
|
+
import { BlockView } from "./blocks";
|
|
15
15
|
|
|
16
16
|
export function QuestionSettings({ theme, host }: PluginSurfaceProps) {
|
|
17
17
|
return <><QuestionModeSettings theme={theme} /><DisplaySettings theme={theme} /><SkillSettings colors={theme.colors} hostId={host.id} /></>;
|
|
@@ -53,7 +53,7 @@ function SkillSettings({ colors, hostId }: { colors: Colors; hostId: string }) {
|
|
|
53
53
|
const fetchSkill = useRpc(readSkill);
|
|
54
54
|
const document = useQuery({ queryKey: ["progress", "skill-document", hostId], queryFn: () => fetchSkill({}), staleTime: 60_000 });
|
|
55
55
|
const [open, setOpen] = useState(false);
|
|
56
|
-
const blocks = document.data ? withoutTitle(
|
|
56
|
+
const blocks = document.data ? withoutTitle(parseBlocks(document.data.markdown)) : [];
|
|
57
57
|
const muted = { color: colors.foregroundMuted, fontSize: 13, lineHeight: 19 };
|
|
58
58
|
return (
|
|
59
59
|
<Section colors={colors} icon="Sparkles" title={m.agentSkill}>
|
package/client/show-more.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
|
|
2
|
-
import { Icon } from "@getpaseo/plugin/client/
|
|
2
|
+
import { Icon } from "@getpaseo/plugin/client/ui";
|
|
3
3
|
import React, { useState } from "react";
|
|
4
4
|
import { Text } from "react-native";
|
|
5
5
|
import { IconSwap } from "./motion";
|
package/client/spinner.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Icon } from "@getpaseo/plugin/client/
|
|
1
|
+
import { Icon } from "@getpaseo/plugin/client/ui";
|
|
2
2
|
import React, { useEffect, useRef } from "react";
|
|
3
3
|
import { ActivityIndicator, Animated, Easing, Platform, View } from "react-native";
|
|
4
4
|
import { useProgressLanguage } from "./language";
|
package/client/tabs.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
|
|
2
|
-
import { Icon } from "@getpaseo/plugin/client/
|
|
2
|
+
import { Icon } from "@getpaseo/plugin/client/ui";
|
|
3
3
|
import React, { useState } from "react";
|
|
4
4
|
import { Pressable, Text, View } from "react-native";
|
|
5
5
|
|
package/client/ticket-dialog.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type PluginWorkspacePanelProps, useAgent } from "@getpaseo/plugin/client";
|
|
2
|
-
import { Icon } from "@getpaseo/plugin/client/
|
|
2
|
+
import { Icon } from "@getpaseo/plugin/client/ui";
|
|
3
3
|
import React, { useState } from "react";
|
|
4
4
|
import { Text, View } from "react-native";
|
|
5
5
|
import type { Dashboard, Ticket } from "../shared/dashboard";
|
package/client/whats-new.tsx
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { type PluginWorkspacePanelProps, useRpc } from "@getpaseo/plugin/client";
|
|
2
|
-
import { Icon, Modal } from "@getpaseo/plugin/client/
|
|
2
|
+
import { Icon, Modal } from "@getpaseo/plugin/client/ui";
|
|
3
3
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
4
|
import React, { useState } from "react";
|
|
5
5
|
import { Text, View } from "react-native";
|
|
6
6
|
import { dismissWhatsNew, getWhatsNew } from "../shared/rpc";
|
|
7
7
|
import { NOTE_KINDS, type NoteKind, type NoteTone, type WhatsNew, type WhatsNewNote, type WhatsNewRelease } from "../shared/whats-new";
|
|
8
|
-
import { spans } from "../shared/
|
|
8
|
+
import { spans } from "../shared/blocks";
|
|
9
9
|
import { PressScale, Presence } from "./motion";
|
|
10
10
|
import { isLight, raised, tint } from "./surfaces";
|
|
11
|
-
import { BlockView, Spans } from "./
|
|
11
|
+
import { BlockView, Spans } from "./blocks";
|
|
12
12
|
import { ProgressLanguageProvider, useProgressLanguage } from "./language";
|
|
13
13
|
|
|
14
14
|
type Colors = PluginWorkspacePanelProps["theme"]["colors"];
|
package/client/write-up.tsx
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { type PluginWorkspacePanelProps, useRpc } from "@getpaseo/plugin/client";
|
|
2
|
-
import { Icon } from "@getpaseo/plugin/client/
|
|
2
|
+
import { Icon, Markdown } from "@getpaseo/plugin/client/ui";
|
|
3
3
|
import { useQuery } from "@tanstack/react-query";
|
|
4
|
-
import React, { useState } from "react";
|
|
4
|
+
import React, { useMemo, useState } from "react";
|
|
5
5
|
import { Platform, Text, View } from "react-native";
|
|
6
6
|
import { previewKind } from "../shared/attachments";
|
|
7
7
|
import { previewDeliverable } from "../shared/rpc";
|
|
8
|
-
import {
|
|
8
|
+
import { flattenHeadings, dropTitle, markdownExcerpt, textExcerpt } from "../shared/markdown";
|
|
9
9
|
import { type Attachment, attachmentIcon } from "./attachments";
|
|
10
10
|
import { PressableRow } from "./row";
|
|
11
11
|
import { errorText } from "./errors";
|
|
12
12
|
import { useProgressLanguage } from "./language";
|
|
13
13
|
|
|
14
14
|
type Colors = PluginWorkspacePanelProps["theme"]["colors"];
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
const WRITE_UP_HEADING = 5;
|
|
17
|
+
const EXCERPT_CHARS = 280;
|
|
16
18
|
|
|
17
19
|
// What the ticket is: the start of its write-up in a quiet inset, with the
|
|
18
20
|
// rest a press away. A link or a file that isn't text shows only its name.
|
|
@@ -34,18 +36,31 @@ export function WriteUp({ colors, source, workspaceId, workspaceDirectory, onOpe
|
|
|
34
36
|
enabled: readable,
|
|
35
37
|
});
|
|
36
38
|
const [all, setAll] = useState(false);
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
39
|
+
const data = preview.data;
|
|
40
|
+
const parts = useMemo(() => {
|
|
41
|
+
if (data?.kind === "markdown") {
|
|
42
|
+
const excerpt = markdownExcerpt(data.markdown, EXCERPT_CHARS);
|
|
43
|
+
return { markdown: true, excerpt: flattenHeadings(excerpt.text, WRITE_UP_HEADING), whole: flattenHeadings(dropTitle(data.markdown), WRITE_UP_HEADING), more: excerpt.more, truncated: data.truncated };
|
|
44
|
+
}
|
|
45
|
+
if (data?.kind === "text") {
|
|
46
|
+
const excerpt = textExcerpt(data.text, EXCERPT_CHARS);
|
|
47
|
+
return { markdown: false, excerpt: excerpt.text, whole: data.text, more: excerpt.more, truncated: data.truncated };
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}, [data]);
|
|
51
|
+
const shown = parts ? (all ? parts.whole : parts.excerpt) : "";
|
|
52
|
+
const muted = { color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 } as const;
|
|
40
53
|
return (
|
|
41
54
|
<View style={{ gap: 12, padding: 14, borderRadius: 8, backgroundColor: colors.surface0 }}>
|
|
42
|
-
{shown.
|
|
43
|
-
|
|
55
|
+
{shown.trim() ? (parts?.markdown ? <Markdown>{shown}</Markdown>
|
|
56
|
+
: <Text selectable style={{ color: colors.foreground, fontSize: 13, lineHeight: 19 }}>{shown}</Text>) : null}
|
|
57
|
+
{all && parts?.truncated ? <Text style={muted}>{m.truncatedText}</Text> : null}
|
|
58
|
+
{preview.error ? <Text style={muted}>{m.readError(errorText(preview.error, m.unknownError))}</Text> : null}
|
|
44
59
|
{/* The file on the left, opening it whole; expanding in place on the right. */}
|
|
45
60
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
|
|
46
61
|
<TextButton colors={colors} icon={attachmentIcon(source)} label={source.title} accessibilityLabel={m.openItem(source.title)} onPress={() => onOpen(source, [source])} grow />
|
|
47
62
|
<View style={{ flex: 1 }} />
|
|
48
|
-
{
|
|
63
|
+
{parts?.more ? <TextButton colors={colors} label={all ? m.showLess : m.readMore} onPress={() => setAll(!all)} expanded={all} /> : null}
|
|
49
64
|
</View>
|
|
50
65
|
</View>
|
|
51
66
|
);
|
|
@@ -63,37 +78,3 @@ function TextButton({ colors, icon, label, accessibilityLabel, onPress, grow = f
|
|
|
63
78
|
</PressableRow>
|
|
64
79
|
);
|
|
65
80
|
}
|
|
66
|
-
|
|
67
|
-
export function BlockView({ colors, block, first }: { colors: Colors; block: Block; first: boolean }) {
|
|
68
|
-
switch (block.kind) {
|
|
69
|
-
case "heading":
|
|
70
|
-
return (
|
|
71
|
-
// A step under the section's own 13px title: smaller and muted, set apart from the body by weight.
|
|
72
|
-
<Text accessibilityRole="header" selectable style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17, fontWeight: "600", marginTop: first ? 0 : 6 }}>
|
|
73
|
-
<Spans colors={colors} spans={block.spans} />
|
|
74
|
-
</Text>
|
|
75
|
-
);
|
|
76
|
-
case "item":
|
|
77
|
-
return (
|
|
78
|
-
<View style={{ flexDirection: "row", gap: 6, paddingStart: block.depth * 14 }}>
|
|
79
|
-
<Text style={{ minWidth: 10, color: colors.foregroundMuted, fontSize: 13, lineHeight: 19, fontVariant: ["tabular-nums"] }}>{block.marker}</Text>
|
|
80
|
-
<Text selectable style={{ flex: 1, color: colors.foreground, fontSize: 13, lineHeight: 19 }}><Spans colors={colors} spans={block.spans} /></Text>
|
|
81
|
-
</View>
|
|
82
|
-
);
|
|
83
|
-
case "code":
|
|
84
|
-
return (
|
|
85
|
-
<View style={{ padding: 8, borderRadius: 6, backgroundColor: colors.surface0 }}>
|
|
86
|
-
<Text selectable style={{ color: colors.foreground, fontFamily: MONO, fontSize: 12, lineHeight: 18 }}>{block.text}</Text>
|
|
87
|
-
</View>
|
|
88
|
-
);
|
|
89
|
-
default:
|
|
90
|
-
return <Text selectable style={{ color: colors.foreground, fontSize: 13, lineHeight: 19 }}><Spans colors={colors} spans={block.spans} /></Text>;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function Spans({ colors, spans }: { colors: Colors; spans: Span[] }) {
|
|
95
|
-
return <>{spans.map((span, index) => span.code
|
|
96
|
-
? <Text key={index} style={{ fontFamily: MONO, fontSize: 12, color: colors.foreground }}>{span.text}</Text>
|
|
97
|
-
: span.strong ? <Text key={index} style={{ fontWeight: "600" }}>{span.text}</Text>
|
|
98
|
-
: <React.Fragment key={index}>{span.text}</React.Fragment>)}</>;
|
|
99
|
-
}
|