@stevecastaneda/paseo-progress 0.3.5 → 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/OVERVIEW.md +1 -1
- package/README.md +20 -12
- package/client/panel.tsx +8 -8
- package/client/pill-state.ts +3 -1
- package/client/when.tsx +11 -4
- package/dist/server/cli.js +6 -11
- package/icon.png +0 -0
- package/images/1-panel.png +0 -0
- package/images/2-ticket.png +0 -0
- package/images/3-question.png +0 -0
- package/images/4-pill.png +0 -0
- package/package.json +4 -3
- package/paseo-plugin.json +8 -1
- package/server/cli.ts +2 -1
- package/shared/format.ts +8 -16
- package/shared/show.ts +2 -2
package/OVERVIEW.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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.11 or later, and Node.js 22.18 or later on the computer running the Paseo daemon.
|
|
2
2
|
|
|
3
3
|
The panel shows how much of the estimated work is done, what the agent is doing right now, and where each ticket stands. A ticket turns stuck when it's blocked or runs past its estimate without an update. Press a ticket to see its timeline, activity, questions and deliverables. Images and text files the agent records open in a preview inside Paseo. Any other file opens in an app on the computer running the Paseo daemon, and so does a previewed file when you press Open. That's true from your phone too. The app opens on that computer, not on the phone.
|
|
4
4
|
|
package/README.md
CHANGED
|
@@ -2,21 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
# Progress
|
|
4
4
|
|
|
5
|
-
A plugin for Paseo 0.
|
|
5
|
+
A plugin for Paseo 0.11 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
|
-
<img src="images/1-panel.png" alt="The Progress panel in Explorer" width="
|
|
7
|
+
<img src="images/1-panel.png" alt="The Progress panel in Explorer" width="440">
|
|
8
8
|
|
|
9
9
|
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 "
|
|
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.
|
|
17
17
|
- **Activity**, **Answered** questions and **Deliverables** (press one to open it), sharing one card with a tab for each; the panel remembers the tab you picked. Each shows the newest 10, with **Show 10 older** below
|
|
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
|
+
|
|
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
|
+
|
|
23
|
+
<img src="images/4-pill.png" alt="The pill above the message box, showing two questions waiting" width="600">
|
|
20
24
|
|
|
21
25
|
On phones, where Paseo has no Explorer pane, Progress opens as a tab of its own, and the pill stays above the message box while a run is open so there's always a way in. SVG files preview on desktop; on phones they open on the daemon host.
|
|
22
26
|
|
|
@@ -119,17 +123,21 @@ The progress file, `progress.jsonl`, holds one JSON event per line, each with a
|
|
|
119
123
|
- Previews cover images up to 10 MB and the first 256 KB of a text file. Other files open in their default app.
|
|
120
124
|
- Stale warnings and overdue times use the daemon host's clock.
|
|
121
125
|
|
|
122
|
-
##
|
|
126
|
+
## Changelog
|
|
123
127
|
|
|
124
|
-
|
|
125
|
-
cd progress
|
|
126
|
-
npm install
|
|
127
|
-
npm run dev
|
|
128
|
-
```
|
|
128
|
+
### 1.0.0
|
|
129
129
|
|
|
130
|
-
|
|
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.
|
|
131
135
|
|
|
132
|
-
|
|
136
|
+
### 0.4.0
|
|
137
|
+
|
|
138
|
+
- Needs Paseo 0.11 or later. The plugin now carries its own icon and screenshots, and Paseo 0.9 and 0.10 refuse to load a plugin that does. On an older Paseo, the version you already have keeps working.
|
|
139
|
+
- The plugin store now takes the icon and screenshots from each release, so they match the version you install.
|
|
140
|
+
- New screenshots in this README.
|
|
133
141
|
|
|
134
142
|
### 0.3.5
|
|
135
143
|
|
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 {
|
|
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 "
|
|
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:
|
|
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 "
|
|
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:
|
|
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
|
);
|
package/client/pill-state.ts
CHANGED
|
@@ -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
|
-
//
|
|
10
|
-
//
|
|
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
|
-
|
|
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 }}>({
|
|
22
|
+
{formatClock(iso)} <Text style={{ color: colors.foregroundMuted, opacity: 0.7 }}>({ago})</Text>
|
|
16
23
|
</Text>
|
|
17
24
|
);
|
|
18
25
|
}
|
package/dist/server/cli.js
CHANGED
|
@@ -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 "<
|
|
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}
|
|
20447
|
-
return minutes2 ? `${hours}
|
|
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
|
-
|
|
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} ${
|
|
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}
|
|
20688
|
+
message: `Added ${id}: ${title} (${formatMinutes(estimateMin)})${waitsFor ? `, waits for ${waitsFor}` : ""}${source ? `, source ${source.path ?? source.url}` : ""}`
|
|
20694
20689
|
};
|
|
20695
20690
|
}
|
|
20696
20691
|
},
|
package/icon.png
ADDED
|
Binary file
|
package/images/1-panel.png
CHANGED
|
Binary file
|
package/images/2-ticket.png
CHANGED
|
Binary file
|
package/images/3-question.png
CHANGED
|
Binary file
|
package/images/4-pill.png
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stevecastaneda/paseo-progress",
|
|
3
|
-
"version": "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",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"shared/",
|
|
33
33
|
"skills/",
|
|
34
34
|
"images/",
|
|
35
|
+
"icon.png",
|
|
35
36
|
"tsconfig.json",
|
|
36
37
|
"!**/*.test.ts"
|
|
37
38
|
],
|
|
@@ -48,8 +49,8 @@
|
|
|
48
49
|
"postpack": "rm -rf dist"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
|
-
"@getpaseo/client": "^0.
|
|
52
|
-
"@getpaseo/plugin": "^0.
|
|
52
|
+
"@getpaseo/client": "^0.11.2",
|
|
53
|
+
"@getpaseo/plugin": "^0.11.2",
|
|
53
54
|
"@tanstack/react-query": "^5.90.11",
|
|
54
55
|
"@types/node": "^20.9.0",
|
|
55
56
|
"@types/react": "~19.2.0",
|
package/paseo-plugin.json
CHANGED
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}
|
|
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 "<
|
|
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}
|
|
12
|
-
return minutes ? `${hours}
|
|
13
|
+
if (!hours) return `${minutes}m`;
|
|
14
|
+
return minutes ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
//
|
|
16
|
-
//
|
|
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
|
-
|
|
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,
|
|
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} ${
|
|
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) {
|