@stevecastaneda/paseo-progress 0.1.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/LICENSE +21 -0
- package/README.md +109 -0
- package/client/attachments.tsx +95 -0
- package/client/dashboard-query.ts +32 -0
- package/client/deliverables.tsx +67 -0
- package/client/dialog-stack.tsx +104 -0
- package/client/directory.ts +90 -0
- package/client/launcher.tsx +111 -0
- package/client/motion.tsx +173 -0
- package/client/panel-opened.ts +13 -0
- package/client/panel.tsx +480 -0
- package/client/pill-popover.tsx +122 -0
- package/client/pill-state.ts +34 -0
- package/client/pills.tsx +235 -0
- package/client/preview.tsx +228 -0
- package/client/questions.tsx +248 -0
- package/client/row.tsx +34 -0
- package/client/section-title.tsx +24 -0
- package/client/show-more.tsx +37 -0
- package/client/spinner.tsx +35 -0
- package/client/status-badge.tsx +30 -0
- package/client/surfaces.ts +45 -0
- package/client/ticket-dialog.tsx +203 -0
- package/client/when.tsx +15 -0
- package/dist/server/cli.js +528 -0
- package/dist/server/launcher.js +90 -0
- package/dist/server/lock.js +34 -0
- package/dist/server/paths.js +6 -0
- package/dist/server/progress-file.js +123 -0
- package/dist/shared/attachments.js +86 -0
- package/dist/shared/dashboard.js +369 -0
- package/dist/shared/events.js +117 -0
- package/dist/shared/format.js +37 -0
- package/dist/shared/show.js +72 -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/index.client.tsx +33 -0
- package/index.server.ts +19 -0
- package/npm-shrinkwrap.json +4041 -0
- package/package.json +62 -0
- package/paseo-plugin.json +7 -0
- package/server/cli.ts +541 -0
- package/server/dashboard.ts +46 -0
- package/server/launcher.ts +97 -0
- package/server/lock.ts +32 -0
- package/server/open.ts +107 -0
- package/server/paths.ts +7 -0
- package/server/progress-file.ts +141 -0
- package/server/skill.ts +85 -0
- package/shared/attachments.ts +100 -0
- package/shared/dashboard.ts +491 -0
- package/shared/events.ts +131 -0
- package/shared/format.ts +39 -0
- package/shared/rpc.ts +109 -0
- package/shared/show.ts +68 -0
- package/shared/ticket-story.ts +93 -0
- package/skills/paseo-progress/SKILL.md +79 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
|
|
2
|
+
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Text, View } from "react-native";
|
|
5
|
+
import type { Dashboard, Ticket } from "../shared/dashboard";
|
|
6
|
+
import { formatMinutes } from "../shared/format";
|
|
7
|
+
import { type Link, ticketStory, type TimelineStep } from "../shared/ticket-story";
|
|
8
|
+
import { type Attachment, AttachmentList, deliverableAttachment } from "./attachments";
|
|
9
|
+
import { StackedDialog } from "./dialog-stack";
|
|
10
|
+
import { IconSwap } from "./motion";
|
|
11
|
+
import { PressableRow } from "./row";
|
|
12
|
+
import { Spinner, StalledPulse } from "./spinner";
|
|
13
|
+
import { StatusBadge } from "./status-badge";
|
|
14
|
+
import { When } from "./when";
|
|
15
|
+
|
|
16
|
+
type Colors = PluginWorkspacePanelProps["theme"]["colors"];
|
|
17
|
+
type AttachmentContext = { workspaceId: string; workspaceDirectory: string; navigation: PluginWorkspacePanelProps["navigation"] };
|
|
18
|
+
|
|
19
|
+
// A ticket's story, opened from its row: where its time went, and what came out
|
|
20
|
+
// of it. Its questions and previews open in the same dialog (see StackedDialog).
|
|
21
|
+
export function TicketDialogs({ colors, dashboard, openId, setOpenId, now, live, context }: {
|
|
22
|
+
colors: Colors;
|
|
23
|
+
dashboard: Dashboard;
|
|
24
|
+
openId: string | null;
|
|
25
|
+
setOpenId(id: string | null): void;
|
|
26
|
+
now: number;
|
|
27
|
+
live: boolean;
|
|
28
|
+
context: AttachmentContext;
|
|
29
|
+
}) {
|
|
30
|
+
return (
|
|
31
|
+
<StackedDialog colors={colors} root={openId ? { kind: "ticket", id: openId } : null} onClose={() => setOpenId(null)}
|
|
32
|
+
questions={[...dashboard.questions.open, ...dashboard.questions.answered]} dashboard={dashboard} now={now} live={live} context={context} />
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function TicketStoryView({ colors, dashboard, ticket, now, live, onOpenAttachment, onOpenQuestion }: {
|
|
37
|
+
colors: Colors;
|
|
38
|
+
dashboard: Dashboard;
|
|
39
|
+
ticket: Ticket;
|
|
40
|
+
now: number;
|
|
41
|
+
live: boolean;
|
|
42
|
+
onOpenAttachment(attachment: Attachment, group: Attachment[]): void;
|
|
43
|
+
onOpenQuestion(id: string): void;
|
|
44
|
+
}) {
|
|
45
|
+
const story = ticketStory(ticket, {
|
|
46
|
+
deliverables: dashboard.deliverables,
|
|
47
|
+
questions: [...dashboard.questions.open, ...dashboard.questions.answered].sort((a, b) => Date.parse(a.askedAt) - Date.parse(b.askedAt)),
|
|
48
|
+
activity: [...dashboard.activity].reverse(),
|
|
49
|
+
}, now);
|
|
50
|
+
const overMin = story.workedMin - ticket.estimateMin;
|
|
51
|
+
const byTime = [...story.questions, ...story.activity].some((item) => item.link === "by-time");
|
|
52
|
+
return (
|
|
53
|
+
<View style={{ gap: 20 }}>
|
|
54
|
+
<View style={{ gap: 6 }}>
|
|
55
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
|
|
56
|
+
<StatusBadge colors={colors} status={ticket.status} waiting={Boolean(ticket.waitingFor)} />
|
|
57
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
|
|
58
|
+
<Icon name="Timer" size={12} color={colors.foregroundMuted} />
|
|
59
|
+
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontVariant: ["tabular-nums"] }}>
|
|
60
|
+
Worked {formatMinutes(story.workedMin)} of a {formatMinutes(ticket.estimateMin)} estimate
|
|
61
|
+
{overMin >= 1 ? <Text style={{ color: colors.statusDanger }}>, {formatMinutes(overMin)} over</Text> : null}
|
|
62
|
+
</Text>
|
|
63
|
+
</View>
|
|
64
|
+
</View>
|
|
65
|
+
{ticket.waitingFor ? (
|
|
66
|
+
<View style={{ flexDirection: "row", gap: 4 }}>
|
|
67
|
+
{/* Held on the first line when a long title wraps. */}
|
|
68
|
+
<View style={{ height: 18, justifyContent: "center" }}><Icon name="Hourglass" size={12} color={colors.foregroundMuted} /></View>
|
|
69
|
+
<Text style={{ flex: 1, color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>Waits for {ticket.waitingFor.id}: {ticket.waitingFor.title}</Text>
|
|
70
|
+
</View>
|
|
71
|
+
) : null}
|
|
72
|
+
{ticket.note ? <Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 19 }}>{ticket.note}</Text> : null}
|
|
73
|
+
</View>
|
|
74
|
+
|
|
75
|
+
<Section colors={colors} icon="History" title="Timeline">
|
|
76
|
+
<View>
|
|
77
|
+
{story.timeline.map((step, index) => {
|
|
78
|
+
const last = index === story.timeline.length - 1;
|
|
79
|
+
// A current "Blocked" that's only waiting on another ticket reads as waiting, in the calm color.
|
|
80
|
+
const shown = last && ticket.waitingFor && step.status === "blocked"
|
|
81
|
+
? { ...step, label: `Waiting for ${ticket.waitingFor.id}`, waiting: true }
|
|
82
|
+
: step;
|
|
83
|
+
return <TimelineRow key={`${step.at}-${index}`} colors={colors} step={shown} last={last} now={now} live={live} />;
|
|
84
|
+
})}
|
|
85
|
+
</View>
|
|
86
|
+
</Section>
|
|
87
|
+
|
|
88
|
+
{story.deliverables.length ? (
|
|
89
|
+
<Section colors={colors} icon="Package" title="Deliverables">
|
|
90
|
+
<AttachmentList colors={colors} attachments={story.deliverables.map(deliverableAttachment)} onOpen={onOpenAttachment} />
|
|
91
|
+
</Section>
|
|
92
|
+
) : null}
|
|
93
|
+
|
|
94
|
+
{story.questions.length ? (
|
|
95
|
+
<Section colors={colors} icon="MessageCircleQuestion" title="Questions">
|
|
96
|
+
<View style={{ gap: 2 }}>
|
|
97
|
+
{story.questions.map((question) => {
|
|
98
|
+
const chosen = question.answer ? question.options.find((option) => option.letter === question.answer!.choice) : undefined;
|
|
99
|
+
return (
|
|
100
|
+
<PressableRow key={question.id} colors={colors} onSurface1 accessibilityRole="button" accessibilityLabel={`Open ${question.id}`}
|
|
101
|
+
onPress={() => onOpenQuestion(question.id)}
|
|
102
|
+
style={{ flexDirection: "row", gap: 8, paddingVertical: 3, paddingHorizontal: 4, marginHorizontal: -4, borderRadius: 4 }}>
|
|
103
|
+
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontWeight: "600", fontVariant: ["tabular-nums"], minWidth: 30 }}>{question.id}</Text>
|
|
104
|
+
<Text style={{ flex: 1, color: colors.foreground, fontSize: 12, lineHeight: 18 }}>
|
|
105
|
+
{question.title}{" "}
|
|
106
|
+
{question.answer
|
|
107
|
+
? <Text style={{ color: colors.statusSuccess, fontWeight: "600" }}>{question.answer.choice}{chosen ? <Text style={{ fontWeight: "400" }}> {chosen.label}</Text> : null}</Text>
|
|
108
|
+
: <Text style={{ color: question.waits ? colors.statusWarning : colors.foregroundMuted }}>Open, default {question.default}</Text>}
|
|
109
|
+
<LinkNote colors={colors} link={question.link} />
|
|
110
|
+
</Text>
|
|
111
|
+
<View style={{ paddingTop: 2 }}><Icon name="ChevronRight" size={14} color={colors.foregroundMuted} /></View>
|
|
112
|
+
</PressableRow>
|
|
113
|
+
);
|
|
114
|
+
})}
|
|
115
|
+
</View>
|
|
116
|
+
</Section>
|
|
117
|
+
) : null}
|
|
118
|
+
|
|
119
|
+
{story.activity.length ? (
|
|
120
|
+
<Section colors={colors} icon="Activity" title="Activity">
|
|
121
|
+
<View style={{ gap: 8 }}>
|
|
122
|
+
{story.activity.map((entry) => (
|
|
123
|
+
<View key={entry.id} style={{ gap: 1 }}>
|
|
124
|
+
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>{entry.text}<LinkNote colors={colors} link={entry.link} /></Text>
|
|
125
|
+
<Text style={{ color: colors.foregroundMuted, fontSize: 11, lineHeight: 16, opacity: 0.8 }}><When colors={colors} iso={entry.at} now={now} /></Text>
|
|
126
|
+
</View>
|
|
127
|
+
))}
|
|
128
|
+
</View>
|
|
129
|
+
</Section>
|
|
130
|
+
) : null}
|
|
131
|
+
|
|
132
|
+
{byTime ? (
|
|
133
|
+
<Text style={{ color: colors.foregroundMuted, fontSize: 11, lineHeight: 16 }}>
|
|
134
|
+
Items marked “by time” weren't tagged to {ticket.id}; they happened while it was the ticket being worked on.
|
|
135
|
+
</Text>
|
|
136
|
+
) : null}
|
|
137
|
+
</View>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// A section heading: a 14px icon beside the 13px semibold title, like the panel's cards.
|
|
142
|
+
function Section({ colors, icon, title, children }: { colors: Colors; icon: string; title: string; children: React.ReactNode }) {
|
|
143
|
+
return (
|
|
144
|
+
<View style={{ gap: 8 }}>
|
|
145
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
|
146
|
+
<Icon name={icon} size={14} color={colors.foregroundMuted} />
|
|
147
|
+
<Text accessibilityRole="header" style={{ color: colors.foreground, fontSize: 13, lineHeight: 18, fontWeight: "600" }}>{title}</Text>
|
|
148
|
+
</View>
|
|
149
|
+
{children}
|
|
150
|
+
</View>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function LinkNote({ colors, link }: { colors: Colors; link: Link }) {
|
|
155
|
+
return link === "by-time" ? <Text style={{ color: colors.foregroundMuted, opacity: 0.7 }}> by time</Text> : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The same icons as the ticket rows, so a step reads like the status it was.
|
|
159
|
+
// Past stage steps get an accent ringed dot: work that happened, now over.
|
|
160
|
+
function Marker({ colors, step }: { colors: Colors; step: TimelineStep & { waiting?: boolean } }) {
|
|
161
|
+
const icon = step.waiting ? { name: "Hourglass", color: colors.foregroundMuted }
|
|
162
|
+
: step.label === "Added" ? { name: "CirclePlus", color: colors.foregroundMuted }
|
|
163
|
+
: step.status === "done" ? { name: "CircleCheck", color: colors.statusSuccess }
|
|
164
|
+
: step.status === "blocked" ? { name: "Ban", color: colors.statusDanger }
|
|
165
|
+
: step.status === "skipped" ? { name: "CircleSlash", color: colors.foregroundMuted }
|
|
166
|
+
: step.status === "not_started" ? { name: "Circle", color: colors.foregroundMuted }
|
|
167
|
+
: { name: "CircleDot", color: colors.accent };
|
|
168
|
+
return <Icon name={icon.name} size={12} color={icon.color} />;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// A rail of status icons joined by a line; the step still going shows the live spinner.
|
|
172
|
+
function TimelineRow({ colors, step, last, now, live }: { colors: Colors; step: TimelineStep & { waiting?: boolean }; last: boolean; now: number; live: boolean }) {
|
|
173
|
+
const current = last && step.minutes !== null;
|
|
174
|
+
const working = current && step.status === "working";
|
|
175
|
+
return (
|
|
176
|
+
// The rail is as wide as a section icon, so markers sit under it and labels line up with the title.
|
|
177
|
+
<View style={{ flexDirection: "row", gap: 6 }}>
|
|
178
|
+
<View style={{ width: 14, alignItems: "center" }}>
|
|
179
|
+
{/* Optical: centers the marker on the 18px first line. */}
|
|
180
|
+
<View style={{ height: 18, justifyContent: "center" }}>
|
|
181
|
+
<IconSwap swapKey={working ? (live ? "live" : "stale") : step.waiting ? "waiting" : step.status} size={12}>
|
|
182
|
+
{working ? (live ? <Spinner color={colors.accent} /> : <StalledPulse color={colors.statusWarning} />) : <Marker colors={colors} step={step} />}
|
|
183
|
+
</IconSwap>
|
|
184
|
+
</View>
|
|
185
|
+
{last ? null : <View style={{ flex: 1, width: 1, backgroundColor: colors.border }} />}
|
|
186
|
+
</View>
|
|
187
|
+
<View style={{ flex: 1, minWidth: 0, paddingBottom: last ? 0 : 12, gap: 1 }}>
|
|
188
|
+
<View style={{ flexDirection: "row", gap: 8 }}>
|
|
189
|
+
<Text style={{ flex: 1, color: colors.foreground, fontSize: 13, lineHeight: 18 }}>{step.label}</Text>
|
|
190
|
+
{step.minutes !== null ? (
|
|
191
|
+
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontVariant: ["tabular-nums"] }}>
|
|
192
|
+
{formatMinutes(step.minutes)}{current ? " so far" : ""}
|
|
193
|
+
</Text>
|
|
194
|
+
) : null}
|
|
195
|
+
</View>
|
|
196
|
+
<Text style={{ color: colors.foregroundMuted, fontSize: 11, lineHeight: 16 }}><When colors={colors} iso={step.at} now={now} /></Text>
|
|
197
|
+
{step.notes.map((note, index) => (
|
|
198
|
+
<Text key={index} style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17, paddingTop: 2 }}>{note}</Text>
|
|
199
|
+
))}
|
|
200
|
+
</View>
|
|
201
|
+
</View>
|
|
202
|
+
);
|
|
203
|
+
}
|
package/client/when.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Text } from "react-native";
|
|
4
|
+
import { formatAgo, formatClock } from "../shared/format";
|
|
5
|
+
|
|
6
|
+
type Colors = PluginWorkspacePanelProps["theme"]["colors"];
|
|
7
|
+
|
|
8
|
+
// "Sep 27, 8:14 PM (10 h 2 min ago)", inline inside other text.
|
|
9
|
+
export function When({ colors, iso, now }: { colors: Colors; iso: string; now: number }) {
|
|
10
|
+
return (
|
|
11
|
+
<Text>
|
|
12
|
+
{formatClock(iso)} <Text style={{ color: colors.foregroundMuted, opacity: 0.7 }}>({formatAgo(iso, now)})</Text>
|
|
13
|
+
</Text>
|
|
14
|
+
);
|
|
15
|
+
}
|