@stevecastaneda/paseo-progress 0.1.5 → 0.1.6

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/client/panel.tsx CHANGED
@@ -10,7 +10,7 @@ import { raised } from "./surfaces";
10
10
  import { When } from "./when";
11
11
  import React, { useEffect, useRef, useState } from "react";
12
12
  import { ActivityIndicator, Animated, Platform, Pressable, ScrollView, Text, View } from "react-native";
13
- import { shortTitle, type Activity, type Dashboard, type FileIssue, type ProgressSegment, type StuckItem, type Ticket } from "../shared/dashboard";
13
+ import { finishedAtTop, shortTitle, type Activity, type Dashboard, type FileIssue, type ProgressSegment, type StuckItem, type Ticket } from "../shared/dashboard";
14
14
  import { PROGRESS_FILE, type TicketStatus } from "../shared/events";
15
15
  import { formatEstimate, formatMinutes, formatWorkDone, minutesSince } from "../shared/format";
16
16
  import { markPanelOpened } from "../shared/rpc";
@@ -214,10 +214,7 @@ function DashboardView({ colors, dashboard, agentRunning, compact, workspaceId,
214
214
  No {(dashboard.run?.itemLabel ?? "ticket").toLowerCase()}s yet.
215
215
  </Text>
216
216
  ) : (
217
- dashboard.tickets.map((ticket) => (
218
- <TicketRow key={ticket.id} colors={colors} ticket={ticket} live={live} now={now} onOpen={() => setOpenTicket(ticket.id)}
219
- deliverables={dashboard.deliverables.filter((deliverable) => deliverable.ticketId === ticket.id).length} />
220
- ))
217
+ <TicketList colors={colors} dashboard={dashboard} live={live} now={now} workspaceId={workspaceId} onOpen={setOpenTicket} />
221
218
  )}
222
219
  <TicketDialogs colors={colors} dashboard={dashboard} openId={openTicket} setOpenId={setOpenTicket} now={now} live={live} context={attachmentContext} />
223
220
  </Card>
@@ -415,6 +412,59 @@ function StuckSection({ colors, items, now }: { colors: Colors; items: StuckItem
415
412
  );
416
413
  }
417
414
 
415
+ // Whether the finished tickets at the top are shown, per workspace, kept for
416
+ // the app session like the history tab.
417
+ const finishedShown = new Map<string, boolean>();
418
+
419
+ // Finished tickets at the top fold into one row, so the work still ahead is
420
+ // near the top without scrolling (see finishedAtTop).
421
+ function TicketList({ colors, dashboard, live, now, workspaceId, onOpen }: { colors: Colors; dashboard: Dashboard; live: boolean; now: number; workspaceId: string; onOpen(id: string): void }) {
422
+ const [shown, setShown] = useState(() => finishedShown.get(workspaceId) ?? false);
423
+ const toggle = () => {
424
+ finishedShown.set(workspaceId, !shown);
425
+ setShown(!shown);
426
+ };
427
+ const folded = finishedAtTop(dashboard.tickets);
428
+ const row = (ticket: Ticket) => (
429
+ <TicketRow key={ticket.id} colors={colors} ticket={ticket} live={live} now={now} onOpen={() => onOpen(ticket.id)}
430
+ deliverables={dashboard.deliverables.filter((deliverable) => deliverable.ticketId === ticket.id).length} />
431
+ );
432
+ if (!folded) return <>{dashboard.tickets.map(row)}</>;
433
+ const finished = dashboard.tickets.slice(0, folded);
434
+ return (
435
+ <>
436
+ <FinishedRow colors={colors} tickets={finished} shown={shown} onPress={toggle} />
437
+ {shown ? finished.map(row) : null}
438
+ {dashboard.tickets.slice(folded).map(row)}
439
+ </>
440
+ );
441
+ }
442
+
443
+ // Sits on the ticket rows' columns: status icon, label, estimate, then the
444
+ // chevron, which points the way the list will move, like "Show 10 older".
445
+ function FinishedRow({ colors, tickets, shown, onPress }: { colors: Colors; tickets: Ticket[]; shown: boolean; onPress(): void }) {
446
+ const done = tickets.filter((ticket) => ticket.status === "done").length;
447
+ const skipped = tickets.length - done;
448
+ const label = [done ? `${done} done` : "", skipped ? `${skipped} skipped` : ""].filter(Boolean).join(", ");
449
+ const estimateMin = tickets.reduce((sum, ticket) => sum + (ticket.status === "done" ? ticket.estimateMin : 0), 0);
450
+ const chevron = shown ? "ChevronUp" : "ChevronDown";
451
+ return (
452
+ <PressableRow colors={colors} accessibilityRole="button" accessibilityState={{ expanded: shown }}
453
+ accessibilityLabel={`${shown ? "Hide" : "Show"} ${label}`} onPress={onPress}
454
+ style={{ flexDirection: "row", alignItems: "flex-start", gap: 10, paddingHorizontal: 10, paddingVertical: 8, borderTopWidth: 1, borderTopColor: colors.border }}>
455
+ <View style={{ width: 16, paddingTop: 2, alignItems: "center" }}>
456
+ <Icon name={done ? "Check" : "CircleSlash"} size={14} color={done ? colors.statusSuccess : colors.foregroundMuted} />
457
+ </View>
458
+ <Text style={{ flex: 1, minWidth: 0, color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontWeight: "600", fontVariant: ["tabular-nums"] }}>{label}</Text>
459
+ <View style={{ width: 12 }} />
460
+ <Text style={{ minWidth: 40, flexShrink: 0, textAlign: "right", color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontVariant: ["tabular-nums"] }}>{estimateMin ? formatEstimate(estimateMin) : ""}</Text>
461
+ <View style={{ paddingTop: 2 }}>
462
+ <IconSwap swapKey={chevron} size={14}><Icon name={chevron} size={14} color={colors.foregroundMuted} /></IconSwap>
463
+ </View>
464
+ </PressableRow>
465
+ );
466
+ }
467
+
418
468
  // Press for the ticket's story. Hover and press only tint the row: it's a list, pressed often.
