@stevecastaneda/paseo-progress 0.4.0 → 1.0.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/README.md CHANGED
@@ -10,7 +10,7 @@ The dashboard shows:
10
10
 
11
11
  - the run's title, a headline ("2 of 4 tickets done, 5 questions waiting for you, 1 stuck"), a one-line "now" ticker, and when it was last updated
12
12
  - a "possibly stale" warning after 15 minutes with no update while tickets remain, unless Paseo shows an agent in the workspace still running, and never once every ticket is done or skipped or the run is finished; working items swap their spinner for a pulsing amber warning
13
- - percent of estimated work done, time done of time estimated (like "1 h 30 min of 2 h"; once everything is done, how long it really took), and a bar with one segment per ticket
13
+ - percent of estimated work done, time done of time estimated (like "1h 30m of 2h"; once everything is done, how long it really took), and a bar with one segment per ticket
14
14
  - **Stuck:** blocked tickets, working tickets with no update for longer than their estimate, and blockers the agent flags
15
15
  - **Tickets:** estimate, status, and the working ticket's stage. A ticket that waits for another (`--waits-for T03`) shows "Waits for Ticket 03" under its title until that one is done; waiting is the order of work, so it never counts as stuck, even if the agent also marked it blocked. Press a ticket for its story in a dialog. A summary on top shows its status and stage, time worked against the estimate as a bar that turns red past it, the agent's latest status line while it's working, and the agent on it. Tabs below hold the rest, each only when it has something: the ticket's write-up when it has a `--source` file (the start of it, with the rest a press away), a timeline of its stages and statuses with how long each took, and the activity, questions and deliverables that belong to it. The agent on a ticket shows by its chat title; pressing it opens that chat. Items tagged with `--ticket` belong exactly; older, untagged ones are matched by when they happened and marked "by time"
16
16
  - **Questions:** each with a permanent reference (Q1, Q2, ...), lettered options, and the option the agent recommends. The agent builds nothing a question affects until you answer; it works on other things meanwhile, and stops when nothing else is left. Press a question to open it in a dialog, then press **Copy Q2 B** to copy `Q2 (title): B` for your reply. Files and links the agent attached (`--file`) open like deliverables: images and text files preview in Paseo (with **Back** to the question), links open in Paseo's browser, and other files open in their default app. From the pill's popover, the preview opens over the popover, which stays open so you can go back to it. Answers stay listed under the same reference.
@@ -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
- The panel lives in Explorer, beside the agent chat, so you can watch both. Open it from Command Center with **Open Progress**. When questions are waiting or something is stuck, a pill above the message box shows it (`3 questions · 1 stuck`). Pressing it opens a popover with each open question, its choices and their Copy buttons, and what is stuck, so you can answer without leaving the chat. **Open Progress** at the bottom opens the panel in Explorer.
21
+ The panel lives in Explorer, beside the agent chat, so you can watch both. Open it from Command Center with **Open Progress**. When questions are waiting or something is stuck, a pill above the message box says so, for example `3 questions`. With something stuck too, it shortens to `3Q · 1 stuck` to fit. Pressing it opens a popover with each open question, its choices and their Copy buttons, and what is stuck, so you can answer without leaving the chat. **Open Progress** at the bottom opens the panel in Explorer.
22
22
 
23
23
  <img src="images/4-pill.png" alt="The pill above the message box, showing two questions waiting" width="600">
24
24
 
@@ -123,17 +123,15 @@ The progress file, `progress.jsonl`, holds one JSON event per line, each with a
123
123
  - Previews cover images up to 10 MB and the first 256 KB of a text file. Other files open in their default app.
124
124
  - Stale warnings and overdue times use the daemon host's clock.
125
125
 
126
- ## Local development
127
-
128
- ```sh
129
- cd progress
130
- npm install
131
- npm run dev
132
- ```
126
+ ## Changelog
133
127
 
134
- `npm run dev` type-checks and tests the plugin, then points Paseo at this folder. The tests run the agent's commands in a throwaway worktree and read back the dashboard the panel would get.
128
+ ### 1.0.0
135
129
 
136
- ## Changelog
130
+ - Version 1.0. Nothing you use changes. From now on, a new first number means an update breaks something, a new middle number means a new feature, and a new last number means a fix.
131
+ - The pill above the message box fits again when questions are waiting and something is stuck. It reads `2Q · 1 stuck`. Before, Paseo cut it off at `2 questions · 1 st…`.
132
+ - A time like `(11m ago)` moves to the next line whole. It no longer splits in the middle.
133
+ - Every duration uses one style, like `2h 15m`. Before, the progress bar said `2 h 15 min` and the finished tickets said `2.3 h` for the same work. Estimates show the minutes you set, so a 90-minute ticket reads `1h 30m`, not `1.5 h`.
134
+ - New screenshots in this README.
137
135
 
