@stevecastaneda/paseo-progress 1.8.0 → 1.9.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
@@ -20,7 +20,7 @@ The dashboard shows:
20
20
 
21
21
  <img src="images/5-settings.png" alt="Progress settings in Paseo: Questions set to Work around, Language set to System, and the Agent Skills section with View skill" width="600">
22
22
 
23
- The panel lives in Explorer, beside the agent chat, so you can watch both. Open it from Command Center with **Open Progress**. The cog at the top right of the panel opens the plugin's Settings. After an update, the panel shows what's new once, including any versions you skipped. 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.
23
+ The panel lives in Explorer, beside the agent chat, so you can watch both. Open it from Command Center with **Open Progress**. The cog at the top right of the panel opens the plugin's Settings. After an update, the panel shows what's new once, including any versions you skipped. 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 an open question, its choices and their Copy buttons, and what is stuck, so you can answer without leaving the chat. With more than one question, the arrows beside **Question 1 of 3** step through them. After you copy an answer, the popover opens on the next question. **Open Progress** at the bottom opens the panel in Explorer.
24
24
 
25
25
  <img src="images/4-pill.png" alt="The pill above the message box, showing two questions waiting" width="600">
26
26
 
@@ -145,6 +145,11 @@ The progress file, `progress.jsonl`, holds one JSON event per line, each with a
145
145
 
146
146
  ## Changelog
147
147
 
148
+ ### 1.9.0
149
+
150
+ - [Improved] **The pill's popover shows one question at a time.** You used to scroll past every open question to reach the one you wanted. Now **Question 2 of 3** sits at the top with back and next arrows. Copy an answer and the popover opens on the next question the next time you press the pill.
151
+ - [Improved] **A long question shows there's more below.** When a question runs past the bottom of the popover, its last lines fade out until you scroll to the end. Before, it stopped at the edge and looked finished.
152
+
148
153
  ### 1.8.0
149
154
 
150
155
  - [New] **HTML reports open on a server with no screen.** An HTML report on a computer that can't open it, like a Linux server, used to fail and copy the file's path. In the desktop app it now opens across Paseo's main area, with its pictures, styles and scripts. Esc or Close takes you back. Where the computer can open it, it still opens in your browser.
