@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/panel.tsx CHANGED
@@ -12,7 +12,17 @@ import { raised } from "./surfaces";
12
12
  import { unbroken, When } from "./when";
13
13
  import React, { useEffect, useRef, useState } from "react";
14
14
  import { ActivityIndicator, Animated, Platform, ScrollView, Text, View } from "react-native";
15
- import { finishedToFold, isFinished, shortTitle, type Activity, type Dashboard, type FileIssue, type ProgressSegment, type StuckItem, type Ticket } from "../shared/dashboard";
15
+ import {
16
+ finishedToFold,
17
+ isFinished,
18
+ shortTitle,
19
+ type Activity,
20
+ type Dashboard,
21
+ type FileIssue,
22
+ type ProgressSegment,
23
+ type StuckItem,
24
+ type Ticket,
25
+ } from "../shared/dashboard";
16
26
  import type { TicketStatus } from "../shared/events";
17
27
  import { minutesSince } from "../shared/format";
18
28
  import { clientHeadline, formatClientMinutes, formatClientWorkDone } from "./i18n";
@@ -29,7 +39,6 @@ import { useProgressLanguage } from "./language";
29
39
 
30
40
  type Colors = PluginWorkspacePanelProps["theme"]["colors"];
31
41
 
32
-
33
42
  const mono = () => (Platform.OS === "ios" ? "Menlo" : "monospace");
34
43
 
35
44
  type PanelProps = PluginWorkspacePanelProps & { openSettings(): void };
@@ -53,24 +62,41 @@ function WorkspaceProgress({ theme, workspaceId, host, layout, navigation, openS
53
62
 
54
63
  return (
55
64
  <NarrowProvider value={narrow}>
56
- <ScrollView onLayout={(event) => setNarrow(event.nativeEvent.layout.width <= NARROW_MAX)}
57
- style={{ flex: 1, backgroundColor: colors.surface0 }} contentContainerStyle={{ paddingBottom: 16 }}>
58
- {query.isPending ? <ActivityIndicator color={colors.foregroundMuted} accessibilityLabel={m.loadingProgress} style={{ padding: 12 }} /> : null}
59
- {query.error ? (
60
- <Text accessibilityRole="alert" selectable style={{ color: colors.statusDanger, fontSize: 12, lineHeight: 18, padding: 12 }}>
61
- {m.readProgressError(errorText(query.error, m.unknownError))}
62
- </Text>
63
- ) : null}
64
- <WhatsNewBanner colors={colors} host={host} />
65
- <LauncherBanner colors={colors} host={host} />
66
- <SkillBanner colors={colors} host={host} />
67
- {/* Fades in like the dashboard's sections instead of popping in after the spinner. */}
68
- <Presence show={Boolean(result && !result.configured)}>{result && !result.configured ? <EmptyState colors={colors} hostId={host.id} file={result.file} openSettings={openSettings} /> : null}</Presence>
69
- {/* Keyed by run: a new run starts with fresh tabs, pages and dialogs. */}
70
- {result?.configured ? <DashboardView key={result.dashboard.run?.startedAt ?? "none"} colors={colors} openSettings={openSettings} dashboard={result.dashboard} agentRunning={agentRunning} compact={layout.compact}
71
- workspaceId={workspaceId} workspaceDirectory={result.root} file={result.file} navigation={navigation} /> : null}
72
- {result?.inRepo && directory ? <HistoryRow colors={colors} workspaceId={workspaceId} directory={directory} savedTo={result.savedTo} /> : null}
73
- </ScrollView>
65
+ <ScrollView
66
+ onLayout={(event) => setNarrow(event.nativeEvent.layout.width <= NARROW_MAX)}
67
+ style={{ flex: 1, backgroundColor: colors.surface0 }}
68
+ contentContainerStyle={{ paddingBottom: 16 }}
69
+ >
70
+ {query.isPending ? <ActivityIndicator color={colors.foregroundMuted} accessibilityLabel={m.loadingProgress} style={{ padding: 12 }} /> : null}
71
+ {query.error ? (
72
+ <Text accessibilityRole="alert" selectable style={{ color: colors.statusDanger, fontSize: 12, lineHeight: 18, padding: 12 }}>
73
+ {m.readProgressError(errorText(query.error, m.unknownError))}
74
+ </Text>
75
+ ) : null}
76
+ <WhatsNewBanner colors={colors} host={host} />
77
+ <LauncherBanner colors={colors} host={host} />
78
+ <SkillBanner colors={colors} host={host} />
79
+ {/* Fades in like the dashboard's sections instead of popping in after the spinner. */}
80
+ <Presence show={Boolean(result && !result.configured)}>
81
+ {result && !result.configured ? <EmptyState colors={colors} hostId={host.id} file={result.file} openSettings={openSettings} /> : null}
82
+ </Presence>
83
+ {/* Keyed by run: a new run starts with fresh tabs, pages and dialogs. */}
84
+ {result?.configured ? (
85
+ <DashboardView
86
+ key={result.dashboard.run?.startedAt ?? "none"}
87
+ colors={colors}
88
+ openSettings={openSettings}
89
+ dashboard={result.dashboard}
90
+ agentRunning={agentRunning}
91
+ compact={layout.compact}
92
+ workspaceId={workspaceId}
93
+ workspaceDirectory={result.root}
94
+ file={result.file}
95
+ navigation={navigation}
96
+ />
97
+ ) : null}
98
+ {result?.inRepo && directory ? <HistoryRow colors={colors} workspaceId={workspaceId} directory={directory} savedTo={result.savedTo} /> : null}
99
+ </ScrollView>
74
100
  </NarrowProvider>
75
101
  );
76
102
  }
@@ -86,7 +112,9 @@ function useMarkPanelOpened(workspaceId: string, directory: string | null, neede
86
112
  // Tell the pill only once the file exists, so its next poll agrees.
87
113
  mark({ workspaceId, workspaceDirectory: directory })
88
114
  .then(() => notePanelOpened(workspaceId))
89
- .catch(() => { sent.current = false; });
115
+ .catch(() => {
116
+ sent.current = false;
117
+ });
90
118
  }, [needed, directory, workspaceId, mark]);
