@stevecastaneda/paseo-progress 1.14.2 → 1.14.3
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/README.md +24 -17
- package/client/attachments.tsx +122 -28
- package/client/blocks.tsx +33 -8
- package/client/chat-row-view.tsx +19 -6
- package/client/chat-row.ts +13 -2
- package/client/dashboard-query.ts +9 -4
- package/client/deliverables.tsx +82 -17
- package/client/dialog-stack.tsx +104 -42
- package/client/directory.ts +1 -3
- package/client/errors.ts +6 -1
- package/client/history.tsx +233 -79
- package/client/i18n.ts +10 -5
- package/client/launcher.tsx +57 -15
- package/client/locales/en.ts +28 -22
- package/client/locales/zh-CN.ts +18 -17
- package/client/messages.ts +1 -0
- package/client/motion.tsx +38 -14
- package/client/panel-opened.ts +3 -1
- package/client/panel.tsx +442 -160
- package/client/pdf.tsx +18 -4
- package/client/pieces.ts +6 -1
- package/client/pill-popover.tsx +108 -23
- package/client/pills.tsx +35 -12
- package/client/preview.tsx +265 -79
- package/client/questions.tsx +173 -52
- package/client/row.tsx +14 -3
- package/client/section-title.tsx +11 -2
- package/client/settings.tsx +76 -24
- package/client/show-more.tsx +23 -6
- package/client/spinner.tsx +11 -7
- package/client/status-badge.tsx +9 -7
- package/client/tabs.tsx +98 -20
- package/client/ticket-dialog.tsx +241 -73
- package/client/time-bar.tsx +5 -2
- package/client/video.tsx +15 -6
- package/client/whats-new.tsx +94 -36
- package/client/when.tsx +2 -1
- package/client/write-up.tsx +79 -12
- package/dist/server/cli.js +199 -101
- package/index.client.tsx +28 -13
- package/index.server.ts +19 -1
- package/package.json +15 -14
- package/server/cli.ts +72 -32
- package/server/dashboard.ts +9 -13
- package/server/launcher.ts +33 -17
- package/server/lock.ts +14 -7
- package/server/open.ts +32 -98
- package/server/progress-file.ts +6 -6
- package/server/question-mode.ts +12 -6
- package/server/skill.ts +22 -16
- package/server/store.ts +26 -41
- package/server/system.ts +113 -0
- package/server/tool.ts +89 -44
- package/server/whats-new.ts +25 -13
- package/shared/attachments.ts +40 -8
- package/shared/blocks.ts +9 -6
- package/shared/dashboard.ts +46 -29
- package/shared/events.ts +12 -10
- package/shared/markdown.ts +16 -8
- package/shared/question-modes.ts +8 -5
- package/shared/rpc.ts +9 -3
- package/shared/show.ts +20 -8
- package/shared/ticket-story.ts +1 -3
- package/skills/paseo-progress/SKILL.md +1 -0
- package/tsconfig.json +3 -10
- package/client/page-opener.ts +0 -9
- package/client/page-screen.tsx +0 -97
package/shared/rpc.ts
CHANGED
|
@@ -11,7 +11,14 @@ import type { WhatsNew } from "./whats-new.ts";
|
|
|
11
11
|
// `inRepo`: saving history needs a git repo to keep the choice in.
|
|
12
12
|
// `version` changes whenever anything else in the result does.
|
|
13
13
|
export type DashboardResult = {
|
|
14
|
-
configured: boolean;
|
|
14
|
+
configured: boolean;
|
|
15
|
+
dashboard: Dashboard;
|
|
16
|
+
panelOpened: boolean;
|
|
17
|
+
file: string;
|
|
18
|
+
savedTo: string | null;
|
|
19
|
+
inRepo: boolean;
|
|
20
|
+
root: string;
|
|
21
|
+
version: string;
|
|
15
22
|
};
|
|
16
23
|
|
|
17
24
|
export const getDashboard = defineRpc({
|
|
@@ -153,7 +160,7 @@ export const canOpenOnHost = defineRpc({
|
|
|
153
160
|
output: z.object({ canOpen: z.boolean() }),
|
|
154
161
|
});
|
|
155
162
|
|
|
156
|
-
// An image, text
|
|
163
|
+
// An image, text or Markdown file's contents, for the preview dialog.
|
|
157
164
|
export const previewDeliverable = defineRpc({
|
|
158
165
|
name: "progress.deliverable.preview",
|
|
159
166
|
input: z.object({
|
|
@@ -168,7 +175,6 @@ export const previewDeliverable = defineRpc({
|
|
|
168
175
|
z.object({ kind: z.literal("pdf"), bytes: z.number() }),
|
|
169
176
|
z.object({ kind: z.literal("text"), text: z.string(), bytes: z.number(), truncated: z.boolean() }),
|
|
170
177
|
z.object({ kind: z.literal("markdown"), markdown: z.string(), bytes: z.number(), truncated: z.boolean() }),
|
|
171
|
-
z.object({ kind: z.literal("html"), html: z.string(), bytes: z.number() }),
|
|
172
178
|
z.object({ kind: z.literal("folder"), entries: z.array(z.object({ name: z.string(), folder: z.boolean() })), truncated: z.boolean() }),
|
|
173
179
|
]),
|
|
174
180
|
});
|
package/shared/show.ts
CHANGED
|
@@ -4,11 +4,15 @@ import { formatAgo, formatClock, formatMinutes, formatWorkDone, minutesSince } f
|
|
|
4
4
|
import type { QuestionState } from "./question-modes.ts";
|
|
5
5
|
|
|
6
6
|
const STATUS: Record<string, string> = { not_started: "Not started", working: "Working", blocked: "Blocked", done: "Done", skipped: "Skipped" };
|
|
7
|
-
const QUESTION_STATE: Record<QuestionState, string> = {
|
|
7
|
+
const QUESTION_STATE: Record<QuestionState, string> = {
|
|
8
|
+
stopped: "agent stopped until the answer",
|
|
9
|
+
waiting: "waiting for the answer",
|
|
10
|
+
building: "building with it",
|
|
11
|
+
};
|
|
8
12
|
|
|
9
13
|
// The dashboard as plain text, so an agent can check what the user sees.
|
|
10
14
|
export function dashboardText(dashboard: Dashboard, nowMs: number): string {
|
|
11
|
-
if (!dashboard.run && !dashboard.tickets.length) return
|
|
15
|
+
if (!dashboard.run && !dashboard.tickets.length) return 'No run yet. Start one with: paseo-progress start "<title>"';
|
|
12
16
|
const lines: string[] = [];
|
|
13
17
|
const when = (iso: string) => `${formatClock(iso)} (${formatAgo(iso, nowMs)})`;
|
|
14
18
|
lines.push(dashboard.run?.title ?? "Progress");
|
|
@@ -22,7 +26,9 @@ export function dashboardText(dashboard: Dashboard, nowMs: number): string {
|
|
|
22
26
|
if (dashboard.updatedAt) lines.push(`Last updated ${when(dashboard.updatedAt)}`);
|
|
23
27
|
if (dashboard.stale && dashboard.updatedAt) lines.push(`POSSIBLY STALE: no update for ${formatMinutes(minutesSince(dashboard.updatedAt, nowMs))}.`);
|
|
24
28
|
if (dashboard.progress.totalMin) {
|
|
25
|
-
lines.push(
|
|
29
|
+
lines.push(
|
|
30
|
+
`${dashboard.progress.percent}% of estimated work done, ${dashboard.progress.tookMin !== undefined ? "took " : ""}${formatWorkDone(dashboard.progress.doneMin, dashboard.progress.totalMin, dashboard.progress.tookMin)}`,
|
|
31
|
+
);
|
|
26
32
|
}
|
|
27
33
|
if (dashboard.issues.length) {
|
|
28
34
|
lines.push("", `Skipped lines: ${dashboard.issues.map((issue) => `line ${issue.line} (${issue.reason})`).join(", ")}`);
|
|
@@ -30,9 +36,12 @@ export function dashboardText(dashboard: Dashboard, nowMs: number): string {
|
|
|
30
36
|
if (dashboard.stuck.length) {
|
|
31
37
|
lines.push("", "Stuck");
|
|
32
38
|
for (const item of dashboard.stuck) {
|
|
33
|
-
const detail =
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
const detail =
|
|
40
|
+
item.kind === "overdue"
|
|
41
|
+
? `no update for longer than its ${formatMinutes(item.estimateMin)} estimate`
|
|
42
|
+
: item.kind === "blocked"
|
|
43
|
+
? `blocked${item.note ? `: ${item.note}` : ""}`
|
|
44
|
+
: `${item.id}: ${item.reason}`;
|
|
36
45
|
lines.push(` ${item.title} (${detail}), since ${when(item.since)}`);
|
|
37
46
|
}
|
|
38
47
|
}
|
|
@@ -46,7 +55,9 @@ export function dashboardText(dashboard: Dashboard, nowMs: number): string {
|
|
|
46
55
|
if (dashboard.questions.open.length) {
|
|
47
56
|
lines.push("", "Questions waiting");
|
|
48
57
|
for (const question of dashboard.questions.open) {
|
|
49
|
-
lines.push(
|
|
58
|
+
lines.push(
|
|
59
|
+
` ${question.id} ${question.ticketId ? `[${question.ticketId}] ` : ""}(${question.title}): ${question.question} You recommended ${question.recommended}, ${QUESTION_STATE[question.state]}${question.big ? `. Big: ${question.big}` : ""}`,
|
|
60
|
+
);
|
|
50
61
|
for (const option of question.options) lines.push(` ${option.letter}) ${option.label}${option.consequence ? `: ${option.consequence}` : ""}`);
|
|
51
62
|
}
|
|
52
63
|
}
|
|
@@ -65,7 +76,8 @@ export function dashboardText(dashboard: Dashboard, nowMs: number): string {
|
|
|
65
76
|
}
|
|
66
77
|
if (dashboard.activity.length) {
|
|
67
78
|
lines.push("", "Activity");
|
|
68
|
-
for (const entry of dashboard.activity.slice(0, 5))
|
|
79
|
+
for (const entry of dashboard.activity.slice(0, 5))
|
|
80
|
+
lines.push(` ${entry.id} ${entry.ticketId ? `[${entry.ticketId}] ` : ""}${entry.text} (${formatAgo(entry.at, nowMs)})`);
|
|
69
81
|
}
|
|
70
82
|
return lines.join("\n");
|
|
71
83
|
}
|
package/shared/ticket-story.ts
CHANGED
|
@@ -68,9 +68,7 @@ export function ticketTimeline(ticket: Ticket, nowMs: number): TimelineStep[] {
|
|
|
68
68
|
// Each includes its start but not its end, so an entry logged at a handoff
|
|
69
69
|
// belongs to the ticket that just started.
|
|
70
70
|
function workingWindows(steps: TimelineStep[], nowMs: number): Array<[number, number]> {
|
|
71
|
-
return steps
|
|
72
|
-
.filter((step) => step.status === "working")
|
|
73
|
-
.map((step) => [Date.parse(step.at), step.until ? Date.parse(step.until) : nowMs + 1]);
|
|
71
|
+
return steps.filter((step) => step.status === "working").map((step) => [Date.parse(step.at), step.until ? Date.parse(step.until) : nowMs + 1]);
|
|
74
72
|
}
|
|
75
73
|
|
|
76
74
|
export function ticketStory(ticket: Ticket, related: { deliverables: Deliverable[]; questions: Question[]; activity: Activity[] }, nowMs: number): TicketStory {
|
|
@@ -25,6 +25,7 @@ Done when `show` lists every ticket with an estimate.
|
|
|
25
25
|
|
|
26
26
|
Move each ticket through `--status working --stage Build`, then the stages `Review`, `Fixes` and `Verify`, then `--status done`. Along the way, record what the user would want to see: `deliverable add` for anything reviewable, `activity add` for milestones, `ticker set` for what you are doing right now. Tag each with `--ticket` unless it covers the whole run.
|
|
27
27
|
|
|
28
|
+
- An HTML deliverable must be self-contained, with its CSS in the page and images embedded as data URIs, and no scripts or styles from the web, because Paseo's preview blocks the network.
|
|
28
29
|
- A ticket past its estimate with no update turns stuck. Keep its stage current, or raise its `--estimate`.
|
|
29
30
|
- A ticket another session adds is part of the plan. Work it in order.
|
|
30
31
|
- `--status blocked` and `stuck set` are for things outside the run, like a service that's down or missing access.
|
package/tsconfig.json
CHANGED
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
"target": "ES2020",
|
|
4
4
|
"module": "ESNext",
|
|
5
5
|
"moduleResolution": "Bundler",
|
|
6
|
-
"lib": [
|
|
7
|
-
"ES2023"
|
|
8
|
-
],
|
|
6
|
+
"lib": ["ES2023"],
|
|
9
7
|
"jsx": "react-jsx",
|
|
10
8
|
"strict": true,
|
|
11
9
|
"skipLibCheck": true,
|
|
@@ -14,11 +12,6 @@
|
|
|
14
12
|
"esModuleInterop": true,
|
|
15
13
|
"allowSyntheticDefaultImports": true
|
|
16
14
|
},
|
|
17
|
-
"include": [
|
|
18
|
-
|
|
19
|
-
"**/*.tsx"
|
|
20
|
-
],
|
|
21
|
-
"exclude": [
|
|
22
|
-
"node_modules"
|
|
23
|
-
]
|
|
15
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
16
|
+
"exclude": ["node_modules"]
|
|
24
17
|
}
|
package/client/page-opener.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export type PageParams = { workspaceId: string; workspaceDirectory: string; ref: string; title: string };
|
|
2
|
-
|
|
3
|
-
// Panels can't open a screen themselves, only the plugin's client can, so it
|
|
4
|
-
// sets this once it has added the page screen.
|
|
5
|
-
export let openPage: ((params: PageParams) => void) | null = null;
|
|
6
|
-
|
|
7
|
-
export function setPageOpener(opener: typeof openPage) {
|
|
8
|
-
openPage = opener;
|
|
9
|
-
}
|
package/client/page-screen.tsx
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { type PluginClientContext, type PluginScreenProps, useRpc } from "@getpaseo/plugin/client";
|
|
2
|
-
import { useQuery } from "@tanstack/react-query";
|
|
3
|
-
import React, { useEffect, useRef, useState } from "react";
|
|
4
|
-
import { ActivityIndicator, Text, View } from "react-native";
|
|
5
|
-
import { previewDeliverable } from "../shared/rpc";
|
|
6
|
-
import { errorText } from "./errors";
|
|
7
|
-
import { ProgressLanguageProvider, useProgressLanguage } from "./language";
|
|
8
|
-
import { type PageParams, setPageOpener } from "./page-opener";
|
|
9
|
-
|
|
10
|
-
const PAGE_SCREEN_ID = "page";
|
|
11
|
-
|
|
12
|
-
export function addPageScreen(client: PluginClientContext) {
|
|
13
|
-
setPageOpener((params) => client.openScreen({ screenId: PAGE_SCREEN_ID, params }));
|
|
14
|
-
const stop = client.addScreen({
|
|
15
|
-
id: PAGE_SCREEN_ID,
|
|
16
|
-
title: (params) => params.title ?? "",
|
|
17
|
-
Component: (props) => <ProgressLanguageProvider><PageScreen {...props} /></ProgressLanguageProvider>,
|
|
18
|
-
});
|
|
19
|
-
return () => {
|
|
20
|
-
setPageOpener(null);
|
|
21
|
-
void stop();
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Keys pressed inside the page go to the page, so it tells Progress about
|
|
26
|
-
// Escape itself, unless one of its own handlers has used the key.
|
|
27
|
-
const ESCAPE = "progress:page-escape";
|
|
28
|
-
const ESCAPE_SCRIPT = `<script>addEventListener("keydown",function(e){if(e.key==="Escape"&&!e.defaultPrevented)parent.postMessage("${ESCAPE}","*")})</script>`;
|
|
29
|
-
|
|
30
|
-
// The plugin compiles without the DOM's types; these are the browser APIs used here.
|
|
31
|
-
type KeyEvent = { key: string; defaultPrevented: boolean };
|
|
32
|
-
type FrameMessage = { data: unknown; source: unknown };
|
|
33
|
-
type Frame = { contentWindow: unknown; getClientRects(): { length: number } };
|
|
34
|
-
const browser = globalThis as unknown as {
|
|
35
|
-
addEventListener(type: "keydown", listener: (event: KeyEvent) => void): void;
|
|
36
|
-
addEventListener(type: "message", listener: (event: FrameMessage) => void): void;
|
|
37
|
-
removeEventListener(type: "keydown" | "message", listener: (event: never) => void): void;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
// Escape leaves the page, like the Close button in Paseo's header.
|
|
41
|
-
// Paseo can keep a screen mounted, hidden, after leaving it, so Escape only
|
|
42
|
-
// counts while the page is on screen.
|
|
43
|
-
function useEscapeToClose(frame: React.RefObject<Frame | null>, close: (() => void) | undefined) {
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
if (!close || typeof browser.addEventListener !== "function") return;
|
|
46
|
-
const onKey = (event: KeyEvent) => {
|
|
47
|
-
if (event.key === "Escape" && !event.defaultPrevented && frame.current?.getClientRects().length) close();
|
|
48
|
-
};
|
|
49
|
-
const onMessage = (event: FrameMessage) => {
|
|
50
|
-
if (event.data === ESCAPE && event.source === frame.current?.contentWindow) close();
|
|
51
|
-
};
|
|
52
|
-
browser.addEventListener("keydown", onKey);
|
|
53
|
-
browser.addEventListener("message", onMessage);
|
|
54
|
-
return () => {
|
|
55
|
-
browser.removeEventListener("keydown", onKey);
|
|
56
|
-
browser.removeEventListener("message", onMessage);
|
|
57
|
-
};
|
|
58
|
-
}, [close]);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// An HTML deliverable across the whole screen, the width it was written for.
|
|
62
|
-
function PageScreen({ params, theme: { colors }, navigation }: PluginScreenProps) {
|
|
63
|
-
const { messages: m } = useProgressLanguage();
|
|
64
|
-
const fetchPreview = useRpc(previewDeliverable);
|
|
65
|
-
const { workspaceId, workspaceDirectory, ref } = params as PageParams;
|
|
66
|
-
const page = useQuery({
|
|
67
|
-
queryKey: ["progress", "page", workspaceId, ref],
|
|
68
|
-
queryFn: () => fetchPreview({ workspaceId, workspaceDirectory, ref }),
|
|
69
|
-
});
|
|
70
|
-
// The page stays hidden on Paseo's background until it has drawn, so opening it
|
|
71
|
-
// doesn't flash an empty white frame first.
|
|
72
|
-
const [shownHtml, setShownHtml] = useState<string | null>(null);
|
|
73
|
-
const frame = useRef<Frame | null>(null);
|
|
74
|
-
useEscapeToClose(frame, navigation ? () => navigation.openWorkspace({ workspaceId }) : undefined);
|
|
75
|
-
if (page.data?.kind === "html") {
|
|
76
|
-
const html = page.data.html + ESCAPE_SCRIPT;
|
|
77
|
-
return (
|
|
78
|
-
<View style={{ flex: 1, backgroundColor: colors.surface0 }}>
|
|
79
|
-
{/* `allow-scripts` alone, as in Paseo's own HTML preview: the page runs its scripts
|
|
80
|
-
(and loads them from the web, so no CSP) in an opaque origin that can't reach
|
|
81
|
-
Paseo's DOM or storage, navigate the app, open windows or submit forms. */}
|
|
82
|
-
<iframe ref={frame as never} srcDoc={html} sandbox="allow-scripts" referrerPolicy="no-referrer" title={params.title} onLoad={() => setShownHtml(html)}
|
|
83
|
-
style={{ flex: 1, width: "100%", height: "100%", display: "block", border: "none", backgroundColor: "white",
|
|
84
|
-
opacity: shownHtml === html ? 1 : 0, transition: "opacity 150ms ease-out" }} />
|
|
85
|
-
</View>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
return (
|
|
89
|
-
<View style={{ flex: 1, alignItems: "center", justifyContent: "center", padding: 24 }}>
|
|
90
|
-
{page.error ? (
|
|
91
|
-
<Text accessibilityRole="alert" selectable style={{ color: colors.statusDanger, fontSize: 12, lineHeight: 18 }}>
|
|
92
|
-
{m.previewError(errorText(page.error, m.unknownError))}
|
|
93
|
-
</Text>
|
|
94
|
-
) : <ActivityIndicator color={colors.foregroundMuted} accessibilityLabel={m.loadingPreview} />}
|
|
95
|
-
</View>
|
|
96
|
-
);
|
|
97
|
-
}
|