@@ -67,6 +67,9 @@ export const messages: ProgressMessages = {
67
67
  openProgress: "Open Progress",
68
68
  needsYou: "Progress needs you",
69
69
  nothingNeedsYou: "Nothing needs you right now.",
70
+ questionPosition: (total: number) => ["Question ", ` of ${total}`],
71
+ previousQuestion: "Previous question",
72
+ nextQuestion: "Next question",
70
73
  overdueAside: " (no update past its estimate)",
71
74
  writeUp: "Write-up",
72
75
  timeline: "Timeline",
@@ -67,6 +67,9 @@ export const messages: ProgressMessages = {
67
67
  openProgress: "打开 Progress",
68
68
  needsYou: "Progress 需要你处理",
69
69
  nothingNeedsYou: "目前没有需要你处理的事项。",
70
+ questionPosition: (total: number) => ["第 ", ` 个问题,共 ${total} 个`],
71
+ previousQuestion: "上一个问题",
72
+ nextQuestion: "下一个问题",
70
73
  overdueAside: "(超过预计时长未更新)",
71
74
  writeUp: "总结",
72
75
  timeline: "时间线",
@@ -49,6 +49,9 @@ export interface ProgressMessages {
49
49
  openProgress: string;
50
50
  needsYou: string;
51
51
  nothingNeedsYou: string;
52
+ questionPosition: (total: number) => Around;
53
+ previousQuestion: string;
54
+ nextQuestion: string;
52
55
  overdueAside: string;
53
56
  writeUp: string;
54
57
  timeline: string;
@@ -11,14 +11,20 @@ import { StackedDialog } from "./dialog-stack";
11
11
  import { galleryFiles } from "./preview";
12
12
  import { QuestionDetail, QuestionState, useCopy } from "./questions";
13
13
  import { errorText } from "./errors";
14
- import { raised } from "./surfaces";
14
+ import { raised, withAlpha } from "./surfaces";
15
15
  import { useProgressLanguage } from "./language";
16
16
 
17
17
  type Colors = PluginButtonContentProps["theme"]["colors"];
18
+ type Question = Dashboard["questions"]["open"][number];
18
19
 
19
20
  // On desktop, Paseo caps the popover at 440px and scrolls all of it, padding included. The
20
21
  // list scrolls on its own below that cap so Open Progress stays in view.
21
22
  const LIST_MAX_HEIGHT = 340;
23
+ const HEADER_GAP = 12;
24
+ const TOUCH_TARGET = 44;
25
+
26
+ // Paseo unmounts the popover when it closes, so reopening comes back to where the user was.
27
+ const shownQuestions = new Map<string, string>();
22
28
 
23
29
  // What the pill opens: the open questions with their Copy buttons, and anything
24
30
  // stuck, so the user can answer without leaving the chat.
@@ -43,8 +49,8 @@ export function AttentionPopover({ theme, host, layout, workspaceId, close, open
43
49
  return (
44
50
  <View style={{ width: 360, maxWidth: "100%", gap: 8 }}>
45
51
  {dashboard ? (
46
- <List compact={layout.compact}>
47
- <AttentionList colors={colors} dashboard={dashboard} onSurface1={onSurface1} onCopy={async (question, letter) => {
52
+ <AttentionList colors={colors} dashboard={dashboard} workspaceId={workspaceId} compact={layout.compact} touch={touch} onSurface1={onSurface1}
53
+ onCopy={async (question, letter) => {
48
54
  await copy(question, letter);
49
55
  close();
50
56
  }} onOpenAttachment={(attachment, from) => {
@@ -52,7 +58,6 @@ export function AttentionPopover({ theme, host, layout, workspaceId, close, open
52
58
  if (!isPreviewable(attachment)) close();
53
59
  void opener.open(attachment);
54
60
  }} />
55
- </List>
56
61
  ) : (
57
62
  <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
58
63
  {query.error ? m.readProgressError(errorText(query.error, m.unknownError)) : m.loading}
@@ -63,7 +68,7 @@ export function AttentionPopover({ theme, host, layout, workspaceId, close, open
63
68
  <View style={{ paddingTop: touch ? 12 : 8, borderTopWidth: 1, borderTopColor: colors.border }}>
64
69
  {/* A full 44pt touch target on phones; desktop keeps the compact button. */}
65
70
  <PressScale accessibilityRole="button" onPress={() => { close(); openPanel(); }}
66
- style={({ pressed }) => ({ flexDirection: "row", justifyContent: "center", alignItems: "center", gap: touch ? 8 : 6, paddingVertical: 6, minHeight: touch ? 44 : undefined, borderRadius: touch ? 8 : 4, ...raised(colors),
71
+ style={({ pressed }) => ({ flexDirection: "row", justifyContent: "center", alignItems: "center", gap: touch ? 8 : 6, paddingVertical: 6, minHeight: touch ? TOUCH_TARGET : undefined, borderRadius: touch ? 8 : 4, ...raised(colors),
67
72
  backgroundColor: onSurface1 ? (pressed ? colors.surface1 : colors.surface2) : pressed ? colors.surface2 : colors.surface1 })}>
68
73
  {/* Beside the chat on desktop; on a phone it opens as its own tab, shown with the tab's icon. */}
69
74
  <Icon name={touch ? "ListChecks" : "PanelRight"} size={touch ? 16 : 12} color={colors.foreground} />
@@ -77,54 +82,140 @@ export function AttentionPopover({ theme, host, layout, workspaceId, close, open
77
82
  // On a phone the popover is a sheet that already scrolls, and sizes itself to
78
83
  // whichever scroll view reports last. Paseo 0.10.1 turns a ScrollView inside it
79
84
  // into one of those, so a short list would shrink the sheet to its title.
80
- function List({ compact, children }: { compact: boolean; children: React.ReactNode }) {
85
+ function List({ compact, maxHeight, surface, children }: { compact: boolean; maxHeight: number; surface: string; children: React.ReactNode }) {
86
+ const [moreBelow, setMoreBelow] = useState(false);
87
+ const size = useRef({ viewport: 0, content: 0, offset: 0 });
81
88
  if (compact) return <View style={{ gap: 12 }}>{children}</View>;
82
- return <ScrollView style={{ maxHeight: LIST_MAX_HEIGHT }} contentContainerStyle={{ gap: 12 }}>{children}</ScrollView>;
89
+ const measure = (next: Partial<typeof size.current>) => {
90
+ const s = Object.assign(size.current, next);
91
+ const below = s.offset + s.viewport < s.content - 1;
92
+ if (below !== moreBelow) setMoreBelow(below);
93
+ };
94
+ return (
95
+ <View style={{ position: "relative" }}>
96
+ <ScrollView style={{ maxHeight }} contentContainerStyle={{ gap: 12 }} scrollEventThrottle={16}
97
+ onLayout={(event) => measure({ viewport: event.nativeEvent.layout.height })}
98
+ onContentSizeChange={(_, height) => measure({ content: height })}
99
+ onScroll={(event) => measure({ offset: event.nativeEvent.contentOffset.y })}>
100
+ {children}
101
+ </ScrollView>
102
+ {moreBelow ? <BottomFade color={surface} /> : null}
103
+ </View>
104
+ );
105
+ }
106
+
107
+ const FADE_SLICES = 8;
108
+
109
+ // React Native Web 0.21 has no experimental_backgroundImage, so no single gradient style draws on both desktop and iPad.
110
+ function BottomFade({ color }: { color: string }) {
111
+ return (
112
+ <View pointerEvents="none" style={{ position: "absolute", left: 0, right: 0, bottom: 0 }}>
113
+ {Array.from({ length: FADE_SLICES }, (_, i) => (
114
+ <View key={i} style={{ height: 3, backgroundColor: withAlpha(color, (0.9 * i) / (FADE_SLICES - 1)) ?? undefined }} />
115
+ ))}
116
+ </View>
117
+ );
83
118
  }
84
119
 
85
- export function AttentionList({ colors, dashboard, onSurface1, onCopy, onOpenAttachment }: {
120
+ export function AttentionList({ colors, dashboard, workspaceId, compact, touch, onSurface1, onCopy, onOpenAttachment }: {
86
121
  colors: Colors;
87
122
  dashboard: Dashboard;
123
+ workspaceId: string;
124
+ compact: boolean;
125
+ touch: boolean;
88
126
  onSurface1: boolean;
89
- onCopy(question: Dashboard["questions"]["open"][number], letter: string): void;
127
+ onCopy(question: Question, letter: string): void;
90
128
  onOpenAttachment(attachment: Attachment, group: Attachment[]): void;
91
129
  }) {
92
130
  const { messages: m } = useProgressLanguage();
131
+ const [shownId, setShownId] = useState(() => shownQuestions.get(workspaceId));
93
132
  const now = Date.now();
133
+ const open = dashboard.questions.open;
134
+ const found = open.findIndex((question) => question.id === shownId);
135
+ const index = found === -1 ? 0 : found;
136
+ const question: Question | undefined = open[index];
94
137
  // The last question can be answered while the popover is open.
95
- if (!dashboard.questions.open.length && !dashboard.stuck.length) {
138
+ if (!question && !dashboard.stuck.length) {
96
139
  return <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>{m.nothingNeedsYou}</Text>;
97
140
  }
141
+ const show = (target: Question) => {
142
+ shownQuestions.set(workspaceId, target.id);
143
+ setShownId(target.id);
144
+ };
145
+ const advancePast = (copied: Question) => shownQuestions.set(workspaceId, (open[index + 1] ?? copied).id);
146
+ const step = { colors, touch, onSurface1, onPress: show };
147
+ const stepping = open.length > 1;
148
+ const position = m.questionPosition(open.length);
98
149
  return (
99
- <>
100
- {dashboard.questions.open.map((question) => (
101
- <View key={question.id} style={{ gap: 6 }}>
102
- <View style={{ flexDirection: "row", alignItems: "flex-start", gap: 8 }}>
103
- <Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 19, fontWeight: "600" }}>{question.id}</Text>
104
- <View style={{ flex: 1, minWidth: 0, gap: 2 }}>
105
- <Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 19 }}>{question.question}</Text>
106
- <QuestionState colors={colors} question={question} />
107
- </View>
150
+ <View style={{ gap: HEADER_GAP }}>
151
+ {stepping ? (
152
+ <View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
153
+ <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17, fontVariant: ["tabular-nums"] }}>
154
+ {position[0]}<Text style={{ color: colors.foreground, fontWeight: "600" }}>{index + 1}</Text>{position[1]}
155
+ </Text>
156
+ <View style={{ flexDirection: "row", gap: touch ? stepSlop * 2 : 4 }}>
157
+ <StepButton {...step} icon="ChevronLeft" label={m.previousQuestion} target={open[index - 1]} />
158
+ <StepButton {...step} icon="ChevronRight" label={m.nextQuestion} target={open[index + 1]} />
108
159
  </View>
109
- <QuestionDetail colors={colors} question={question} now={now} onSurface1={onSurface1} onCopy={(letter) => onCopy(question, letter)} onOpenAttachment={onOpenAttachment} />
110
160
  </View>
111
- ))}
112
- {dashboard.stuck.length ? (
113
- <View style={{ gap: 4 }}>
114
- <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
115
- <Icon name="OctagonAlert" size={12} color={colors.statusDanger} />
116
- <Text style={{ color: colors.statusDanger, fontSize: 12, lineHeight: 17, fontWeight: "600" }}>{m.stuck}</Text>
161
+ ) : null}
162
+ <List key={question?.id} compact={compact} surface={onSurface1 ? colors.surface1 : colors.surface0} maxHeight={stepping ? LIST_MAX_HEIGHT - stepSize(touch) - HEADER_GAP : LIST_MAX_HEIGHT}>
163
+ {question ? (
164
+ <View style={{ gap: 6 }}>
165
+ <View style={{ flexDirection: "row", alignItems: "flex-start", gap: 8 }}>
166
+ <Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 19, fontWeight: "600" }}>{question.id}</Text>
167
+ <View style={{ flex: 1, minWidth: 0, gap: 2 }}>
168
+ <Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 19 }}>{question.question}</Text>
169
+ <QuestionState colors={colors} question={question} />
170
+ </View>
171
+ </View>
172
+ <QuestionDetail colors={colors} question={question} now={now} onSurface1={onSurface1} onOpenAttachment={onOpenAttachment} onCopy={(letter) => {
173
+ advancePast(question);
174
+ onCopy(question, letter);
175
+ }} />
117
176
  </View>
118
- {dashboard.stuck.map((item) => (
119
- <Text key={item.key} style={{ color: colors.foreground, fontSize: 12, lineHeight: 17 }}>
120
- {item.title}
121
- <Text style={{ color: colors.foregroundMuted }}>
122
- {item.kind === "overdue" ? m.overdueAside : item.kind === "blocked" ? m.blockedAside(item.note) : ""}
177
+ ) : null}
178
+ {dashboard.stuck.length ? (
179
+ <View style={{ gap: 4 }}>
180
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
181
+ <Icon name="OctagonAlert" size={12} color={colors.statusDanger} />
182
+ <Text style={{ color: colors.statusDanger, fontSize: 12, lineHeight: 17, fontWeight: "600" }}>{m.stuck}</Text>
183
+ </View>
184
+ {dashboard.stuck.map((item) => (
185
+ <Text key={item.key} style={{ color: colors.foreground, fontSize: 12, lineHeight: 17 }}>
186
+ {item.title}
187
+ <Text style={{ color: colors.foregroundMuted }}>
188
+ {item.kind === "overdue" ? m.overdueAside : item.kind === "blocked" ? m.blockedAside(item.note) : ""}
189
+ </Text>
123
190
  </Text>
124
- </Text>
125
- ))}
126
- </View>
127
- ) : null}
128
- </>
191
+ ))}
192
+ </View>
193
+ ) : null}
194
+ </List>
195
+ </View>
196
+ );
197
+ }
198
+
199
+ const stepSize = (touch: boolean) => (touch ? 36 : 24);
200
+ const stepSlop = (TOUCH_TARGET - stepSize(true)) / 2;
201
+
202
+ function StepButton({ colors, touch, onSurface1, icon, label, target, onPress }: {
203
+ colors: Colors;
204
+ touch: boolean;
205
+ onSurface1: boolean;
206
+ icon: "ChevronLeft" | "ChevronRight";
207
+ label: string;
208
+ target: Question | undefined;
209
+ onPress(target: Question): void;
210
+ }) {
211
+ const button = onSurface1 ? { rest: colors.surface2, pressed: colors.surface1 } : { rest: colors.surface1, pressed: colors.surface2 };
212
+ const size = stepSize(touch);
213
+ return (
214
+ <PressScale accessibilityRole="button" accessibilityLabel={label} accessibilityState={{ disabled: !target }} disabled={!target}
215
+ onPress={target ? () => onPress(target) : undefined} hitSlop={touch ? stepSlop : undefined}
216
+ style={({ pressed }) => ({ width: size, height: size, alignItems: "center", justifyContent: "center", borderRadius: touch ? 8 : 4, opacity: target ? 1 : 0.35, ...raised(colors),
217
+ backgroundColor: pressed ? button.pressed : button.rest })}>
218
+ <Icon name={icon} size={touch ? 16 : 14} color={colors.foreground} />
219
+ </PressScale>
129
220
  );
130
221
  }
@@ -51,7 +51,10 @@ export function raised(colors: Colors): ViewStyle {
51
51
  // drifting second color. Paseo's own status badges use 12% in light themes and
52
52
  // 16% in dark ones, where the surface swallows more of it.
53
53
  export function tint(color: string, colors: Colors): string {
54
+ return withAlpha(color, isLight(colors) ? 0.12 : 0.16) ?? colors.surface2;
55
+ }
56
+
57
+ export function withAlpha(color: string, alpha: number): string | null {
54
58
  const rgb = channels(color);
55
- if (!rgb) return colors.surface2;
56
- return `rgba(${rgb.join(", ")}, ${isLight(colors) ? 0.12 : 0.16})`;
59
+ return rgb ? `rgba(${rgb.join(", ")}, ${alpha})` : null;
57
60
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stevecastaneda/paseo-progress",
3
- "version": "1.8.0",
3
+ "version": "1.9.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",