@stevecastaneda/paseo-progress 0.1.5 → 0.1.7

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 { finishedToFold, isFinished, 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,58 @@ 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 fold into one row at the top, so the work still ahead is
420
+ // near the top without scrolling (see finishedToFold).
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 finished = finishedToFold(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 (!finished.length) return <>{dashboard.tickets.map(row)}</>;
433
+ return (
434
+ <>
435
+ <FinishedRow colors={colors} tickets={finished} shown={shown} onPress={toggle} />
436
+ {shown ? finished.map(row) : null}
437
+ {dashboard.tickets.filter((ticket) => !isFinished(ticket)).map(row)}
438
+ </>
439
+ );
440
+ }
441
+
442
+ // Sits on the ticket rows' columns: status icon, label, estimate, then the
443
+ // chevron, which points the way the list will move, like "Show 10 older".
444
+ function FinishedRow({ colors, tickets, shown, onPress }: { colors: Colors; tickets: Ticket[]; shown: boolean; onPress(): void }) {
445
+ const done = tickets.filter((ticket) => ticket.status === "done").length;
446
+ const skipped = tickets.length - done;
447
+ const label = [done ? `${done} done` : "", skipped ? `${skipped} skipped` : ""].filter(Boolean).join(", ");
448
+ const estimateMin = tickets.reduce((sum, ticket) => sum + (ticket.status === "done" ? ticket.estimateMin : 0), 0);
449
+ const chevron = shown ? "ChevronUp" : "ChevronDown";
450
+ return (
451
+ <PressableRow colors={colors} accessibilityRole="button" accessibilityState={{ expanded: shown }}
452
+ accessibilityLabel={`${shown ? "Hide" : "Show"} ${label}`} onPress={onPress}
453
+ style={{ flexDirection: "row", alignItems: "flex-start", gap: 10, paddingHorizontal: 10, paddingVertical: 8, borderTopWidth: 1, borderTopColor: colors.border }}>
454
+ <View style={{ width: 16, paddingTop: 2, alignItems: "center" }}>
455
+ <Icon name={done ? "Check" : "CircleSlash"} size={14} color={done ? colors.statusSuccess : colors.foregroundMuted} />
456
+ </View>
457
+ <Text style={{ flex: 1, minWidth: 0, color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontWeight: "600", fontVariant: ["tabular-nums"] }}>{label}</Text>
458
+ <View style={{ width: 12 }} />
459
+ <Text style={{ minWidth: 40, flexShrink: 0, textAlign: "right", color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontVariant: ["tabular-nums"] }}>{estimateMin ? formatEstimate(estimateMin) : ""}</Text>
460
+ <View style={{ paddingTop: 2 }}>
461
+ <IconSwap swapKey={chevron} size={14}><Icon name={chevron} size={14} color={colors.foregroundMuted} /></IconSwap>
462
+ </View>
463
+ </PressableRow>
464
+ );
465
+ }
466
+
418
467
  // Press for the ticket's story. Hover and press only tint the row: it's a list, pressed often.
419
468
  function TicketRow({ colors, ticket, live, now, onOpen, deliverables }: { colors: Colors; ticket: Ticket; live: boolean; now: number; onOpen(): void; deliverables: number }) {
420
469
  const muted = ticket.status === "skipped";
@@ -328,6 +328,18 @@ 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
+ // The finished tickets (done or skipped) the panel folds into one row at the
332
+ // top, in plan order, so the work still ahead sits near the top. A ticket
333
+ // finished out of order folds too. A single one isn't worth a fold row. Once
334
+ // every ticket is finished nothing is ahead, and the whole list is the record
335
+ // of the run, so nothing folds.
336
+ export function finishedToFold(tickets) {
337
+ const finished = tickets.filter(isFinished);
338
+ return finished.length >= 2 && finished.length < tickets.length ? finished : [];
339
+ }
340
+ export function isFinished(ticket) {
341
+ return ticket.status === "done" || ticket.status === "skipped";
342
+ }
331
343
  function plural(count, noun) {
332
344
  return `${count} ${count === 1 ? noun : `${noun}s`}`;
333
345
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@stevecastaneda/paseo-progress",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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.7",
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.7",
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,20 @@ export function shortTitle(title: string): string {
449
449
  return index > 0 && index <= 24 ? title.slice(0, index) : title;
450
450
  }
451
451
 
452
+ // The finished tickets (done or skipped) the panel folds into one row at the
453
+ // top, in plan order, so the work still ahead sits near the top. A ticket
454
+ // finished out of order folds too. A single one isn't worth a fold row. Once
455
+ // every ticket is finished nothing is ahead, and the whole list is the record
456
+ // of the run, so nothing folds.
457
+ export function finishedToFold(tickets: Ticket[]): Ticket[] {
458
+ const finished = tickets.filter(isFinished);
459
+ return finished.length >= 2 && finished.length < tickets.length ? finished : [];
460
+ }
461
+
462
+ export function isFinished(ticket: Ticket): boolean {
463
+ return ticket.status === "done" || ticket.status === "skipped";
464
+ }
465
+
452
466
  function plural(count: number, noun: string): string {
453
467
  return `${count} ${count === 1 ? noun : `${noun}s`}`;
454
468
  }