91
119
  }
92
120
 
@@ -94,8 +122,14 @@ function useMarkPanelOpened(workspaceId: string, directory: string | null, neede
94
122
  function SettingsButton({ colors, onPress }: { colors: Colors; onPress(): void }) {
95
123
  const { messages: m } = useProgressLanguage();
96
124
  return (
97
- <PressScale accessibilityRole="button" accessibilityLabel={m.openSettings} hitSlop={6} outerStyle={{ marginVertical: -2 }} onPress={onPress}
98
- style={({ pressed, hovered }) => ({ padding: 4, borderRadius: 4, backgroundColor: pressed ? colors.border : hovered ? colors.surface1 : "transparent" })}>
125
+ <PressScale
126
+ accessibilityRole="button"
127
+ accessibilityLabel={m.openSettings}
128
+ hitSlop={6}
129
+ outerStyle={{ marginVertical: -2 }}
130
+ onPress={onPress}
131
+ style={({ pressed, hovered }) => ({ padding: 4, borderRadius: 4, backgroundColor: pressed ? colors.border : hovered ? colors.surface1 : "transparent" })}
132
+ >
99
133
  <Icon name="Settings" size={16} color={colors.foregroundMuted} />
100
134
  </PressScale>
101
135
  );
@@ -119,7 +153,9 @@ function EmptyState({ colors, hostId, file, openSettings }: { colors: Colors; ho
119
153
  ) : (
120
154
  <>
121
155
  <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
122
- {m.emptyIntro[0]}<Text style={{ color: colors.foreground }}>paseo-progress</Text>{m.emptyIntro[1]}
156
+ {m.emptyIntro[0]}
157
+ <Text style={{ color: colors.foreground }}>paseo-progress</Text>
158
+ {m.emptyIntro[1]}
123
159
  </Text>
124
160
  <View style={{ backgroundColor: colors.surface1, borderRadius: 6, padding: 10 }}>
125
161
  <Text selectable style={{ color: colors.foreground, fontFamily: mono(), fontSize: 12, lineHeight: 18 }}>
@@ -136,7 +172,11 @@ function EmptyState({ colors, hostId, file, openSettings }: { colors: Colors; ho
136
172
  ) : null}
137
173
  {/* So people know nothing lands in the repo. */}
138
174
  <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
139
- {m.savedOutside[0]}<Text selectable style={{ color: colors.foreground }}>{file}</Text>{m.savedOutside[1]}
175
+ {m.savedOutside[0]}
176
+ <Text selectable style={{ color: colors.foreground }}>
177
+ {file}
178
+ </Text>
179
+ {m.savedOutside[1]}
140
180
  </Text>
141
181
  </View>
142
182
  );
@@ -165,7 +205,17 @@ function useNow(intervalMs: number): number {
165
205
 
166
206
  const WIDE_MIN = 760;
167
207
 
168
- function DashboardView({ colors, dashboard, agentRunning, compact, workspaceId, workspaceDirectory, file, navigation, openSettings }: {
208
+ function DashboardView({
209
+ colors,
210
+ dashboard,
211
+ agentRunning,
212
+ compact,
213
+ workspaceId,
214
+ workspaceDirectory,
215
+ file,
216
+ navigation,
217
+ openSettings,
218
+ }: {
169
219
  openSettings(): void;
170
220
  colors: Colors;
171
221
  dashboard: Dashboard;
@@ -190,98 +240,189 @@ function DashboardView({ colors, dashboard, agentRunning, compact, workspaceId,
190
240
  return (
191
241
  <StaggerRoot>
192
242
  <Presence show order={0}>
193
- <View style={{ paddingHorizontal: 12, paddingTop: 12, paddingBottom: 10, gap: 4, borderBottomWidth: 1, borderBottomColor: colors.border }}>
194
- <View style={{ flexDirection: "row", alignItems: "flex-start", gap: 8 }}>
195
- <Text accessibilityRole="header" style={{ flex: 1, color: colors.foreground, fontSize: 15, lineHeight: 21, fontWeight: "600" }}>
196
- {dashboard.run?.title ?? "Progress"}
197
- </Text>
198
- <SettingsButton colors={colors} onPress={openSettings} />
199
- </View>
200
- {dashboard.run?.subtitle ? (
201
- <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>{dashboard.run.subtitle}</Text>
202
- ) : null}
203
- {headline.length ? (
204
- <Text accessibilityLiveRegion="polite" style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
205
- {headline.map((part, index) => (
206
- <Text key={part.text} style={part.tone === "danger" ? { color: colors.statusDanger } : undefined}>
207
- {index ? m.listSeparator : ""}{part.text}
208
- </Text>
209
- ))}
210
- </Text>
211
- ) : null}
212
- {/* One status slot: the icon morphs from spinner (or stalled pulse) to the check on finish. */}
213
- {dashboard.run?.finished || dashboard.ticker ? (
214
- <StatusLine icon={
215
- <IconSwap swapKey={dashboard.run?.finished ? "finished" : live ? "live" : "stale"} size={12}>
216
- {dashboard.run?.finished ? <Icon name="CircleCheck" size={12} color={colors.statusSuccess} />
217
- : live ? <Spinner color={colors.accent} /> : <StalledPulse color={colors.statusWarning} />}
218
- </IconSwap>
219
- }>
220
- {dashboard.run?.finished ? (
221
- <>
222
- <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
223
- <Text style={{ color: colors.statusSuccess, fontWeight: "600" }}>{m.finished}</Text> <When colors={colors} iso={dashboard.run.finished.at} now={now} />
243
+ <View style={{ paddingHorizontal: 12, paddingTop: 12, paddingBottom: 10, gap: 4, borderBottomWidth: 1, borderBottomColor: colors.border }}>
244
+ <View style={{ flexDirection: "row", alignItems: "flex-start", gap: 8 }}>
245
+ <Text accessibilityRole="header" style={{ flex: 1, color: colors.foreground, fontSize: 15, lineHeight: 21, fontWeight: "600" }}>
246
+ {dashboard.run?.title ?? "Progress"}
247
+ </Text>
248
+ <SettingsButton colors={colors} onPress={openSettings} />
249
+ </View>
250
+ {dashboard.run?.subtitle ? <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>{dashboard.run.subtitle}</Text> : null}
251
+ {headline.length ? (
252
+ <Text accessibilityLiveRegion="polite" style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
253
+ {headline.map((part, index) => (
254
+ <Text key={part.text} style={part.tone === "danger" ? { color: colors.statusDanger } : undefined}>
255
+ {index ? m.listSeparator : ""}
256
+ {part.text}
224
257
  </Text>
225
- <Text style={{ color: colors.foreground, fontSize: 12, lineHeight: 18 }}>{dashboard.run.finished.outcome}</Text>
226
- </>
227
- ) : (
228
- <Text accessibilityLiveRegion="polite" style={{ color: colors.foreground, fontSize: 12, lineHeight: 18 }}>{dashboard.ticker!.text}</Text>
229
- )}
230
- </StatusLine>
231
- ) : null}
232
- {/* A finished run takes no more updates, so its finish time already says this. */}
233
- {dashboard.updatedAt && !dashboard.run?.finished ? (
234
- <StatusLine icon={<Icon name="Clock" size={12} color={colors.foregroundMuted} />}>
235
- <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
236
- {m.lastUpdated}<When colors={colors} iso={dashboard.updatedAt} now={now} />
258
+ ))}
259
+ </Text>
260
+ ) : null}
261
+ {/* One status slot: the icon morphs from spinner (or stalled pulse) to the check on finish. */}
262
+ {dashboard.run?.finished || dashboard.ticker ? (
263
+ <StatusLine
264
+ icon={
265
+ <IconSwap swapKey={dashboard.run?.finished ? "finished" : live ? "live" : "stale"} size={12}>
266
+ {dashboard.run?.finished ? (
267
+ <Icon name="CircleCheck" size={12} color={colors.statusSuccess} />
268
+ ) : live ? (
269
+ <Spinner color={colors.accent} />
270
+ ) : (
271
+ <StalledPulse color={colors.statusWarning} />
272
+ )}
273
+ </IconSwap>
274
+ }
275
+ >
276
+ {dashboard.run?.finished ? (
277
+ <>
278
+ <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
279
+ <Text style={{ color: colors.statusSuccess, fontWeight: "600" }}>{m.finished}</Text>{" "}
280
+ <When colors={colors} iso={dashboard.run.finished.at} now={now} />
281
+ </Text>
282
+ <Text style={{ color: colors.foreground, fontSize: 12, lineHeight: 18 }}>{dashboard.run.finished.outcome}</Text>
283
+ </>
284
+ ) : (
285
+ <Text accessibilityLiveRegion="polite" style={{ color: colors.foreground, fontSize: 12, lineHeight: 18 }}>
286
+ {dashboard.ticker!.text}
287
+ </Text>
288
+ )}
289
+ </StatusLine>
290
+ ) : null}
291
+ {/* A finished run takes no more updates, so its finish time already says this. */}
292
+ {dashboard.updatedAt && !dashboard.run?.finished ? (
293
+ <StatusLine icon={<Icon name="Clock" size={12} color={colors.foregroundMuted} />}>
294
+ <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
295
+ {m.lastUpdated}
296
+ <When colors={colors} iso={dashboard.updatedAt} now={now} />
297
+ </Text>
298
+ </StatusLine>
299
+ ) : null}
300
+ </View>
301
+ </Presence>
302
+ <Presence show={Boolean(stale && dashboard.updatedAt)} order={1}>
303
+ {stale && dashboard.updatedAt ? (
304
+ <View
305
+ accessibilityRole="alert"
306
+ style={{
307
+ flexDirection: "row",
308
+ gap: 8,
309
+ margin: 12,
310
+ marginBottom: 0,
311
+ padding: 10,
312
+ borderWidth: 1,
313
+ borderColor: colors.statusWarning,
314
+ borderRadius: 6,
315
+ }}
316
+ >
317
+ {/* Optical: centers the 14px icon on the first 18px line. */}
318
+ <View style={{ paddingTop: 2 }}>
319
+ <Icon name="TriangleAlert" size={14} color={colors.statusWarning} />
320
+ </View>
321
+ <Text style={{ flex: 1, color: colors.statusWarning, fontSize: 12, lineHeight: 18 }}>
322
+ {m.staleWarning(unbroken(formatClientMinutes(locale, minutesSince(dashboard.updatedAt, now))))}
237
323
  </Text>
238
- </StatusLine>
324
+ </View>
239
325
  ) : null}
240
- </View>
241
326
  </Presence>
242
- <Presence show={Boolean(stale && dashboard.updatedAt)} order={1}>{stale && dashboard.updatedAt ? (
243
- <View accessibilityRole="alert" style={{ flexDirection: "row", gap: 8, margin: 12, marginBottom: 0, padding: 10, borderWidth: 1, borderColor: colors.statusWarning, borderRadius: 6 }}>
244
- {/* Optical: centers the 14px icon on the first 18px line. */}
245
- <View style={{ paddingTop: 2 }}><Icon name="TriangleAlert" size={14} color={colors.statusWarning} /></View>
246
- <Text style={{ flex: 1, color: colors.statusWarning, fontSize: 12, lineHeight: 18 }}>
247
- {m.staleWarning(unbroken(formatClientMinutes(locale, minutesSince(dashboard.updatedAt, now))))}
248
- </Text>
249
- </View>
250
- ) : null}</Presence>
251
- <Presence show={Boolean(dashboard.issues.length)} order={1}>{dashboard.issues.length ? <IssuesNotice colors={colors} issues={dashboard.issues} file={file} /> : null}</Presence>
252
- <Presence show={Boolean(dashboard.progress.totalMin > 0)} order={2}>{dashboard.progress.totalMin > 0 ? <ProgressBar colors={colors} progress={dashboard.progress} live={live} /> : null}</Presence>
253
- <View onLayout={(event) => setWide(event.nativeEvent.layout.width >= WIDE_MIN)}
254
- style={{ flexDirection: wide ? "row" : "column", alignItems: "flex-start" }}>
327
+ <Presence show={Boolean(dashboard.issues.length)} order={1}>
328
+ {dashboard.issues.length ? <IssuesNotice colors={colors} issues={dashboard.issues} file={file} /> : null}
329
+ </Presence>
330
+ <Presence show={Boolean(dashboard.progress.totalMin > 0)} order={2}>
331
+ {dashboard.progress.totalMin > 0 ? <ProgressBar colors={colors} progress={dashboard.progress} live={live} /> : null}
332
+ </Presence>
333
+ <View
334
+ onLayout={(event) => setWide(event.nativeEvent.layout.width >= WIDE_MIN)}
335
+ style={{ flexDirection: wide ? "row" : "column", alignItems: "flex-start" }}
336
+ >
255
337
  <View style={{ flex: wide ? 3 : undefined, alignSelf: "stretch", minWidth: 0 }}>
256
- <Presence show={dashboard.questions.open.length > 0} order={3}>{dashboard.questions.open.length ? (
257
- <QuestionsSection colors={colors} questions={dashboard.questions.open} answered={dashboard.questions.answered} now={now} context={attachmentContext} />
258
- ) : null}</Presence>
259
- <Presence show={Boolean(dashboard.stuck.length)} order={3}>{dashboard.stuck.length ? <StuckSection colors={colors} items={dashboard.stuck} now={now} /> : null}</Presence>
338
+ <Presence show={dashboard.questions.open.length > 0} order={3}>
339
+ {dashboard.questions.open.length ? (
340
+ <QuestionsSection
341
+ colors={colors}
342
+ questions={dashboard.questions.open}
343
+ answered={dashboard.questions.answered}
344
+ now={now}
345
+ context={attachmentContext}
346
+ />
347
+ ) : null}
348
+ </Presence>
349
+ <Presence show={Boolean(dashboard.stuck.length)} order={3}>
350
+ {dashboard.stuck.length ? <StuckSection colors={colors} items={dashboard.stuck} now={now} /> : null}
351
+ </Presence>
260
352
  <Presence show order={4}>
261
- <Card colors={colors} icon="ListChecks" title={m.itemHeading(dashboard.run?.itemLabel ?? "Ticket")}>
262
- {dashboard.tickets.length === 0 ? (
263
- <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, paddingHorizontal: 10, paddingVertical: 8, borderTopWidth: 1, borderTopColor: colors.border }}>
264
- {m.noItems(dashboard.run?.itemLabel ?? "Ticket")}
265
- </Text>
266
- ) : (
267
- <TicketList colors={colors} dashboard={dashboard} live={live} now={now} workspaceId={workspaceId} onOpen={setOpenTicket} />
268
- )}
269
- <TicketDialogs colors={colors} dashboard={dashboard} openId={openTicket} setOpenId={setOpenTicket} now={now} live={live} compact={compact} context={attachmentContext} />
270
- </Card>
353
+ <Card colors={colors} icon="ListChecks" title={m.itemHeading(dashboard.run?.itemLabel ?? "Ticket")}>
354
+ {dashboard.tickets.length === 0 ? (
355
+ <Text
356
+ style={{
357
+ color: colors.foregroundMuted,
358
+ fontSize: 12,
359
+ lineHeight: 18,
360
+ paddingHorizontal: 10,
361
+ paddingVertical: 8,
362
+ borderTopWidth: 1,
363
+ borderTopColor: colors.border,
364
+ }}
365
+ >
366
+ {m.noItems(dashboard.run?.itemLabel ?? "Ticket")}
367
+ </Text>
368
+ ) : (
369
+ <TicketList colors={colors} dashboard={dashboard} live={live} now={now} workspaceId={workspaceId} onOpen={setOpenTicket} />
370
+ )}
371
+ <TicketDialogs
372
+ colors={colors}
373
+ dashboard={dashboard}
374
+ openId={openTicket}
375
+ setOpenId={setOpenTicket}
376
+ now={now}
377
+ live={live}
378
+ compact={compact}
379
+ context={attachmentContext}
380
+ />
381
+ </Card>
271
382
  </Presence>
272
383
  </View>
273
384
  <View style={{ flex: wide ? 2 : undefined, alignSelf: "stretch", minWidth: 0 }}>
274
385
  <Presence show={hasHistory} order={5}>
275
386
  {hasHistory ? (
276
- <HistoryCard colors={colors} workspaceId={workspaceId} tabs={[
277
- { id: "activity", label: m.activity, icon: "Activity", count: dashboard.activity.length, empty: m.noActivity,
278
- content: <ActivityList colors={colors} activity={dashboard.activity} now={now} /> },
279
- { id: "answered", label: m.answered, icon: "CircleCheck", count: dashboard.questions.answered.length, empty: m.noAnswers,
280
- content: <AnsweredQuestionsList colors={colors} questions={dashboard.questions.answered} now={now} context={attachmentContext} /> },
281
- { id: "deliverables", label: m.deliverables, icon: "Package", count: dashboard.deliverables.length, empty: m.noDeliverables,
282
- content: <DeliverablesList colors={colors} deliverables={dashboard.deliverables} now={now} workspaceId={workspaceId}
283
- workspaceDirectory={workspaceDirectory} navigation={navigation} /> },
284
- ]} />
387
+ <HistoryCard
388
+ colors={colors}
389
+ workspaceId={workspaceId}
390
+ tabs={[
391
+ {
392
+ id: "activity",
393
+ label: m.activity,
394
+ icon: "Activity",
395
+ count: dashboard.activity.length,
396
+ empty: m.noActivity,
397
+ content: <ActivityList colors={colors} activity={dashboard.activity} now={now} />,
398
+ },
399
+ {
400
+ id: "answered",
401
+ label: m.answered,
402
+ icon: "CircleCheck",
403
+ count: dashboard.questions.answered.length,
404
+ empty: m.noAnswers,
405
+ content: <AnsweredQuestionsList colors={colors} questions={dashboard.questions.answered} now={now} context={attachmentContext} />,
406
+ },
407
+ {
408
+ id: "deliverables",
409
+ label: m.deliverables,
410
+ icon: "Package",
411
+ count: dashboard.deliverables.length,
412
+ empty: m.noDeliverables,
413
+ content: (
414
+ <DeliverablesList
415
+ colors={colors}
416
+ deliverables={dashboard.deliverables}
417
+ now={now}
418
+ workspaceId={workspaceId}
419
+ workspaceDirectory={workspaceDirectory}
420
+ navigation={navigation}
421
+ />
422
+ ),
423
+ },
424
+ ]}
425
+ />
285
426
  ) : null}
286
427
  </Presence>
287
428
  </View>
@@ -310,13 +451,23 @@ function HistoryCard({ colors, workspaceId, tabs }: { colors: Colors; workspaceI
310
451
  };
311
452
  const current = tabs.find((entry) => entry.id === tab) ?? tabs[0];
312
453
  return (
313
- <View onLayout={(event) => setShowIcons(event.nativeEvent.layout.width >= TAB_ICONS_MIN)}
314
- style={{ margin: 12, marginBottom: 0, ...raised(colors), borderRadius: 6, overflow: "hidden" }}>
315
- <TabStrip colors={colors} narrow={narrow} tabs={tabs.map((entry) => ({ ...entry, icon: showIcons ? entry.icon : null }))} selected={current.id} onSelect={choose}
316
- style={{ paddingHorizontal: narrow ? 8 : 10 }} />
454
+ <View
455
+ onLayout={(event) => setShowIcons(event.nativeEvent.layout.width >= TAB_ICONS_MIN)}
456
+ style={{ margin: 12, marginBottom: 0, ...raised(colors), borderRadius: 6, overflow: "hidden" }}
457
+ >
458
+ <TabStrip
459
+ colors={colors}
460
+ narrow={narrow}
461
+ tabs={tabs.map((entry) => ({ ...entry, icon: showIcons ? entry.icon : null }))}
462
+ selected={current.id}
463
+ onSelect={choose}
464
+ style={{ paddingHorizontal: narrow ? 8 : 10 }}
465
+ />
317
466
  {current.count === 0 ? (
318
467
  <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, paddingHorizontal: 10, paddingVertical: 8 }}>{current.empty}</Text>
319
- ) : current.content}
468
+ ) : (
469
+ current.content
470
+ )}
320
471
  </View>
321
472
  );
322
473
  }
@@ -329,7 +480,9 @@ function ActivityList({ colors, activity, now }: { colors: Colors; activity: Act
329
480
  {paged.shown.map((entry, index) => (
330
481
  <View key={entry.id} style={{ paddingHorizontal: 10, paddingVertical: 8, gap: 1, borderTopWidth: index ? 1 : 0, borderTopColor: colors.border }}>
331
482
  {/* Muted: a log to glance at, not something to act on. */}
332
- <Text selectable style={{ color: colors.foregroundMuted, fontSize: 13, lineHeight: 18 }}>{entry.text}</Text>
483
+ <Text selectable style={{ color: colors.foregroundMuted, fontSize: 13, lineHeight: 18 }}>
484
+ {entry.text}
485
+ </Text>
333
486
  <Text style={{ color: colors.foregroundMuted, fontSize: 11, lineHeight: 16, opacity: 0.8 }}>
334
487
  <When colors={colors} iso={entry.at} now={now} />
335
488
  </Text>
@@ -346,10 +499,15 @@ function IssuesNotice({ colors, issues, file }: { colors: Colors; issues: FileIs
346
499
  const shown = issues.slice(0, 5);
347
500
  return (
348
501
  <View style={{ flexDirection: "row", gap: 8, margin: 12, marginBottom: 0, padding: 8, borderRadius: 6, backgroundColor: colors.surface1 }}>
349
- <View style={{ paddingTop: 1.5 }}><Icon name="FileWarning" size={14} color={colors.statusWarning} /></View>
502
+ <View style={{ paddingTop: 1.5 }}>
503
+ <Icon name="FileWarning" size={14} color={colors.statusWarning} />
504
+ </View>
350
505
  <Text selectable style={{ flex: 1, color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
351
506
  {m.skippedLines(issues.length, file)}
352
- {m.issueList(shown.map((issue) => m.issueLine(issue.line, issue.reason)), issues.length - shown.length)}
507
+ {m.issueList(
508
+ shown.map((issue) => m.issueLine(issue.line, issue.reason)),
509
+ issues.length - shown.length,
510
+ )}
353
511
  </Text>
354
512
  </View>
355
513
  );
@@ -377,8 +535,11 @@ function ProgressBar({ colors, progress, live }: { colors: Colors; progress: Das
377
535
  {formatClientWorkDone(locale, progress.doneMin, progress.totalMin, progress.tookMin)}
378
536
  </Text>
379
537
  </View>
380
- <View accessibilityRole="progressbar" accessibilityValue={{ min: 0, max: 100, now: progress.percent }}
381
- style={{ flexDirection: "row", gap: 2, height: 8, borderRadius: 4, overflow: "hidden" }}>
538
+ <View
539
+ accessibilityRole="progressbar"
540
+ accessibilityValue={{ min: 0, max: 100, now: progress.percent }}
541
+ style={{ flexDirection: "row", gap: 2, height: 8, borderRadius: 4, overflow: "hidden" }}
542
+ >
382
543
  {progress.segments.map((segment) => (
383
544
  <Segment key={segment.id} colors={colors} segment={segment} live={live} />
384
545
  ))}
@@ -389,10 +550,14 @@ function ProgressBar({ colors, progress, live }: { colors: Colors; progress: Das
389
550
 
390
551
  function Segment({ colors, segment, live }: { colors: Colors; segment: ProgressSegment; live: boolean }) {
391
552
  const working = segment.status === "working";
392
- const color = segment.status === "done" ? colors.accent
393
- : working ? colors.accent
394
- : segment.status === "blocked" && !segment.waiting ? colors.statusDanger
395
- : colors.surface2;
553
+ const color =
554
+ segment.status === "done"
555
+ ? colors.accent
556
+ : working
557
+ ? colors.accent
558
+ : segment.status === "blocked" && !segment.waiting
559
+ ? colors.statusDanger
560
+ : colors.surface2;
396
561
  return (
397
562
  <View style={{ flex: segment.estimateMin, backgroundColor: color, opacity: working ? 0.45 : 1, overflow: "hidden" }}>
398
563
  {working && live ? <Shimmer /> : null}
@@ -405,17 +570,18 @@ function Segment({ colors, segment, live }: { colors: Colors; segment: ProgressS
405
570
  function Shimmer() {
406
571
  const value = useRef(new Animated.Value(0)).current;
407
572
  useEffect(() => {
408
- const loop = Animated.loop(Animated.sequence([
409
- Animated.timing(value, { toValue: 1, duration: 900, useNativeDriver: nativeDriver() }),
410
- Animated.timing(value, { toValue: 0, duration: 900, useNativeDriver: nativeDriver() }),
411
- ]));
573
+ const loop = Animated.loop(
574
+ Animated.sequence([
575
+ Animated.timing(value, { toValue: 1, duration: 900, useNativeDriver: nativeDriver() }),
576
+ Animated.timing(value, { toValue: 0, duration: 900, useNativeDriver: nativeDriver() }),
577
+ ]),
578
+ );
412
579
  loop.start();
413
580
  return () => loop.stop();
414
581
  }, [value]);
415
582
  return <Animated.View style={{ flex: 1, backgroundColor: "#ffffff", opacity: value.interpolate({ inputRange: [0, 1], outputRange: [0, 0.35] }) }} />;
416
583
  }
417
584
 
418
-
419
585
  const STUCK_ICON: Record<StuckItem["kind"], string> = { blocked: "Ban", overdue: "Hourglass", manual: "Flag" };
420
586
 
421
587
  function StuckSection({ colors, items, now }: { colors: Colors; items: StuckItem[]; now: number }) {
@@ -423,19 +589,30 @@ function StuckSection({ colors, items, now }: { colors: Colors; items: StuckItem
423
589
  const narrow = useNarrow();
424
590
  return (
425
591
  <View style={{ margin: 12, marginBottom: 0, borderWidth: 1, borderColor: colors.statusDanger, borderRadius: 6, overflow: "hidden" }}>
426
- <SectionTitle colors={colors} icon="OctagonAlert" title={m.stuck} color={colors.statusDanger} style={{ borderBottomWidth: 1, borderBottomColor: colors.border }} />
592
+ <SectionTitle
593
+ colors={colors}
594
+ icon="OctagonAlert"
595
+ title={m.stuck}
596
+ color={colors.statusDanger}
597
+ style={{ borderBottomWidth: 1, borderBottomColor: colors.border }}
598
+ />
427
599
  {items.map((item, index) => (
428
- <View key={item.key} style={{ flexDirection: "row", gap: 8, paddingHorizontal: 10, paddingVertical: 8, borderTopWidth: index ? 1 : 0, borderTopColor: colors.border }}>
600
+ <View
601
+ key={item.key}
602
+ style={{ flexDirection: "row", gap: 8, paddingHorizontal: 10, paddingVertical: 8, borderTopWidth: index ? 1 : 0, borderTopColor: colors.border }}
603
+ >
429
604
  {/* Optical: centers the 14px icon on the 18px title line. */}
430
- <View style={{ paddingTop: 2 }}><Icon name={STUCK_ICON[item.kind]} size={14} color={colors.statusDanger} /></View>
605
+ <View style={{ paddingTop: 2 }}>
606
+ <Icon name={STUCK_ICON[item.kind]} size={14} color={colors.statusDanger} />
607
+ </View>
431
608
  <View style={{ flex: 1, minWidth: 0, gap: 2 }}>
432
- <Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 18 }}>{item.title}</Text>
433
- <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
434
- {item.kind === "overdue" ? m.overdueUpdate(unbroken(formatClientMinutes(locale, item.estimateMin))) : null}
435
- {item.kind === "blocked" ? m.blockedSince(item.note, narrow) : null}
436
- {item.kind === "manual" ? `${item.ticketId ? `${item.reason}${m.sentenceBreak}` : ""}${m.flagged}` : null}
437
- <When colors={colors} iso={item.since} now={now} />
438
- </Text>
609
+ <Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 18 }}>{item.title}</Text>
610
+ <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
611
+ {item.kind === "overdue" ? m.overdueUpdate(unbroken(formatClientMinutes(locale, item.estimateMin))) : null}
612
+ {item.kind === "blocked" ? m.blockedSince(item.note, narrow) : null}
613
+ {item.kind === "manual" ? `${item.ticketId ? `${item.reason}${m.sentenceBreak}` : ""}${m.flagged}` : null}
614
+ <When colors={colors} iso={item.since} now={now} />
615
+ </Text>
439
616
  </View>
440
617
  </View>
441
618
  ))}
@@ -449,7 +626,21 @@ const finishedShown = new Map<string, boolean>();
449
626
 
450
627
  // Finished tickets fold into one row at the top, so the work still ahead is
451
628
  // near the top without scrolling (see finishedToFold).
452
- function TicketList({ colors, dashboard, live, now, workspaceId, onOpen }: { colors: Colors; dashboard: Dashboard; live: boolean; now: number; workspaceId: string; onOpen(id: string): void }) {
629
+ function TicketList({
630
+ colors,
631
+ dashboard,
632
+ live,
633
+ now,
634
+ workspaceId,
635
+ onOpen,
636
+ }: {
637
+ colors: Colors;
638
+ dashboard: Dashboard;
639
+ live: boolean;
640
+ now: number;
641
+ workspaceId: string;
642
+ onOpen(id: string): void;
643
+ }) {
453
644
  const [shown, setShown] = useState(() => finishedShown.get(workspaceId) ?? false);
454
645
  const toggle = () => {
455
646
  finishedShown.set(workspaceId, !shown);
@@ -457,8 +648,15 @@ function TicketList({ colors, dashboard, live, now, workspaceId, onOpen }: { col
457
648
  };
458
649
  const finished = finishedToFold(dashboard.tickets);
459
650
  const row = (ticket: Ticket) => (
460
- <TicketRow key={ticket.id} colors={colors} ticket={ticket} live={live} now={now} onOpen={() => onOpen(ticket.id)}
461
- deliverables={dashboard.deliverables.filter((deliverable) => deliverable.ticketId === ticket.id).length} />
651
+ <TicketRow
652
+ key={ticket.id}
653
+ colors={colors}
654
+ ticket={ticket}
655
+ live={live}
656
+ now={now}
657
+ onOpen={() => onOpen(ticket.id)}
658
+ deliverables={dashboard.deliverables.filter((deliverable) => deliverable.ticketId === ticket.id).length}
659
+ />
462
660
  );
463
661
  if (!finished.length) return <>{dashboard.tickets.map(row)}</>;
464
662
  return (
@@ -481,52 +679,129 @@ function FinishedRow({ colors, tickets, shown, onPress }: { colors: Colors; tick
481
679
  const chevron = shown ? "ChevronUp" : "ChevronDown";
482
680
  const narrow = useNarrow();
483
681
  return (
484
- <PressableRow colors={colors} accessibilityRole="button" aria-expanded={shown}
485
- accessibilityLabel={m.toggleFinished(shown, label)} onPress={onPress}
486
- style={{ flexDirection: "row", alignItems: "flex-start", gap: 10, paddingHorizontal: 10, paddingVertical: 8, borderTopWidth: 1, borderTopColor: colors.border }}>
682
+ <PressableRow
683
+ colors={colors}
684
+ accessibilityRole="button"
685
+ aria-expanded={shown}
686
+ accessibilityLabel={m.toggleFinished(shown, label)}
687
+ onPress={onPress}
688
+ style={{
689
+ flexDirection: "row",
690
+ alignItems: "flex-start",
691
+ gap: 10,
692
+ paddingHorizontal: 10,
693
+ paddingVertical: 8,
694
+ borderTopWidth: 1,
695
+ borderTopColor: colors.border,
696
+ }}
697
+ >
487
698
  <View style={{ width: 16, paddingTop: 2, alignItems: "center" }}>
488
699
  <Icon name={done ? "Check" : "CircleSlash"} size={14} color={done ? colors.statusSuccess : colors.foregroundMuted} />
489
700
  </View>
490
- <Text style={{ flex: 1, minWidth: 0, color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontWeight: "600", fontVariant: ["tabular-nums"] }}>{label}</Text>
701
+ <Text style={{ flex: 1, minWidth: 0, color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontWeight: "600", fontVariant: ["tabular-nums"] }}>
702
+ {label}
703
+ </Text>
491
704
  {/* Narrow ticket rows drop their chevron, so this total would sit off their estimate column. */}
492
- {narrow ? null : <>
493
- <View style={{ width: 12 }} />
494
- <Text style={{ minWidth: 44, flexShrink: 0, textAlign: "right", color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontVariant: ["tabular-nums"] }}>{estimateMin ? formatClientMinutes(locale, estimateMin) : ""}</Text>
495
- </>}
705
+ {narrow ? null : (
706
+ <>
707
+ <View style={{ width: 12 }} />
708
+ <Text
709
+ style={{
710
+ minWidth: 44,
711
+ flexShrink: 0,
712
+ textAlign: "right",
713
+ color: colors.foregroundMuted,
714
+ fontSize: 12,
715
+ lineHeight: 18,
716
+ fontVariant: ["tabular-nums"],
717
+ }}
718
+ >
719
+ {estimateMin ? formatClientMinutes(locale, estimateMin) : ""}
720
+ </Text>
721
+ </>
722
+ )}
496
723
  <View style={{ paddingTop: 2 }}>
497
- <IconSwap swapKey={chevron} size={14}><Icon name={chevron} size={14} color={colors.foregroundMuted} /></IconSwap>
724
+ <IconSwap swapKey={chevron} size={14}>
725
+ <Icon name={chevron} size={14} color={colors.foregroundMuted} />
726
+ </IconSwap>
498
727
  </View>
499
728
  </PressableRow>
500
729
  );
501
730
  }
502
731
 
503
732
  // Press for the ticket's story. Hover and press only tint the row: it's a list, pressed often.
504
- function TicketRow({ colors, ticket, live, now, onOpen, deliverables }: { colors: Colors; ticket: Ticket; live: boolean; now: number; onOpen(): void; deliverables: number }) {
733
+ function TicketRow({
734
+ colors,
735
+ ticket,
736
+ live,
737
+ now,
738
+ onOpen,
739
+ deliverables,
740
+ }: {
741
+ colors: Colors;
742
+ ticket: Ticket;
743
+ live: boolean;
744
+ now: number;
745
+ onOpen(): void;
746
+ deliverables: number;
747
+ }) {
505
748
  const { messages: m, locale } = useProgressLanguage();
506
749
  const muted = ticket.status === "skipped";
507
750
  const working = ticket.status === "working";
508
751
  // Narrow: tighter gaps and no chevron (the whole row still presses), so the title keeps its width.
509
752
  const narrow = useNarrow();
510
753
  return (
511
- <PressableRow colors={colors} onSurface1={working} transparentAtRest={!working}
512
- accessibilityRole="button" accessibilityLabel={m.details(ticket.id)} onPress={onOpen}
513
- style={{ flexDirection: "row", alignItems: "flex-start", gap: narrow ? 6 : 10, paddingHorizontal: 10, paddingVertical: 8, borderTopWidth: 1, borderTopColor: colors.border }}>
754
+ <PressableRow
755
+ colors={colors}
756
+ onSurface1={working}
757
+ transparentAtRest={!working}
758
+ accessibilityRole="button"
759
+ accessibilityLabel={m.details(ticket.id)}
760
+ onPress={onOpen}
761
+ style={{
762
+ flexDirection: "row",
763
+ alignItems: "flex-start",
764
+ gap: narrow ? 6 : 10,
765
+ paddingHorizontal: 10,
766
+ paddingVertical: 8,
767
+ borderTopWidth: 1,
768
+ borderTopColor: colors.border,
769
+ }}
770
+ >
514
771
  {/* The icon is the only status mark on the row; the ticket dialog spells it out in a badge. */}
515
- <View accessible accessibilityLabel={ticket.waitingFor ? m.waiting : m.status[ticket.status]} style={{ width: 16, paddingTop: 2, alignItems: "center", marginRight: narrow ? 2 : 0 }}>
516
- <IconSwap swapKey={ticket.waitingFor || ticket.status === "not_started" ? "waiting" : ticket.status === "working" ? `working-${live}` : ticket.status} size={14}><StatusIcon colors={colors} status={ticket.status} live={live} waiting={Boolean(ticket.waitingFor)} /></IconSwap>
772
+ <View
773
+ accessible
774
+ accessibilityLabel={ticket.waitingFor ? m.waiting : m.status[ticket.status]}
775
+ style={{ width: 16, paddingTop: 2, alignItems: "center", marginRight: narrow ? 2 : 0 }}
776
+ >
777
+ <IconSwap
778
+ swapKey={ticket.waitingFor || ticket.status === "not_started" ? "waiting" : ticket.status === "working" ? `working-${live}` : ticket.status}
779
+ size={14}
780
+ >
781
+ <StatusIcon colors={colors} status={ticket.status} live={live} waiting={Boolean(ticket.waitingFor)} />
782
+ </IconSwap>
517
783
  </View>
518
784
  <View style={{ flex: 1, minWidth: 0, gap: 2 }}>
519
- <Text style={{ color: muted ? colors.foregroundMuted : colors.foreground, fontSize: 13, lineHeight: 18, textDecorationLine: muted ? "line-through" : "none" }}>
785
+ <Text
786
+ style={{
787
+ color: muted ? colors.foregroundMuted : colors.foreground,
788
+ fontSize: 13,
789
+ lineHeight: 18,
790
+ textDecorationLine: muted ? "line-through" : "none",
791
+ }}
792
+ >
520
793
  {ticket.title}
521
794
  </Text>
522
795
  {ticket.status === "working" && ticket.stage && ticket.stageSince ? (
523
796
  <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
524
- {m.stageStarted(ticket.stage)}<When colors={colors} iso={ticket.stageSince} now={now} />
797
+ {m.stageStarted(ticket.stage)}
798
+ <When colors={colors} iso={ticket.stageSince} now={now} />
525
799
  </Text>
526
800
  ) : null}
527
801
  {ticket.status === "working" && ticket.workingSince ? (
528
802
  <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
529
- {narrow ? m.startedWorking : m.workingSince}<When colors={colors} iso={ticket.workingSince} now={now} />
803
+ {narrow ? m.startedWorking : m.workingSince}
804
+ <When colors={colors} iso={ticket.workingSince} now={now} />
530
805
  </Text>
531
806
  ) : null}
532
807
  {ticket.waitingFor ? (
@@ -538,8 +813,16 @@ function TicketRow({ colors, ticket, live, now, onOpen, deliverables }: { colors
538
813
  <View accessibilityLabel={deliverables ? m.hasDeliverables(deliverables) : undefined} style={{ width: 12, height: 18, justifyContent: "center" }}>
539
814
  {deliverables ? <Icon name="Package" size={12} color={colors.foregroundMuted} /> : null}
540
815
  </View>
541
- <Text style={{ minWidth: 44, flexShrink: 0, textAlign: "right", color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontVariant: ["tabular-nums"] }}>{formatClientMinutes(locale, ticket.estimateMin)}</Text>
542
- {narrow ? null : <View style={{ paddingTop: 2 }}><Icon name="ChevronRight" size={14} color={colors.foregroundMuted} /></View>}
816
+ <Text
817
+ style={{ minWidth: 44, flexShrink: 0, textAlign: "right", color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontVariant: ["tabular-nums"] }}
818
+ >
819
+ {formatClientMinutes(locale, ticket.estimateMin)}
820
+ </Text>
821
+ {narrow ? null : (
822
+ <View style={{ paddingTop: 2 }}>
823
+ <Icon name="ChevronRight" size={14} color={colors.foregroundMuted} />
824
+ </View>
825
+ )}
543
826
  </PressableRow>
544
827
  );
545
828
  }
@@ -560,4 +843,3 @@ function StatusIcon({ colors, status, live, waiting }: { colors: Colors; status:
560
843
  return <WaitingDot color={colors.foregroundMuted} size={14} />;
561
844
  }
562
845
  }
563
-