@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.
Files changed (67) hide show
  1. package/README.md +24 -17
  2. package/client/attachments.tsx +122 -28
  3. package/client/blocks.tsx +33 -8
  4. package/client/chat-row-view.tsx +19 -6
  5. package/client/chat-row.ts +13 -2
  6. package/client/dashboard-query.ts +9 -4
  7. package/client/deliverables.tsx +82 -17
  8. package/client/dialog-stack.tsx +104 -42
  9. package/client/directory.ts +1 -3
  10. package/client/errors.ts +6 -1
  11. package/client/history.tsx +233 -79
  12. package/client/i18n.ts +10 -5
  13. package/client/launcher.tsx +57 -15
  14. package/client/locales/en.ts +28 -22
  15. package/client/locales/zh-CN.ts +18 -17
  16. package/client/messages.ts +1 -0
  17. package/client/motion.tsx +38 -14
  18. package/client/panel-opened.ts +3 -1
  19. package/client/panel.tsx +442 -160
  20. package/client/pdf.tsx +18 -4
  21. package/client/pieces.ts +6 -1
  22. package/client/pill-popover.tsx +108 -23
  23. package/client/pills.tsx +35 -12
  24. package/client/preview.tsx +265 -79
  25. package/client/questions.tsx +173 -52
  26. package/client/row.tsx +14 -3
  27. package/client/section-title.tsx +11 -2
  28. package/client/settings.tsx +76 -24
  29. package/client/show-more.tsx +23 -6
  30. package/client/spinner.tsx +11 -7
  31. package/client/status-badge.tsx +9 -7
  32. package/client/tabs.tsx +98 -20
  33. package/client/ticket-dialog.tsx +241 -73
  34. package/client/time-bar.tsx +5 -2
  35. package/client/video.tsx +15 -6
  36. package/client/whats-new.tsx +94 -36
  37. package/client/when.tsx +2 -1
  38. package/client/write-up.tsx +79 -12
  39. package/dist/server/cli.js +199 -101
  40. package/index.client.tsx +28 -13
  41. package/index.server.ts +19 -1
  42. package/package.json +15 -14
  43. package/server/cli.ts +72 -32
  44. package/server/dashboard.ts +9 -13
  45. package/server/launcher.ts +33 -17
  46. package/server/lock.ts +14 -7
  47. package/server/open.ts +32 -98
  48. package/server/progress-file.ts +6 -6
  49. package/server/question-mode.ts +12 -6
  50. package/server/skill.ts +22 -16
  51. package/server/store.ts +26 -41
  52. package/server/system.ts +113 -0
  53. package/server/tool.ts +89 -44
  54. package/server/whats-new.ts +25 -13
  55. package/shared/attachments.ts +40 -8
  56. package/shared/blocks.ts +9 -6
  57. package/shared/dashboard.ts +46 -29
  58. package/shared/events.ts +12 -10
  59. package/shared/markdown.ts +16 -8
  60. package/shared/question-modes.ts +8 -5
  61. package/shared/rpc.ts +9 -3
  62. package/shared/show.ts +20 -8
  63. package/shared/ticket-story.ts +1 -3
  64. package/skills/paseo-progress/SKILL.md +1 -0
  65. package/tsconfig.json +3 -10
  66. package/client/page-opener.ts +0 -9
  67. 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; dashboard: Dashboard; panelOpened: boolean; file: string; savedTo: string | null; inRepo: boolean; root: string; version: string;
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, Markdown or HTML file's contents, for the preview dialog.
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> = { stopped: "agent stopped until the answer", waiting: "waiting for the answer", building: "building with it" };
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 "No run yet. Start one with: paseo-progress start \"<title>\"";
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(`${dashboard.progress.percent}% of estimated work done, ${dashboard.progress.tookMin !== undefined ? "took " : ""}${formatWorkDone(dashboard.progress.doneMin, dashboard.progress.totalMin, dashboard.progress.tookMin)}`);
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 = item.kind === "overdue" ? `no update for longer than its ${formatMinutes(item.estimateMin)} estimate`
34
- : item.kind === "blocked" ? `blocked${item.note ? `: ${item.note}` : ""}`
35
- : `${item.id}: ${item.reason}`;
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(` ${question.id} ${question.ticketId ? `[${question.ticketId}] ` : ""}(${question.title}): ${question.question} You recommended ${question.recommended}, ${QUESTION_STATE[question.state]}${question.big ? `. Big: ${question.big}` : ""}`);
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)) lines.push(` ${entry.id} ${entry.ticketId ? `[${entry.ticketId}] ` : ""}${entry.text} (${formatAgo(entry.at, nowMs)})`);
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
  }
@@ -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
- "**/*.ts",
19
- "**/*.tsx"
20
- ],
21
- "exclude": [
22
- "node_modules"
23
- ]
15
+ "include": ["**/*.ts", "**/*.tsx"],
16
+ "exclude": ["node_modules"]
24
17
  }
@@ -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
- }
@@ -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
- }