@stevecastaneda/paseo-progress 1.14.1 → 1.14.3

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.
Files changed (67) hide show
  1. package/README.md +28 -17
  2. package/client/attachments.tsx +122 -28
  3. package/client/blocks.tsx +33 -8
  4. package/client/chat-row-view.tsx +19 -6
  5. package/client/chat-row.ts +13 -2
  6. package/client/dashboard-query.ts +9 -4
  7. package/client/deliverables.tsx +82 -17
  8. package/client/dialog-stack.tsx +104 -42
  9. package/client/directory.ts +1 -3
  10. package/client/errors.ts +6 -1
  11. package/client/history.tsx +233 -79
  12. package/client/i18n.ts +10 -5
  13. package/client/launcher.tsx +57 -15
  14. package/client/locales/en.ts +28 -22
  15. package/client/locales/zh-CN.ts +18 -17
  16. package/client/messages.ts +1 -0
  17. package/client/motion.tsx +38 -14
  18. package/client/panel-opened.ts +3 -1
  19. package/client/panel.tsx +442 -160
  20. package/client/pdf.tsx +18 -4
  21. package/client/pieces.ts +6 -1
  22. package/client/pill-popover.tsx +108 -23
  23. package/client/pills.tsx +35 -12
  24. package/client/preview.tsx +265 -79
  25. package/client/questions.tsx +176 -55
  26. package/client/row.tsx +14 -3
  27. package/client/section-title.tsx +11 -2
  28. package/client/settings.tsx +76 -24
  29. package/client/show-more.tsx +23 -6
  30. package/client/spinner.tsx +11 -7
  31. package/client/status-badge.tsx +9 -7
  32. package/client/tabs.tsx +98 -20
  33. package/client/ticket-dialog.tsx +241 -73
  34. package/client/time-bar.tsx +5 -2
  35. package/client/video.tsx +15 -6
  36. package/client/whats-new.tsx +94 -36
  37. package/client/when.tsx +2 -1
  38. package/client/write-up.tsx +79 -12
  39. package/dist/server/cli.js +199 -101
  40. package/index.client.tsx +28 -13
  41. package/index.server.ts +19 -1
  42. package/package.json +15 -14
  43. package/server/cli.ts +72 -32
  44. package/server/dashboard.ts +9 -13
  45. package/server/launcher.ts +33 -17
  46. package/server/lock.ts +14 -7
  47. package/server/open.ts +32 -98
  48. package/server/progress-file.ts +6 -6
  49. package/server/question-mode.ts +12 -6
  50. package/server/skill.ts +22 -16
  51. package/server/store.ts +26 -41
  52. package/server/system.ts +113 -0
  53. package/server/tool.ts +89 -44
  54. package/server/whats-new.ts +25 -13
  55. package/shared/attachments.ts +40 -8
  56. package/shared/blocks.ts +9 -6
  57. package/shared/dashboard.ts +46 -29
  58. package/shared/events.ts +12 -10
  59. package/shared/markdown.ts +16 -8
  60. package/shared/question-modes.ts +8 -5
  61. package/shared/rpc.ts +9 -3
  62. package/shared/show.ts +20 -8
  63. package/shared/ticket-story.ts +1 -3
  64. package/skills/paseo-progress/SKILL.md +1 -0
  65. package/tsconfig.json +3 -10
  66. package/client/page-opener.ts +0 -9
  67. package/client/page-screen.tsx +0 -97
package/client/pdf.tsx CHANGED
@@ -29,15 +29,29 @@ function PdfFile({ file, colors }: { file: PluginFile; colors: Colors }) {
29
29
  const { messages: m } = useProgressLanguage();
30
30
  const url = useFileUrl(file);
31
31
  if (url.status === "loading") return <Loading />;
32
- if (url.status === "error") return <Note colors={colors} alert>{m.previewError(errorText(url.error, m.unknownError))}</Note>;
33
- return <View style={{ flex: 1, minHeight: 0 }}><DocumentFrame url={url.url} /></View>;
32
+ if (url.status === "error")
33
+ return (
34
+ <Note colors={colors} alert>
35
+ {m.previewError(errorText(url.error, m.unknownError))}
36
+ </Note>
37
+ );
38
+ return (
39
+ <View style={{ flex: 1, minHeight: 0 }}>
40
+ <DocumentFrame url={url.url} />
41
+ </View>
42
+ );
34
43
  }
35
44
 