138
136
  ### 0.4.0
139
137
 
package/client/panel.tsx CHANGED
@@ -8,12 +8,12 @@ import { Spinner, StalledPulse, WaitingDot } from "./spinner";
8
8
  import { IconSwap, Presence, StaggerRoot, nativeDriver } from "./motion";
9
9
  import { SectionTitle } from "./section-title";
10
10
  import { raised } from "./surfaces";
11
- import { When } from "./when";
11
+ import { unbroken, When } from "./when";
12
12
  import React, { useEffect, useRef, useState } from "react";
13
13
  import { ActivityIndicator, Animated, Platform, ScrollView, Text, View } from "react-native";
14
14
  import { finishedToFold, isFinished, shortTitle, type Activity, type Dashboard, type FileIssue, type ProgressSegment, type StuckItem, type Ticket } from "../shared/dashboard";
15
15
  import type { TicketStatus } from "../shared/events";
16
- import { formatEstimate, formatMinutes, formatWorkDone, minutesSince } from "../shared/format";
16
+ import { formatMinutes, formatWorkDone, minutesSince } from "../shared/format";
17
17
  import { markPanelOpened } from "../shared/rpc";
18
18
  import { useDashboard } from "./dashboard-query";
19
19
  import { STATUS_LABEL } from "./status-badge";
@@ -119,7 +119,7 @@ function StatusLine({ icon, children }: { icon: React.ReactNode; children: React
119
119
  );
120
120
  }
121
121
 