419
469
  function TicketRow({ colors, ticket, live, now, onOpen, deliverables }: { colors: Colors; ticket: Ticket; live: boolean; now: number; onOpen(): void; deliverables: number }) {
420
470
  const muted = ticket.status === "skipped";
@@ -328,6 +328,16 @@ export function shortTitle(title) {
328
328
  const index = title.indexOf(":");
329
329
  return index > 0 && index <= 24 ? title.slice(0, index) : title;
330
330
  }
331
+ // How many tickets at the top of the list are finished (done or skipped), so
332
+ // the panel can fold them into one row and the work still ahead sits near the
333
+ // top. Only the leading run folds: a ticket finished out of order stays in
334
+ // place, so the plan's order never looks shuffled. A single one isn't worth a
335
+ // fold row. Once every ticket is finished nothing is ahead, and the whole list
336
+ // is the record of the run, so nothing folds.
337
+ export function finishedAtTop(tickets) {
338
+ const index = tickets.findIndex((ticket) => ticket.status !== "done" && ticket.status !== "skipped");
339
+ return index >= 2 ? index : 0;
340
+ }
331
341
  function plural(count, noun) {
332
342
  return `${count} ${count === 1 ? noun : `${noun}s`}`;
333
343
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@stevecastaneda/paseo-progress",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@stevecastaneda/paseo-progress",
9
- "version": "0.1.5",
9
+ "version": "0.1.6",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@getpaseo/client": "^0.9.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stevecastaneda/paseo-progress",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
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",
@@ -449,6 +449,17 @@ export function shortTitle(title: string): string {
449
449
  return index > 0 && index <= 24 ? title.slice(0, index) : title;
450
450
  }
451
451
 
452
+ // How many tickets at the top of the list are finished (done or skipped), so
453
+ // the panel can fold them into one row and the work still ahead sits near the
454
+ // top. Only the leading run folds: a ticket finished out of order stays in
455
+ // place, so the plan's order never looks shuffled. A single one isn't worth a
456
+ // fold row. Once every ticket is finished nothing is ahead, and the whole list
457
+ // is the record of the run, so nothing folds.
458
+ export function finishedAtTop(tickets: Ticket[]): number {
459
+ const index = tickets.findIndex((ticket) => ticket.status !== "done" && ticket.status !== "skipped");
460
+ return index >= 2 ? index : 0;
461
+ }
462
+
452
463
  function plural(count: number, noun: string): string {
453
464
  return `${count} ${count === 1 ? noun : `${noun}s`}`;
454
465
  }