36
45
  function Note({ colors, alert = false, children }: { colors: Colors; alert?: boolean; children: string }) {
37
46
  return (
38
47
  <View style={{ flex: 1, alignItems: "center", justifyContent: "center", padding: 24 }}>
39
- <Text accessibilityRole={alert ? "alert" : undefined} selectable
40
- style={{ maxWidth: 360, textAlign: "center", color: alert ? colors.statusDanger : colors.foregroundMuted, fontSize: 13, lineHeight: 19 }}>{children}</Text>
48
+ <Text
49
+ accessibilityRole={alert ? "alert" : undefined}
50
+ selectable
51
+ style={{ maxWidth: 360, textAlign: "center", color: alert ? colors.statusDanger : colors.foregroundMuted, fontSize: 13, lineHeight: 19 }}
52
+ >
53
+ {children}
54
+ </Text>
41
55
  </View>
42
56
  );
43
57
  }
package/client/pieces.ts CHANGED
@@ -10,7 +10,12 @@ export type FileBlob = { readonly size: number };
10
10
 
11
11
  export class FileChangedError extends Error {}
12
12
 
13
- export async function loadPieces({ bytes, mimeType, readPiece, onProgress }: {
13
+ export async function loadPieces({
14
+ bytes,
15
+ mimeType,
16
+ readPiece,
17
+ onProgress,
18
+ }: {
14
19
  bytes: number;
15
20
  mimeType: string;
16
21
  readPiece(offset: number): Promise<{ data: string; bytes: number }>;
@@ -44,31 +44,64 @@ export function AttentionPopover({ theme, host, layout, workspaceId, close, open
44
44
  // Links and other files leave Paseo, so the popover closes for those.
45
45
  const [previewing, setPreviewing] = useState<{ attachment: Attachment; group: Attachment[] } | null>(null);
46
46
  const group = useRef<Attachment[]>([]);
47
- const opener = useAttachmentOpener({ workspaceId, workspaceDirectory: root, onPreview: (attachment) => setPreviewing({ attachment, group: galleryFiles(group.current) }) });
47
+ const opener = useAttachmentOpener({
48
+ workspaceId,
49
+ workspaceDirectory: root,
50
+ onPreview: (attachment) => setPreviewing({ attachment, group: galleryFiles(group.current) }),
51
+ });
48
52
  return (
49
53
  <View style={{ width: 360, maxWidth: "100%", gap: 8 }}>
50
54
  {dashboard ? (
51
- <AttentionList colors={colors} dashboard={dashboard} workspaceId={workspaceId} compact={layout.compact} touch={touch} onSurface1={onSurface1}
55
+ <AttentionList
56
+ colors={colors}
57
+ dashboard={dashboard}
58
+ workspaceId={workspaceId}
59
+ root={root}
60
+ compact={layout.compact}
61
+ touch={touch}
62
+ onSurface1={onSurface1}
52
63
  onCopy={async (question, letter) => {
53
64
  await copy(question, letter);
54
65
  close();
55
- }} onOpenAttachment={(attachment, from) => {
66
+ }}
67
+ onOpenAttachment={(attachment, from) => {
56
68
  group.current = from;
57
69
  if (!isPreviewable(attachment)) close();
58
70
  void opener.open(attachment);
59
- }} />
71
+ }}
72
+ />
60
73
  ) : (
61
74
  <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
62
75
  {query.error ? m.readProgressError(errorText(query.error, m.unknownError)) : m.loading}
63
76
  </Text>
64
77
  )}
65
- <StackedDialog colors={colors} now={Date.now()} onClose={() => setPreviewing(null)} context={{ workspaceId, workspaceDirectory: root }}
66
- root={previewing ? { kind: "preview", ...previewing, fullScreen: false } : null} />
78
+ <StackedDialog
79
+ colors={colors}
80
+ now={Date.now()}
81
+ onClose={() => setPreviewing(null)}
82
+ context={{ workspaceId, workspaceDirectory: root }}
83
+ root={previewing ? { kind: "preview", ...previewing, fullScreen: false } : null}
84
+ />
67
85
  <View style={{ paddingTop: touch ? 12 : 8, borderTopWidth: 1, borderTopColor: colors.border }}>
68
86
  {/* A full 44pt touch target on phones; desktop keeps the compact button. */}
69
- <PressScale accessibilityRole="button" onPress={() => { close(); openPanel(); }}
70
- 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),
71
- backgroundColor: onSurface1 ? (pressed ? colors.surface1 : colors.surface2) : pressed ? colors.surface2 : colors.surface1 })}>
87
+ <PressScale
88
+ accessibilityRole="button"
89
+ onPress={() => {
90
+ close();
91
+ openPanel();
92
+ }}
93
+ style={({ pressed }) => ({
94
+ flexDirection: "row",
95
+ justifyContent: "center",
96
+ alignItems: "center",
97
+ gap: touch ? 8 : 6,
98
+ paddingVertical: 6,
99
+ minHeight: touch ? TOUCH_TARGET : undefined,
100
+ borderRadius: touch ? 8 : 4,
101
+ ...raised(colors),
102
+ backgroundColor: onSurface1 ? (pressed ? colors.surface1 : colors.surface2) : pressed ? colors.surface2 : colors.surface1,
103
+ })}
104
+ >
72
105
  {/* Beside the chat on desktop; on a phone it opens as its own tab, shown with the tab's icon. */}
73
106
  <Icon name={touch ? "ListChecks" : "PanelRight"} size={touch ? 16 : 12} color={colors.foreground} />
74
107
  <Text style={{ color: colors.foreground, fontSize: touch ? 15 : 12, lineHeight: touch ? 20 : 16, fontWeight: "600" }}>{m.openProgress}</Text>
@@ -92,10 +125,14 @@ function List({ compact, maxHeight, surface, children }: { compact: boolean; max
92
125
  };
93
126
  return (
94
127
  <View style={{ position: "relative" }}>
95
- <ScrollView style={{ maxHeight }} contentContainerStyle={{ gap: 12 }} scrollEventThrottle={16}
128
+ <ScrollView
129
+ style={{ maxHeight }}
130
+ contentContainerStyle={{ gap: 12 }}
131
+ scrollEventThrottle={16}
96
132
  onLayout={(event) => measure({ viewport: event.nativeEvent.layout.height })}
97
133
  onContentSizeChange={(_, height) => measure({ content: height })}
98
- onScroll={(event) => measure({ offset: event.nativeEvent.contentOffset.y })}>
134
+ onScroll={(event) => measure({ offset: event.nativeEvent.contentOffset.y })}
135
+ >
99
136
  {children}
100
137
  </ScrollView>
101
138
  {moreBelow ? <BottomFade color={surface} /> : null}
@@ -116,10 +153,22 @@ function BottomFade({ color }: { color: string }) {
116
153
  );
117
154
  }
118
155
 
119
- export function AttentionList({ colors, dashboard, workspaceId, compact, touch, onSurface1, onCopy, onOpenAttachment }: {
156
+ export function AttentionList({
157
+ colors,
158
+ dashboard,
159
+ workspaceId,
160
+ root,
161
+ compact,
162
+ touch,
163
+ onSurface1,
164
+ onCopy,
165
+ onOpenAttachment,
166
+ }: {
120
167
  colors: Colors;
121
168
  dashboard: Dashboard;
122
169
  workspaceId: string;
170
+ // The worktree root the attachments' paths are relative to.
171
+ root: string;
123
172
  compact: boolean;
124
173
  touch: boolean;
125
174
  onSurface1: boolean;
@@ -150,7 +199,9 @@ export function AttentionList({ colors, dashboard, workspaceId, compact, touch,
150
199
  {stepping ? (
151
200
  <View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
152
201
  <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17, fontVariant: ["tabular-nums"] }}>
153
- {position[0]}<Text style={{ color: colors.foreground, fontWeight: "600" }}>{index + 1}</Text>{position[1]}
202
+ {position[0]}
203
+ <Text style={{ color: colors.foreground, fontWeight: "600" }}>{index + 1}</Text>
204
+ {position[1]}
154
205
  </Text>
155
206
  <View style={{ flexDirection: "row", gap: touch ? stepSlop * 2 : 4 }}>
156
207
  <StepButton {...step} icon="ChevronLeft" label={m.previousQuestion} target={open[index - 1]} />
@@ -158,7 +209,12 @@ export function AttentionList({ colors, dashboard, workspaceId, compact, touch,
158
209
  </View>
159
210
  </View>
160
211
  ) : null}
161
- <List key={question?.id} compact={compact} surface={onSurface1 ? colors.surface1 : colors.surface0} maxHeight={stepping ? LIST_MAX_HEIGHT - stepSize(touch) - HEADER_GAP : LIST_MAX_HEIGHT}>
212
+ <List
213
+ key={question?.id}
214
+ compact={compact}
215
+ surface={onSurface1 ? colors.surface1 : colors.surface0}
216
+ maxHeight={stepping ? LIST_MAX_HEIGHT - stepSize(touch) - HEADER_GAP : LIST_MAX_HEIGHT}
217
+ >
162
218
  {question ? (
163
219
  <View style={{ gap: 6 }}>
164
220
  <View style={{ flexDirection: "row", alignItems: "flex-start", gap: 8 }}>
@@ -168,10 +224,18 @@ export function AttentionList({ colors, dashboard, workspaceId, compact, touch,
168
224
  <QuestionState colors={colors} question={question} />
169
225
  </View>
170
226
  </View>
171
- <QuestionDetail colors={colors} question={question} now={now} onSurface1={onSurface1} onOpenAttachment={onOpenAttachment} onCopy={(letter) => {
172
- advancePast(question);
173
- onCopy(question, letter);
174
- }} />
227
+ <QuestionDetail
228
+ colors={colors}
229
+ question={question}
230
+ now={now}
231
+ root={root}
232
+ onSurface1={onSurface1}
233
+ onOpenAttachment={onOpenAttachment}
234
+ onCopy={(letter) => {
235
+ advancePast(question);
236
+ onCopy(question, letter);
237
+ }}
238
+ />
175
239
  </View>
176
240
  ) : null}
177
241
  {dashboard.stuck.length ? (
@@ -198,7 +262,15 @@ export function AttentionList({ colors, dashboard, workspaceId, compact, touch,
198
262
  const stepSize = (touch: boolean) => (touch ? 36 : 24);
199
263
  const stepSlop = (TOUCH_TARGET - stepSize(true)) / 2;
200
264
 
201
- function StepButton({ colors, touch, onSurface1, icon, label, target, onPress }: {
265
+ function StepButton({
266
+ colors,
267
+ touch,
268
+ onSurface1,
269
+ icon,
270
+ label,
271
+ target,
272
+ onPress,
273
+ }: {
202
274
  colors: Colors;
203
275
  touch: boolean;
204
276
  onSurface1: boolean;
@@ -210,10 +282,23 @@ function StepButton({ colors, touch, onSurface1, icon, label, target, onPress }:
210
282
  const button = onSurface1 ? { rest: colors.surface2, pressed: colors.surface1 } : { rest: colors.surface1, pressed: colors.surface2 };
211
283
  const size = stepSize(touch);
212
284
  return (
213
- <PressScale accessibilityRole="button" accessibilityLabel={label} disabled={!target}
214
- onPress={target ? () => onPress(target) : undefined} hitSlop={touch ? stepSlop : undefined}
215
- style={({ pressed }) => ({ width: size, height: size, alignItems: "center", justifyContent: "center", borderRadius: touch ? 8 : 4, opacity: target ? 1 : 0.35, ...raised(colors),
216
- backgroundColor: pressed ? button.pressed : button.rest })}>
285
+ <PressScale
286
+ accessibilityRole="button"
287
+ accessibilityLabel={label}
288
+ disabled={!target}
289
+ onPress={target ? () => onPress(target) : undefined}
290
+ hitSlop={touch ? stepSlop : undefined}
291
+ style={({ pressed }) => ({
292
+ width: size,
293
+ height: size,
294
+ alignItems: "center",
295
+ justifyContent: "center",
296
+ borderRadius: touch ? 8 : 4,
297
+ opacity: target ? 1 : 0.35,
298
+ ...raised(colors),
299
+ backgroundColor: pressed ? button.pressed : button.rest,
300
+ })}
301
+ >
217
302
  <Icon name={icon} size={touch ? 16 : 14} color={colors.foreground} />
218
303
  </PressScale>
219
304
  );
package/client/pills.tsx CHANGED
@@ -53,8 +53,14 @@ export function contributePills(client: PluginClientContext) {
53
53
  let stopped = false;
54
54
  let timer: ReturnType<typeof setTimeout> | undefined;
55
55
 
56
+ // The rule doesn't count <LocalizedPillIcon />, which closes over this scope, as a use of it.
57
+ // oxlint-disable-next-line unicorn/consistent-function-scoping
56
58
  function PillIcon(props: PluginButtonIconProps) {
57
- return <ProgressLanguageProvider><LocalizedPillIcon {...props} /></ProgressLanguageProvider>;
59
+ return (
60
+ <ProgressLanguageProvider>
61
+ <LocalizedPillIcon {...props} />
62
+ </ProgressLanguageProvider>
63
+ );
58
64
  }
59
65
 
60
66
  function LocalizedPillIcon(props: PluginButtonIconProps) {
@@ -65,15 +71,25 @@ export function contributePills(client: PluginClientContext) {
65
71
  publish();
66
72
  }, [language.locale]);
67
73
  const icon = useSyncExternalStore(
68
- (listener) => { listeners.add(listener); return () => { listeners.delete(listener); }; },
74
+ (listener) => {
75
+ listeners.add(listener);
76
+ return () => {
77
+ listeners.delete(listener);
78
+ };
79
+ },
69
80
  () => pillOf(props.workspaceId)?.icon ?? "open",
70
81
  );
71
82
  return (
72
83
  <IconSwap swapKey={icon} size={props.size}>
73
- {icon === "stuck" ? <Icon name="OctagonAlert" size={props.size} color={props.theme.colors.statusDanger} />
74
- : icon === "questions" ? <Icon name="MessageCircleQuestion" size={props.size} color={props.color} />
75
- : icon === "started" ? <Spinner color={props.color} size={props.size} />
76
- : <Icon name="ListChecks" size={props.size} color={props.color} />}
84
+ {icon === "stuck" ? (
85
+ <Icon name="OctagonAlert" size={props.size} color={props.theme.colors.statusDanger} />
86
+ ) : icon === "questions" ? (
87
+ <Icon name="MessageCircleQuestion" size={props.size} color={props.color} />
88
+ ) : icon === "started" ? (
89
+ <Spinner color={props.color} size={props.size} />
90
+ ) : (
91
+ <Icon name="ListChecks" size={props.size} color={props.color} />
92
+ )}
77
93
  </IconSwap>
78
94
  );
79
95
  }
@@ -83,12 +99,17 @@ export function contributePills(client: PluginClientContext) {
83
99
  client.openPanel(panel.id, { workspaceId, location: panel.location });
84
100
  };
85
101
 
86
- const buttonFor = (workspaceId: string, mode: Pill["mode"], label: string) => mode === "attention"
87
- ? { title: messagesFor(locale).needsYou, icon: PillIcon, label, behavior: { kind: "popover" as const, Content: PillContent } }
88
- : { title: messagesFor(locale).openProgress, icon: PillIcon, label, behavior: { kind: "action" as const, onPress: () => openPanel(workspaceId) } };
102
+ const buttonFor = (workspaceId: string, mode: Pill["mode"], label: string) =>
103
+ mode === "attention"
104
+ ? { title: messagesFor(locale).needsYou, icon: PillIcon, label, behavior: { kind: "popover" as const, Content: PillContent } }
105
+ : { title: messagesFor(locale).openProgress, icon: PillIcon, label, behavior: { kind: "action" as const, onPress: () => openPanel(workspaceId) } };
89
106
 
90
107
  function PillContent(props: PluginButtonContentProps) {
91
- return <ProgressLanguageProvider><AttentionPopover {...props} openPanel={() => openPanel(props.workspaceId)} /></ProgressLanguageProvider>;
108
+ return (
109
+ <ProgressLanguageProvider>
110
+ <AttentionPopover {...props} openPanel={() => openPanel(props.workspaceId)} />
111
+ </ProgressLanguageProvider>
112
+ );
92
113
  }
93
114
 
94
115
  const publish = () => {
@@ -149,7 +170,9 @@ export function contributePills(client: PluginClientContext) {
149
170
  if (isIdle(result)) idleWait.set(workspaceId, IDLE_POLL_EVERY - 1);
150
171
  else idleWait.delete(workspaceId);
151
172
  publish();
152
- } catch { /* Try again on the next poll. */ } finally {
173
+ } catch {
174
+ /* Try again on the next poll. */
175
+ } finally {
153
176
  pending.delete(workspaceId);
154
177
  }
155
178
  };
@@ -182,7 +205,7 @@ export function contributePills(client: PluginClientContext) {
182
205
  select: (message) => (message.type === "workspace_update" ? message.payload : undefined),
183
206
  snapshot: (entries) => {
184
207
  const next = new Map((entries as Workspace[]).map((workspace) => [workspace.id, directoryOf(workspace)]));
185
- for (const id of [...workspaces.keys()]) if (!next.has(id)) forget(id);
208
+ for (const id of Array.from(workspaces.keys())) if (!next.has(id)) forget(id);
186
209
  const hasAgent = new Set(agents.map((agent) => agent.workspaceId));
187
210
  for (const [id, directory] of next) {
188
211
  if (workspaces.get(id) !== directory) {