122
- // Re-renders on an interval so "x min ago" keeps counting between polls.
122
+ // Re-renders on an interval so "11m ago" keeps counting between polls.
123
123
  function useNow(intervalMs: number): number {
124
124
  const [now, setNow] = useState(() => Date.now());
125
125
  useEffect(() => {
@@ -204,7 +204,7 @@ function DashboardView({ colors, dashboard, agentRunning, compact, workspaceId,
204
204
  {/* Optical: centers the 14px icon on the first 18px line. */}
205
205
  <View style={{ paddingTop: 2 }}><Icon name="TriangleAlert" size={14} color={colors.statusWarning} /></View>
206
206
  <Text style={{ flex: 1, color: colors.statusWarning, fontSize: 12, lineHeight: 18 }}>
207
- Possibly stale. No update for {formatMinutes(minutesSince(dashboard.updatedAt, now))}. The work may have stopped, so working items show a warning until the next update.
207
+ Possibly stale. No update for {unbroken(formatMinutes(minutesSince(dashboard.updatedAt, now)))}. The work may have stopped, so working items show a warning until the next update.
208
208
  </Text>
209
209
  </View>
210
210
  ) : null}</Presence>
@@ -389,7 +389,7 @@ function StuckSection({ colors, items, now }: { colors: Colors; items: StuckItem
389
389
  <View style={{ flex: 1, minWidth: 0, gap: 2 }}>
390
390
  <Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 18 }}>{item.title}</Text>
391
391
  <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
392
- {item.kind === "overdue" ? `No update for longer than its ${formatMinutes(item.estimateMin)} estimate. Last update ` : null}
392
+ {item.kind === "overdue" ? `No update for longer than its ${unbroken(formatMinutes(item.estimateMin))} estimate. Last update ` : null}
393
393
  {item.kind === "blocked" ? `Blocked${item.note ? `: ${item.note}` : ""}${narrow ? ", " : ". Since "}` : null}
394
394
  {item.kind === "manual" ? `${item.ticketId ? `${item.reason}. ` : ""}Flagged ` : null}
395
395
  <When colors={colors} iso={item.since} now={now} />
@@ -448,7 +448,7 @@ function FinishedRow({ colors, tickets, shown, onPress }: { colors: Colors; tick
448
448
  {/* Narrow ticket rows drop their chevron, so this total would sit off their estimate column. */}
449
449
  {narrow ? null : <>
450
450
  <View style={{ width: 12 }} />
451
- <Text style={{ minWidth: 40, flexShrink: 0, textAlign: "right", color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontVariant: ["tabular-nums"] }}>{estimateMin ? formatEstimate(estimateMin) : ""}</Text>
451
+ <Text style={{ minWidth: 44, flexShrink: 0, textAlign: "right", color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontVariant: ["tabular-nums"] }}>{estimateMin ? formatMinutes(estimateMin) : ""}</Text>
452
452
  </>}
453
453
  <View style={{ paddingTop: 2 }}>
454
454
  <IconSwap swapKey={chevron} size={14}><Icon name={chevron} size={14} color={colors.foregroundMuted} /></IconSwap>
@@ -490,11 +490,11 @@ function TicketRow({ colors, ticket, live, now, onOpen, deliverables }: { colors
490
490
  ) : null}
491
491
  {ticket.note ? <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>{ticket.note}</Text> : null}
492
492
  </View>
493
- {/* Fixed slots so the Package icon and estimates line up down the list, whether or not a row has deliverables; they're listed in its dialog. The estimate slot fits "45 min" and "10.5 h"; longer ones grow rather than clip. */}
493
+ {/* Fixed slots so the Package icon and estimates line up down the list, whether or not a row has deliverables; they're listed in its dialog. The estimate slot fits "45m" up to "9h 45m"; longer ones grow rather than clip. */}
494
494
  <View accessibilityLabel={deliverables ? (deliverables === 1 ? "Has a deliverable" : `Has ${deliverables} deliverables`) : undefined} style={{ width: 12, height: 18, justifyContent: "center" }}>
495
495
  {deliverables ? <Icon name="Package" size={12} color={colors.foregroundMuted} /> : null}
496
496
  </View>
497
- <Text style={{ minWidth: 40, flexShrink: 0, textAlign: "right", color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontVariant: ["tabular-nums"] }}>{formatEstimate(ticket.estimateMin)}</Text>
497
+ <Text style={{ minWidth: 44, flexShrink: 0, textAlign: "right", color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontVariant: ["tabular-nums"] }}>{formatMinutes(ticket.estimateMin)}</Text>
498
498
  {narrow ? null : <View style={{ paddingTop: 2 }}><Icon name="ChevronRight" size={14} color={colors.foregroundMuted} /></View>}
499
499
  </PressableRow>
500
500
  );
@@ -17,8 +17,10 @@ export function pillFor(report: AttentionResult | undefined, where: { phone: boo
17
17
  if (!report?.configured) return null;
18
18
  const { questions, stuck } = report;
19
19
  if (questions || stuck) {
20
+ // Paseo cuts composer pills off at 160 px, so when something is stuck
21
+ // too, the questions shorten to "2Q" to leave room for "1 stuck".
20
22
  const parts = [];
21
- if (questions) parts.push(`${questions} ${questions === 1 ? "question" : "questions"}`);
23
+ if (questions) parts.push(stuck ? `${questions}Q` : `${questions} ${questions === 1 ? "question" : "questions"}`);
22
24
  if (stuck) parts.push(`${stuck} stuck`);
23
25
  return { mode: "attention", label: parts.join(" · "), icon: stuck ? "stuck" : "questions" };
24
26
  }
package/client/when.tsx CHANGED
@@ -6,13 +6,20 @@ import { useNarrow } from "./narrow";
6
6
 
7
7
  type Colors = PluginWorkspacePanelProps["theme"]["colors"];
8
8
 
9
- // "Sep 27, 8:14 PM (10 h 2 min ago)", inline inside other text. A narrow
10
- // panel shows only "10 h 2 min ago"; the ticket dialog keeps the clock time.
9
+ // A time like "2h 15m ago" inside wrapping text moves to the next line as one
10
+ // piece instead of breaking after "2h".
11
+ export function unbroken(text: string): string {
12
+ return text.replaceAll(" ", "\u00a0");
13
+ }
14
+
15
+ // "Sep 27, 8:14 PM (10h 2m ago)", inline inside other text. A narrow
16
+ // panel shows only "10h 2m ago"; the ticket dialog keeps the clock time.
11
17
  export function When({ colors, iso, now }: { colors: Colors; iso: string; now: number }) {
12
- if (useNarrow()) return <Text>{formatAgo(iso, now)}</Text>;
18
+ const ago = unbroken(formatAgo(iso, now));
19
+ if (useNarrow()) return <Text>{ago}</Text>;
13
20
  return (
14
21
  <Text>
15
- {formatClock(iso)} <Text style={{ color: colors.foregroundMuted, opacity: 0.7 }}>({formatAgo(iso, now)})</Text>
22
+ {formatClock(iso)} <Text style={{ color: colors.foregroundMuted, opacity: 0.7 }}>({ago})</Text>
16
23
  </Text>
17
24
  );
18
25
  }
@@ -20440,20 +20440,15 @@ async function saveRunCopy(root, text2) {
20440
20440
  var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
20441
20441
  function formatMinutes(totalMin) {
20442
20442
  const whole = Math.max(0, Math.floor(totalMin));
20443
- if (whole < 1) return "<1 min";
20443
+ if (whole < 1) return "<1m";
20444
20444
  const hours = Math.floor(whole / 60);
20445
20445
  const minutes2 = whole % 60;
20446
- if (!hours) return `${minutes2} min`;
20447
- return minutes2 ? `${hours} h ${minutes2} min` : `${hours} h`;
20448
- }
20449
- function formatEstimate(totalMin) {
20450
- if (totalMin < 60) return formatMinutes(totalMin);
20451
- return `${Math.round(totalMin / 6) / 10} h`;
20446
+ if (!hours) return `${minutes2}m`;
20447
+ return minutes2 ? `${hours}h ${minutes2}m` : `${hours}h`;
20452
20448
  }
20453
20449
  function formatWorkDone(doneMin, totalMin, tookMin) {
20454
20450
  if (tookMin !== void 0) return formatMinutes(tookMin);
20455
- if (totalMin < 60 && doneMin >= 1) return `${Math.floor(doneMin)} of ${formatMinutes(totalMin)}`;
20456
- return `${doneMin < 1 ? "0 min" : formatMinutes(doneMin)} of ${formatMinutes(totalMin)}`;
20451
+ return `${doneMin < 1 ? "0m" : formatMinutes(doneMin)} of ${formatMinutes(totalMin)}`;
20457
20452
  }
20458
20453
  function formatClock(iso) {
20459
20454
  const date5 = new Date(iso);
@@ -20502,7 +20497,7 @@ function dashboardText(dashboard, nowMs) {
20502
20497
  for (const ticket of dashboard.tickets) {
20503
20498
  const stage = ticket.status === "working" && ticket.stage ? `, ${ticket.stage} stage` : "";
20504
20499
  const status = ticket.waitingFor ? `Waiting for ${ticket.waitingFor.id}` : STATUS[ticket.status];
20505
- lines.push(` ${ticket.id} ${status}${stage} ${formatEstimate(ticket.estimateMin)} ${ticket.title}`);
20500
+ lines.push(` ${ticket.id} ${status}${stage} ${formatMinutes(ticket.estimateMin)} ${ticket.title}`);
20506
20501
  if (ticket.source) lines.push(` Source: ${ticket.source.path ?? ticket.source.url}${ticket.source.label ? ` (${ticket.source.label})` : ""}`);
20507
20502
  }
20508
20503
  if (dashboard.questions.open.length) {
@@ -20690,7 +20685,7 @@ var commands = {
20690
20685
  const id = state.nextIds.ticket;
20691
20686
  return {
20692
20687
  event: { type: "ticket.add", id, title, estimateMin, status: statusOption(values.status), waitsFor, source },
20693
- message: `Added ${id}: ${title} (${estimateMin} min)${waitsFor ? `, waits for ${waitsFor}` : ""}${source ? `, source ${source.path ?? source.url}` : ""}`
20688
+ message: `Added ${id}: ${title} (${formatMinutes(estimateMin)})${waitsFor ? `, waits for ${waitsFor}` : ""}${source ? `, source ${source.path ?? source.url}` : ""}`
20694
20689
  };
20695
20690
  }
20696
20691
  },
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stevecastaneda/paseo-progress",
3
- "version": "0.4.0",
3
+ "version": "1.0.0",
4
4
  "description": "A live progress dashboard per worktree for Paseo, drawn from a file agents write with the paseo-progress command.",
5
5
  "license": "MIT",
6
6
  "author": "Steve Castaneda",
@@ -49,8 +49,8 @@
49
49
  "postpack": "rm -rf dist"
50
50
  },
51
51
  "devDependencies": {
52
- "@getpaseo/client": "^0.9.2",
53
- "@getpaseo/plugin": "^0.9.2",
52
+ "@getpaseo/client": "^0.11.2",
53
+ "@getpaseo/plugin": "^0.11.2",
54
54
  "@tanstack/react-query": "^5.90.11",
55
55
  "@types/node": "^20.9.0",
56
56
  "@types/react": "~19.2.0",
package/server/cli.ts CHANGED
@@ -11,6 +11,7 @@ import { findRoot, outside } from "./paths.ts";
11
11
  import { installLauncher } from "./launcher.ts";
12
12
  import { findStore, saveRunCopy } from "./store.ts";
13
13
  import { dashboardText } from "../shared/show.ts";
14
+ import { formatMinutes } from "../shared/format.ts";
14
15
  import { imageMimeType } from "../shared/attachments.ts";
15
16
  import { homedir } from "node:os";
16
17
 
@@ -199,7 +200,7 @@ const commands: Record<string, Command> = {
199
200
  const id = state.nextIds.ticket;
200
201
  return {
201
202
  event: { type: "ticket.add", id, title, estimateMin, status: statusOption(values.status), waitsFor, source },
202
- message: `Added ${id}: ${title} (${estimateMin} min)${waitsFor ? `, waits for ${waitsFor}` : ""}${source ? `, source ${source.path ?? source.url}` : ""}`,
203
+ message: `Added ${id}: ${title} (${formatMinutes(estimateMin)})${waitsFor ? `, waits for ${waitsFor}` : ""}${source ? `, source ${source.path ?? source.url}` : ""}`,
203
204
  };
204
205
  },
205
206
  },
package/shared/format.ts CHANGED
@@ -3,30 +3,22 @@
3
3
 
4
4
  const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
5
5
 
6
+ // Durations in the compact style the other plugins and most dashboards use:
7
+ // "45m", "2h", "2h 15m". Minutes are whole, so estimates read exactly as set.
6
8
  export function formatMinutes(totalMin: number): string {
7
9
  const whole = Math.max(0, Math.floor(totalMin));
8
- if (whole < 1) return "<1 min";
10
+ if (whole < 1) return "<1m";
9
11
  const hours = Math.floor(whole / 60);
10
12
  const minutes = whole % 60;
11
- if (!hours) return `${minutes} min`;
12
- return minutes ? `${hours} h ${minutes} min` : `${hours} h`;
13
+ if (!hours) return `${minutes}m`;
14
+ return minutes ? `${hours}h ${minutes}m` : `${hours}h`;
13
15
  }
14
16
 
15
- // A ticket's estimate, the way people say one: "45 min" under an hour, then
16
- // hours to one decimal: "1 h", "1.5 h", "2.1 h".
17
- export function formatEstimate(totalMin: number): string {
18
- if (totalMin < 60) return formatMinutes(totalMin);
19
- return `${Math.round(totalMin / 6) / 10} h`;
20
- }
21
-
22
- // Estimated work done out of the total, in the units people estimate in:
23
- // "5 of 10 min", "1 h 30 min of 2 h". Under an hour on both sides, the unit is
24
- // said once.
25
- // Once everything is done, how long it really took instead: "1 h 20 min".
17
+ // Estimated work done out of the total: "5m of 10m", "1h 30m of 2h".
18
+ // Once everything is done, how long it really took instead: "1h 20m".
26
19
  export function formatWorkDone(doneMin: number, totalMin: number, tookMin?: number): string {
27
20
  if (tookMin !== undefined) return formatMinutes(tookMin);
28
- if (totalMin < 60 && doneMin >= 1) return `${Math.floor(doneMin)} of ${formatMinutes(totalMin)}`;
29
- return `${doneMin < 1 ? "0 min" : formatMinutes(doneMin)} of ${formatMinutes(totalMin)}`;
21
+ return `${doneMin < 1 ? "0m" : formatMinutes(doneMin)} of ${formatMinutes(totalMin)}`;
30
22
  }
31
23
 
32
24
  export function formatClock(iso: string): string {
package/shared/show.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Dashboard } from "./dashboard.ts";
2
2
  import { headlineText, shortTitle } from "./dashboard.ts";
3
- import { formatAgo, formatClock, formatEstimate, formatMinutes, formatWorkDone, minutesSince } from "./format.ts";
3
+ import { formatAgo, formatClock, formatMinutes, formatWorkDone, minutesSince } from "./format.ts";
4
4
 
5
5
  const STATUS: Record<string, string> = { not_started: "Not started", working: "Working", blocked: "Blocked", done: "Done", skipped: "Skipped" };
6
6
 
@@ -38,7 +38,7 @@ export function dashboardText(dashboard: Dashboard, nowMs: number): string {
38
38
  for (const ticket of dashboard.tickets) {
39
39
  const stage = ticket.status === "working" && ticket.stage ? `, ${ticket.stage} stage` : "";
40
40
  const status = ticket.waitingFor ? `Waiting for ${ticket.waitingFor.id}` : STATUS[ticket.status];
41
- lines.push(` ${ticket.id} ${status}${stage} ${formatEstimate(ticket.estimateMin)} ${ticket.title}`);
41
+ lines.push(` ${ticket.id} ${status}${stage} ${formatMinutes(ticket.estimateMin)} ${ticket.title}`);
42
42
  if (ticket.source) lines.push(` Source: ${ticket.source.path ?? ticket.source.url}${ticket.source.label ? ` (${ticket.source.label})` : ""}`);
43
43
  }
44
44
  if (dashboard.questions.open.length) {