@stevecastaneda/paseo-progress 1.14.2 → 1.14.4

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 +173 -52
  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 +7 -0
  65. package/tsconfig.json +3 -10
  66. package/client/page-opener.ts +0 -9
  67. package/client/page-screen.tsx +0 -97
@@ -44,7 +44,13 @@ export interface AttachmentContext {
44
44
  navigation?: PluginWorkspacePanelProps["navigation"];
45
45
  }
46
46
 
47
- export function QuestionsSection({ colors, questions, answered, now, context }: {
47
+ export function QuestionsSection({
48
+ colors,
49
+ questions,
50
+ answered,
51
+ now,
52
+ context,
53
+ }: {
48
54
  colors: Colors;
49
55
  questions: Question[];
50
56
  // So a question answered while its dialog is open shows the answer instead of vanishing.
@@ -57,31 +63,53 @@ export function QuestionsSection({ colors, questions, answered, now, context }:
57
63
  const copy = useCopy();
58
64
  return (
59
65
  <View style={{ margin: 12, marginBottom: 0, borderWidth: 1, borderColor: colors.accent, borderRadius: 6, overflow: "hidden" }}>
60
- <View style={{ flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 8, paddingHorizontal: 10, paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: colors.border }}>
66
+ <View
67
+ style={{
68
+ flexDirection: "row",
69
+ alignItems: "center",
70
+ flexWrap: "wrap",
71
+ gap: 8,
72
+ paddingHorizontal: 10,
73
+ paddingVertical: 8,
74
+ borderBottomWidth: 1,
75
+ borderBottomColor: colors.border,
76
+ }}
77
+ >
61
78
  <Icon name="MessageCircleQuestion" size={14} color={colors.accent} />
62
- <Text accessibilityRole="header" style={{ color: colors.foreground, fontSize: 13, lineHeight: 18, fontWeight: "600" }}>{m.questions}</Text>
63
- <View style={{ borderWidth: 1, borderColor: colors.accent, backgroundColor: colors.accent, borderRadius: 999, paddingHorizontal: 8, paddingVertical: 1 }}>
79
+ <Text accessibilityRole="header" style={{ color: colors.foreground, fontSize: 13, lineHeight: 18, fontWeight: "600" }}>
80
+ {m.questions}
81
+ </Text>
82
+ <View
83
+ style={{ borderWidth: 1, borderColor: colors.accent, backgroundColor: colors.accent, borderRadius: 999, paddingHorizontal: 8, paddingVertical: 1 }}
84
+ >
64
85
  <Text style={{ color: colors.accentForeground, fontSize: 11, lineHeight: 16 }}>{m.waitingCount(questions.length)}</Text>
65
86
  </View>
66
87
  </View>
67
88
  {questions.map((question, index) => (
68
- <QuestionRow key={question.id} colors={colors} question={question} first={index === 0}
69
- onOpen={() => setOpenId(question.id)} onCopy={() => void copy(question)} />
89
+ <QuestionRow
90
+ key={question.id}
91
+ colors={colors}
92
+ question={question}
93
+ first={index === 0}
94
+ onOpen={() => setOpenId(question.id)}
95
+ onCopy={() => void copy(question)}
96
+ />
70
97
  ))}
71
- <StackedDialog colors={colors} root={openId ? { kind: "question", id: openId } : null} onClose={() => setOpenId(null)}
72
- questions={[...questions, ...answered]} now={now} context={context} />
98
+ <StackedDialog
99
+ colors={colors}
100
+ root={openId ? { kind: "question", id: openId } : null}
101
+ onClose={() => setOpenId(null)}
102
+ questions={[...questions, ...answered]}
103
+ now={now}
104
+ context={context}
105
+ />
73
106
  </View>
74
107
  );
75
108
  }
76
109
 
77
110
  // Settled questions, one line each (title and the choice). Press a row for the rest.
78
111
  // Rows only: the panel's tabbed card supplies the frame.
79
- export function AnsweredQuestionsList({ colors, questions, now, context }: {
80
- colors: Colors;
81
- questions: Question[];
82
- now: number;
83
- context: AttachmentContext;
84
- }) {
112
+ export function AnsweredQuestionsList({ colors, questions, now, context }: { colors: Colors; questions: Question[]; now: number; context: AttachmentContext }) {
85
113
  const { messages: m } = useProgressLanguage();
86
114
  const [openId, setOpenId] = useState<string | null>(null);
87
115
  const paged = usePaged(questions);
@@ -92,12 +120,31 @@ export function AnsweredQuestionsList({ colors, questions, now, context }: {
92
120
  const chosen = question.options.find((option) => option.letter === answer.choice);
93
121
  return (
94
122
  <View key={question.id} style={{ borderTopWidth: index ? 1 : 0, borderTopColor: colors.border }}>
95
- <PressableRow colors={colors} accessibilityRole="button" accessibilityLabel={m.details(question.id)} onPress={() => setOpenId(question.id)}
96
- style={{ flexDirection: "row", alignItems: "center", gap: 8, paddingHorizontal: 10, paddingVertical: 8 }}>
123
+ <PressableRow
124
+ colors={colors}
125
+ accessibilityRole="button"
126
+ accessibilityLabel={m.details(question.id)}
127
+ onPress={() => setOpenId(question.id)}
128
+ style={{ flexDirection: "row", alignItems: "center", gap: 8, paddingHorizontal: 10, paddingVertical: 8 }}
129
+ >
97
130
  {/* Room for two-digit ids so titles line up; longer ones widen instead of wrapping. */}
98
- <Text numberOfLines={1} style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17, fontWeight: "600", fontVariant: ["tabular-nums"], minWidth: 30, flexShrink: 0 }}>{question.id}</Text>
131
+ <Text
132
+ numberOfLines={1}
133
+ style={{
134
+ color: colors.foregroundMuted,
135
+ fontSize: 12,
136
+ lineHeight: 17,
137
+ fontWeight: "600",
138
+ fontVariant: ["tabular-nums"],
139
+ minWidth: 30,
140
+ flexShrink: 0,
141
+ }}
142
+ >
143
+ {question.id}
144
+ </Text>
99
145
  <Text numberOfLines={1} style={{ flex: 1, minWidth: 0, color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
100
- {question.title}{" "}
146
+ {question.title}
147
+ {" "}
101
148
  <Text style={{ color: colors.statusSuccess, fontWeight: "600" }}>{answer.choice}</Text>
102
149
  {chosen ? <Text style={{ color: colors.foreground }}> {chosen.label}</Text> : null}
103
150
  </Text>
@@ -107,8 +154,14 @@ export function AnsweredQuestionsList({ colors, questions, now, context }: {
107
154
  );
108
155
  })}
109
156
  <ShowMoreRow colors={colors} total={questions.length} paged={paged} />
110
- <StackedDialog colors={colors} root={openId ? { kind: "question", id: openId } : null} onClose={() => setOpenId(null)}
111
- questions={questions} now={now} context={context} />
157
+ <StackedDialog
158
+ colors={colors}
159
+ root={openId ? { kind: "question", id: openId } : null}
160
+ onClose={() => setOpenId(null)}
161
+ questions={questions}
162
+ now={now}
163
+ context={context}
164
+ />
112
165
  </View>
113
166
  );
114
167
  }
@@ -123,28 +176,26 @@ function AnswerSummary({ colors, question, now }: { colors: Colors; question: Qu
123
176
  <Text style={{ color: colors.statusSuccess, fontWeight: "600" }}>{answer.choice}</Text>
124
177
  {chosen ? ` ${chosen.label}.` : ""}
125
178
  {answer.words ? <Text style={{ color: colors.foregroundMuted }}> “{answer.words}”</Text> : null}
126
- <Text style={{ color: colors.foregroundMuted }}>
127
- {answer.changedCourse ? m.changedCourse : ""}
128
- </Text>
179
+ <Text style={{ color: colors.foregroundMuted }}>{answer.changedCourse ? m.changedCourse : ""}</Text>
129
180
  </Text>
130
181
  <Text style={{ color: colors.foregroundMuted, fontSize: 11, lineHeight: 16 }}>
131
- {m.answerRecorded}<When colors={colors} iso={answer.at} now={now} />{question.raisedBy ? m.raisedBy(question.raisedBy) : ""}
182
+ {m.answerRecorded}
183
+ <When colors={colors} iso={answer.at} now={now} />
184
+ {question.raisedBy ? m.raisedBy(question.raisedBy) : ""}
132
185
  </Text>
133
186
  </View>
134
187
  );
135
188
  }
136
189
 
137
- function QuestionRow({ colors, question, first, onOpen, onCopy }: {
138
- colors: Colors;
139
- question: Question;
140
- first: boolean;
141
- onOpen(): void;
142
- onCopy(): void;
143
- }) {
190
+ function QuestionRow({ colors, question, first, onOpen, onCopy }: { colors: Colors; question: Question; first: boolean; onOpen(): void; onCopy(): void }) {
144
191
  const { messages: m } = useProgressLanguage();
145
192
  return (
146
193
  <View style={{ flexDirection: "row", gap: 8, paddingHorizontal: 10, paddingVertical: 10, borderTopWidth: first ? 0 : 1, borderTopColor: colors.border }}>
147
- <PressScale accessibilityRole="button" accessibilityLabel={m.copyQuestion(question.id)} onPress={onCopy} hitSlop={6}
194
+ <PressScale
195
+ accessibilityRole="button"
196
+ accessibilityLabel={m.copyQuestion(question.id)}
197
+ onPress={onCopy}
198
+ hitSlop={6}
148
199
  outerStyle={{ alignSelf: "flex-start", marginTop: 1 }}
149
200
  style={({ pressed }) => ({
150
201
  // One width for every id, so the questions beside them start on one edge.
@@ -154,27 +205,45 @@ function QuestionRow({ colors, question, first, onOpen, onCopy }: {
154
205
  borderRadius: 999,
155
206
  backgroundColor: pressed ? colors.surface2 : colors.surface1,
156
207
  ...raised(colors),
157
- })}>
208
+ })}
209
+ >
158
210
  <Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 17, fontWeight: "600" }}>{question.id}</Text>
159
211
  </PressScale>
160
- <PressableRow colors={colors} accessibilityRole="button" accessibilityLabel={m.questionChoices(question.id)} onPress={onOpen}
161
- style={{ flex: 1, minWidth: 0, flexDirection: "row", alignItems: "flex-start", gap: 8, borderRadius: 4, margin: -4, padding: 4 }}>
212
+ <PressableRow
213
+ colors={colors}
214
+ accessibilityRole="button"
215
+ accessibilityLabel={m.questionChoices(question.id)}
216
+ onPress={onOpen}
217
+ style={{ flex: 1, minWidth: 0, flexDirection: "row", alignItems: "flex-start", gap: 8, borderRadius: 4, margin: -4, padding: 4 }}
218
+ >
162
219
  <View style={{ flex: 1, minWidth: 0, gap: 2 }}>
163
220
  <Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 19 }}>{question.question}</Text>
164
221
  <QuestionState colors={colors} question={question} />
165
222
  </View>
166
223
  {/* Optical: centers the 14px chevron on the 19px first line. */}
167
- <View style={{ paddingTop: 2.5 }}><Icon name="ChevronRight" size={14} color={colors.foregroundMuted} /></View>
224
+ <View style={{ paddingTop: 2.5 }}>
225
+ <Icon name="ChevronRight" size={14} color={colors.foregroundMuted} />
226
+ </View>
168
227
  </PressableRow>
169
228
  </View>
170
229
  );
171
230
  }
172
231
 
173
232
  // A question dialog's body, also shown inside a ticket's dialog. `children` go at the end.
174
- export function QuestionView({ colors, question, now, onCopy, onOpenAttachment, children }: {
233
+ export function QuestionView({
234
+ colors,
235
+ question,
236
+ now,
237
+ root,
238
+ onCopy,
239
+ onOpenAttachment,
240
+ children,
241
+ }: {
175
242
  colors: Colors;
176
243
  question: Question;
177
244
  now: number;
245
+ // The worktree root the attachments' paths are relative to.
246
+ root: string;
178
247
  onCopy(letter: string): void;
179
248
  onOpenAttachment(attachment: Attachment, group: Attachment[]): void;
180
249
  children?: React.ReactNode;
@@ -186,7 +255,7 @@ export function QuestionView({ colors, question, now, onCopy, onOpenAttachment,
186
255
  {!question.answer ? <QuestionState colors={colors} question={question} /> : null}
187
256
  </View>
188
257
  {question.answer ? <AnswerSummary colors={colors} question={question} now={now} /> : null}
189
- <QuestionDetail colors={colors} question={question} now={now} onSurface1 onCopy={onCopy} onOpenAttachment={onOpenAttachment} />
258
+ <QuestionDetail colors={colors} question={question} now={now} root={root} onSurface1 onCopy={onCopy} onOpenAttachment={onOpenAttachment} />
190
259
  {children}
191
260
  </View>
192
261
  );
@@ -201,11 +270,33 @@ export function QuestionState({ colors, question }: { colors: Colors; question:
201
270
  const pick = question.options.find((option) => option.letter === question.recommended);
202
271
  return (
203
272
  <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
204
- {question.state === "building" ? <>{m.buildingWith}<Text style={value}>{question.recommended}{pick ? `${m.colon}${pick.label}` : ""}</Text></> : (
273
+ {question.state === "building" ? (
274
+ <>
275
+ {m.buildingWith}
276
+ <Text style={value}>
277
+ {question.recommended}
278
+ {pick ? `${m.colon}${pick.label}` : ""}
279
+ </Text>
280
+ </>
281
+ ) : (
205
282
  <>
206
- {!question.options.length && question.recommended ? <>{m.recommended}{m.colon}<Text style={value}>{question.recommended}</Text>{m.sentenceBreak}</> : null}
283
+ {!question.options.length && question.recommended ? (
284
+ <>
285
+ {m.recommended}
286
+ {m.colon}
287
+ <Text style={value}>{question.recommended}</Text>
288
+ {m.sentenceBreak}
289
+ </>
290
+ ) : null}
207
291
  {question.state === "stopped" ? m.agentStopped : m.waitingForYou}
208
- {question.big ? <>{m.sentenceBreak}<Text style={value}>{m.big}</Text>{m.colon}{question.big}</> : null}
292
+ {question.big ? (
293
+ <>
294
+ {m.sentenceBreak}
295
+ <Text style={value}>{m.big}</Text>
296
+ {m.colon}
297
+ {question.big}
298
+ </>
299
+ ) : null}
209
300
  </>
210
301
  )}
211
302
  </Text>
@@ -214,10 +305,19 @@ export function QuestionState({ colors, question }: { colors: Colors; question:
214
305
 
215
306
  // `onSurface1` when drawn on a surface1 background (Paseo's dialogs), so the
216
307
  // Copy buttons still stand off it.
217
- export function QuestionDetail({ colors, question, now, onCopy, onOpenAttachment, onSurface1 = false }: {
308
+ export function QuestionDetail({
309
+ colors,
310
+ question,
311
+ now,
312
+ root,
313
+ onCopy,
314
+ onOpenAttachment,
315
+ onSurface1 = false,
316
+ }: {
218
317
  colors: Colors;
219
318
  question: Question;
220
319
  now: number;
320
+ root: string;
221
321
  onCopy(letter: string): void;
222
322
  onOpenAttachment(attachment: Attachment, group: Attachment[]): void;
223
323
  onSurface1?: boolean;
@@ -233,7 +333,10 @@ export function QuestionDetail({ colors, question, now, onCopy, onOpenAttachment
233
333
  const borderColor = question.answer ? (chosen ? colors.statusSuccess : colors.border) : recommended ? colors.accent : colors.border;
234
334
  const settled = Boolean(question.answer);
235
335
  return (
236
- <View key={option.letter} style={{ flexDirection: "row", alignItems: "flex-start", gap: 8, padding: 8, borderRadius: 12, borderWidth: 1, borderColor }}>
336
+ <View
337
+ key={option.letter}
338
+ style={{ flexDirection: "row", alignItems: "flex-start", gap: 8, padding: 8, borderRadius: 12, borderWidth: 1, borderColor }}
339
+ >
237
340
  <Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 18, fontWeight: "600", width: 14 }}>{option.letter}</Text>
238
341
  <View style={{ flex: 1, gap: 2 }}>
239
342
  <Text selectable style={{ color: colors.foreground, fontSize: 13, lineHeight: 18 }}>
@@ -241,12 +344,30 @@ export function QuestionDetail({ colors, question, now, onCopy, onOpenAttachment
241
344
  {chosen ? <Text style={{ color: colors.statusSuccess }}>{" " + m.yourAnswer}</Text> : null}
242
345
  {recommended ? <Text style={{ color: settled ? colors.foregroundMuted : colors.accent }}>{" " + m.recommended}</Text> : null}
243
346
  </Text>
244
- {option.consequence ? <Text selectable style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>{option.consequence}</Text> : null}
347
+ {option.consequence ? (
348
+ <Text selectable style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
349
+ {option.consequence}
350
+ </Text>
351
+ ) : null}
245
352
  </View>
246
353
  {!question.answer ? (
247
- <PressScale accessibilityRole="button" accessibilityLabel={m.copyChoiceForAgent(question.id, option.letter)}
248
- onPress={() => onCopy(option.letter)} hitSlop={4}
249
- style={({ pressed }) => ({ flexDirection: "row", alignItems: "center", gap: 4, paddingLeft: 6, paddingRight: 8, paddingVertical: 3, borderRadius: 4, ...raised(colors), backgroundColor: pressed ? button.pressed : button.rest })}>
354
+ <PressScale
355
+ accessibilityRole="button"
356
+ accessibilityLabel={m.copyChoiceForAgent(question.id, option.letter)}
357
+ onPress={() => onCopy(option.letter)}
358
+ hitSlop={4}
359
+ style={({ pressed }) => ({
360
+ flexDirection: "row",
361
+ alignItems: "center",
362
+ gap: 4,
363
+ paddingLeft: 6,
364
+ paddingRight: 8,
365
+ paddingVertical: 3,
366
+ borderRadius: 4,
367
+ ...raised(colors),
368
+ backgroundColor: pressed ? button.pressed : button.rest,
369
+ })}
370
+ >
250
371
  <Icon name="Copy" size={12} color={colors.foreground} />
251
372
  <Text style={{ color: colors.foreground, fontSize: 12, lineHeight: 16 }}>{m.copyChoice(question.id, option.letter)}</Text>
252
373
  </PressScale>
@@ -254,12 +375,12 @@ export function QuestionDetail({ colors, question, now, onCopy, onOpenAttachment
254
375
  </View>
255
376
  );
256
377
  })}
257
- {question.background ? (
258
- <Markdown>{question.background}</Markdown>
259
- ) : null}
260
- <AttachmentList colors={colors} attachments={questionAttachments(question)} onOpen={onOpenAttachment} />
378
+ {question.background ? <Markdown>{question.background}</Markdown> : null}
379
+ <AttachmentList colors={colors} attachments={questionAttachments(question)} root={root} onOpen={onOpenAttachment} />
261
380
  <Text style={{ color: colors.foregroundMuted, fontSize: 11, lineHeight: 16 }}>
262
- {m.asked}<When colors={colors} iso={question.askedAt} now={now} />{question.raisedBy ? m.raisedBy(question.raisedBy) : ""}
381
+ {m.asked}
382
+ <When colors={colors} iso={question.askedAt} now={now} />
383
+ {question.raisedBy ? m.raisedBy(question.raisedBy) : ""}
263
384
  </Text>
264
385
  </View>
265
386
  );
package/client/row.tsx CHANGED
@@ -13,7 +13,14 @@ export function rowTint(colors: Colors, state: { hovered: boolean; pressed: bool
13
13
  }
14
14
 
15
15
  // A full-width row that opens something. `resting` is its background at rest.
16
- export function PressableRow({ colors, onSurface1 = false, transparentAtRest = true, style, children, ...props }: Omit<PressableProps, "style" | "children"> & {
16
+ export function PressableRow({
17
+ colors,
18
+ onSurface1 = false,
19
+ transparentAtRest = true,
20
+ style,
21
+ children,
22
+ ...props
23
+ }: Omit<PressableProps, "style" | "children"> & {
17
24
  colors: Colors;
18
25
  onSurface1?: boolean;
19
26
  // Rows on a surface1 background rest transparent; the working ticket rests on surface1.
@@ -23,11 +30,15 @@ export function PressableRow({ colors, onSurface1 = false, transparentAtRest = t
23
30
  }) {
24
31
  const [hovered, setHovered] = useState(false);
25
32
  return (
26
- <Pressable {...props} onHoverIn={() => setHovered(true)} onHoverOut={() => setHovered(false)}
33
+ <Pressable
34
+ {...props}
35
+ onHoverIn={() => setHovered(true)}
36
+ onHoverOut={() => setHovered(false)}
27
37
  style={({ pressed }) => {
28
38
  const tint = rowTint(colors, { hovered, pressed }, onSurface1);
29
39
  return [style, { backgroundColor: !hovered && !pressed && transparentAtRest ? "transparent" : tint }];
30
- }}>
40
+ }}
41
+ >
31
42
  {children}
32
43
  </Pressable>
33
44
  );
@@ -6,7 +6,14 @@ import { Text, View, type ViewStyle } from "react-native";
6
6
  type Colors = PluginWorkspacePanelProps["theme"]["colors"];
7
7
 
8
8
  // A card's heading: a 14px Lucide icon beside the 13px semibold title.
9
- export function SectionTitle({ colors, icon, title, color, style, children }: {
9
+ export function SectionTitle({
10
+ colors,
11
+ icon,
12
+ title,
13
+ color,
14
+ style,
15
+ children,
16
+ }: {
10
17
  colors: Colors;
11
18
  icon: string;
12
19
  title: string;
@@ -17,7 +24,9 @@ export function SectionTitle({ colors, icon, title, color, style, children }: {
17
24
  return (
18
25
  <View style={[{ flexDirection: "row", alignItems: "center", gap: 6, paddingHorizontal: 10, paddingVertical: 8 }, style]}>
19
26
  <Icon name={icon} size={14} color={color ?? colors.foregroundMuted} />
20
- <Text accessibilityRole="header" style={{ color: color ?? colors.foreground, fontSize: 13, lineHeight: 18, fontWeight: "600" }}>{title}</Text>
27
+ <Text accessibilityRole="header" style={{ color: color ?? colors.foreground, fontSize: 13, lineHeight: 18, fontWeight: "600" }}>
28
+ {title}
29
+ </Text>
21
30
  {children}
22
31
  </View>
23
32
  );
@@ -14,7 +14,14 @@ import { useSkill, useToolsState } from "./launcher";
14
14
  import { BlockView } from "./blocks";
15
15
 
16
16
  export function QuestionSettings({ theme, host }: PluginSurfaceProps) {
17
- return <><QuestionModeSettings theme={theme} /><DisplaySettings theme={theme} /><SkillSettings colors={theme.colors} hostId={host.id} /><ToolsSettings colors={theme.colors} hostId={host.id} /></>;
17
+ return (
18
+ <>
19
+ <QuestionModeSettings theme={theme} />
20
+ <DisplaySettings theme={theme} />
21
+ <SkillSettings colors={theme.colors} hostId={host.id} />
22
+ <ToolsSettings colors={theme.colors} hostId={host.id} />
23
+ </>
24
+ );
18
25
  }
19
26
 
20
27
  type Colors = PluginSurfaceProps["theme"]["colors"];
@@ -38,11 +45,23 @@ function DisplaySettings({ theme }: Pick<PluginSurfaceProps, "theme">) {
38
45
  const options = LANGUAGE_PREFERENCES.map((value) => ({ value, label: value === "system" ? m.systemLanguage : value === "en" ? "English" : "简体中文" }));
39
46
  return (
40
47
  <Section colors={theme.colors} icon="Languages" title={m.display}>
41
- {settings.status === "loading" ? <Text style={muted}>{m.loadingSettings}</Text>
42
- : settings.status !== "ready" ? <SettingsRecovery colors={theme.colors} settings={settings} />
43
- : <SettingsCard><SettingsSelect label={m.language} hint={m.languageHint} value={settings.values.language} options={options}
44
- disabled={settings.saving} error={settings.saveError}
45
- onValueChange={(language) => void settings.save({ ...settings.values, language }, settings.revision)} /></SettingsCard>}
48
+ {settings.status === "loading" ? (
49
+ <Text style={muted}>{m.loadingSettings}</Text>
50
+ ) : settings.status !== "ready" ? (
51
+ <SettingsRecovery colors={theme.colors} settings={settings} />
52
+ ) : (
53
+ <SettingsCard>
54
+ <SettingsSelect
55
+ label={m.language}
56
+ hint={m.languageHint}
57
+ value={settings.values.language}
58
+ options={options}
59
+ disabled={settings.saving}
60
+ error={settings.saveError}
61
+ onValueChange={(language) => void settings.save({ ...settings.values, language }, settings.revision)}
62
+ />
63
+ </SettingsCard>
64
+ )}
46
65
  </Section>
47
66
  );
48
67
  }
@@ -60,16 +79,29 @@ function SkillSettings({ colors, hostId }: { colors: Colors; hostId: string }) {
60
79
  <SettingsCard>
61
80
  <SettingsAction label={m.skillName} hint={document.data?.description || undefined} actionLabel={m.viewSkill} onPress={() => setOpen(true)} />
62
81
  {skill.notice?.action ? (
63
- <SettingsAction label={skill.state === "outdated" ? m.skillLinkOutdated : m.skillNotInstalled} hint={skill.notice.text}
64
- actionLabel={skill.pending ? m.installing : skill.notice.action} disabled={skill.pending} onPress={skill.install} />
82
+ <SettingsAction
83
+ label={skill.state === "outdated" ? m.skillLinkOutdated : m.skillNotInstalled}
84
+ hint={skill.notice.text}
85
+ actionLabel={skill.pending ? m.installing : skill.notice.action}
86
+ disabled={skill.pending}
87
+ onPress={skill.install}
88
+ />
65
89
  ) : null}
66
90
  </SettingsCard>
67
91
  {/* Paseo draws the modal outside this tree, past the language context, so it only gets finished text. */}
68
92
  <Modal title={m.skillName} icon={<Icon name="Sparkles" size={16} color={colors.foregroundMuted} />} open={open} onOpenChange={setOpen}>
69
93
  <Modal.Content>
70
- {document.error ? <Text style={muted}>{m.readError(errorText(document.error, m.unknownError))}</Text>
71
- : !document.data ? <Text style={muted}>{m.loading}</Text>
72
- : <View style={{ gap: 8 }}>{blocks.map((block, index) => <BlockView key={index} colors={colors} block={block} first={index === 0} />)}</View>}
94
+ {document.error ? (
95
+ <Text style={muted}>{m.readError(errorText(document.error, m.unknownError))}</Text>
96
+ ) : !document.data ? (
97
+ <Text style={muted}>{m.loading}</Text>
98
+ ) : (
99
+ <View style={{ gap: 8 }}>
100
+ {blocks.map((block, index) => (
101
+ <BlockView key={index} colors={colors} block={block} first={index === 0} />
102
+ ))}
103
+ </View>
104
+ )}
73
105
  </Modal.Content>
74
106
  </Modal>
75
107
  </Section>
@@ -98,26 +130,46 @@ function QuestionModeSettings({ theme }: Pick<PluginSurfaceProps, "theme">) {
98
130
  const muted = { color: theme.colors.foregroundMuted, fontSize: 13, lineHeight: 19 };
99
131
  return (
100
132
  <Section colors={theme.colors} icon="MessageCircleQuestion" title={m.questions}>
101
- {settings.status === "loading" ? <Text style={muted}>{m.loadingSettings}</Text>
102
- : settings.status !== "ready" ? <SettingsRecovery colors={theme.colors} settings={settings} resetLabel={m.resetTo(m.questionModes[DEFAULT_QUESTION_MODE].label)} />
103
- : <SettingsCard><SettingsSelect label={m.questionMode} hint={m.questionModes[settings.values.mode].hint}
104
- error={settings.saveError} value={settings.values.mode} options={QUESTION_MODES.map((value) => ({ value, label: m.questionModes[value].label }))}
105
- disabled={settings.saving} onValueChange={(mode) => void settings.save({ ...settings.values, mode }, settings.revision)} /></SettingsCard>}
133
+ {settings.status === "loading" ? (
134
+ <Text style={muted}>{m.loadingSettings}</Text>
135
+ ) : settings.status !== "ready" ? (
136
+ <SettingsRecovery colors={theme.colors} settings={settings} resetLabel={m.resetTo(m.questionModes[DEFAULT_QUESTION_MODE].label)} />
137
+ ) : (
138
+ <SettingsCard>
139
+ <SettingsSelect
140
+ label={m.questionMode}
141
+ hint={m.questionModes[settings.values.mode].hint}
142
+ error={settings.saveError}
143
+ value={settings.values.mode}
144
+ options={QUESTION_MODES.map((value) => ({ value, label: m.questionModes[value].label }))}
145
+ disabled={settings.saving}
146
+ onValueChange={(mode) => void settings.save({ ...settings.values, mode }, settings.revision)}
147
+ />
148
+ </SettingsCard>
149
+ )}
106
150
  </Section>
107
151
  );
108
152
  }
109
153
 
110
- function SettingsRecovery({ colors, settings, resetLabel }: {
154
+ function SettingsRecovery({
155
+ colors,
156
+ settings,
157
+ resetLabel,
158
+ }: {
111
159
  colors: Colors;
112
160
  settings: { status: "error" | "invalid"; error: string; reload(): Promise<void>; reset(): Promise<boolean> };
113
161
  resetLabel?: string;
114
162
  }) {
115
163
  const { messages: m } = useProgressLanguage();
116
- return <>
117
- <Text style={{ color: colors.foregroundMuted, fontSize: 13, lineHeight: 19 }}>{settings.error}</Text>
118
- <SettingsCard>
119
- <SettingsAction label={m.tryAgain} actionLabel={m.reload} onPress={settings.reload} />
120
- {settings.status === "invalid" ? <SettingsAction label={resetLabel ?? m.resetTo(m.systemLanguage)} actionLabel={m.reset} onPress={settings.reset} /> : null}
121
- </SettingsCard>
122
- </>;
164
+ return (
165
+ <>
166
+ <Text style={{ color: colors.foregroundMuted, fontSize: 13, lineHeight: 19 }}>{settings.error}</Text>
167
+ <SettingsCard>
168
+ <SettingsAction label={m.tryAgain} actionLabel={m.reload} onPress={settings.reload} />
169
+ {settings.status === "invalid" ? (
170
+ <SettingsAction label={resetLabel ?? m.resetTo(m.systemLanguage)} actionLabel={m.reset} onPress={settings.reset} />
171
+ ) : null}
172
+ </SettingsCard>
173
+ </>
174
+ );
123
175
  }
@@ -25,14 +25,31 @@ export function usePaged<T>(items: T[]): { shown: T[]; older: number; showOlder(
25
25
  // The row under a paged list: "Show 10 older", or "Show fewer" once all are shown.
26
26
  export function ShowMoreRow({ colors, total, paged }: { colors: Colors; total: number; paged: ReturnType<typeof usePaged> }) {
27
27
  const { messages: m } = useProgressLanguage();
28
- const more = paged.older > 0 ? { label: m.showOlder(Math.min(paged.older, PAGE)), icon: "ChevronDown", onPress: paged.showOlder }
29
- : total > PAGE ? { label: m.showFewer, icon: "ChevronUp", onPress: paged.showFewer }
30
- : null;
28
+ const more =
29
+ paged.older > 0
30
+ ? { label: m.showOlder(Math.min(paged.older, PAGE)), icon: "ChevronDown", onPress: paged.showOlder }
31
+ : total > PAGE
32
+ ? { label: m.showFewer, icon: "ChevronUp", onPress: paged.showFewer }
33
+ : null;
31
34
  if (!more) return null;
32
35
  return (
33
- <PressableRow colors={colors} accessibilityRole="button" onPress={more.onPress}
34
- style={{ flexDirection: "row", alignItems: "center", gap: 6, paddingHorizontal: 10, paddingVertical: 8, borderTopWidth: 1, borderTopColor: colors.border }}>
35
- <IconSwap swapKey={more.icon} size={14}><Icon name={more.icon} size={14} color={colors.foregroundMuted} /></IconSwap>
36
+ <PressableRow
37
+ colors={colors}
38
+ accessibilityRole="button"
39
+ onPress={more.onPress}
40
+ style={{
41
+ flexDirection: "row",
42
+ alignItems: "center",
43
+ gap: 6,
44
+ paddingHorizontal: 10,
45
+ paddingVertical: 8,
46
+ borderTopWidth: 1,
47
+ borderTopColor: colors.border,
48
+ }}
49
+ >
50
+ <IconSwap swapKey={more.icon} size={14}>
51
+ <Icon name={more.icon} size={14} color={colors.foregroundMuted} />
52
+ </IconSwap>
36
53
  <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontWeight: "600", fontVariant: ["tabular-nums"] }}>{more.label}</Text>
37
54
  </PressableRow>
38
55
  );
@@ -10,9 +10,11 @@ export function Spinner({ color, size = 12 }: { color: string; size?: number })
10
10
  const { messages: m } = useProgressLanguage();
11
11
  return (
12
12
  <View accessibilityLabel={m.status.working} style={{ width: size, height: size, alignItems: "center", justifyContent: "center" }}>
13
- {Platform.OS === "ios"
14
- ? <ActivityIndicator size="small" color={color} style={{ transform: [{ scale: size / 20 }] }} />
15
- : <ActivityIndicator size={size} color={color} />}
13
+ {Platform.OS === "ios" ? (
14
+ <ActivityIndicator size="small" color={color} style={{ transform: [{ scale: size / 20 }] }} />
15
+ ) : (
16
+ <ActivityIndicator size={size} color={color} />
17
+ )}
16
18
  </View>
17
19
  );
18
20
  }
@@ -23,10 +25,12 @@ export function StalledPulse({ color, size = 12 }: { color: string; size?: numbe
23
25
  const { messages: m } = useProgressLanguage();
24
26
  const opacity = useRef(new Animated.Value(1)).current;
25
27
  useEffect(() => {
26
- const loop = Animated.loop(Animated.sequence([
27
- Animated.timing(opacity, { toValue: 0.35, duration: 700, easing: Easing.inOut(Easing.ease), useNativeDriver: nativeDriver() }),
28
- Animated.timing(opacity, { toValue: 1, duration: 700, easing: Easing.inOut(Easing.ease), useNativeDriver: nativeDriver() }),
29
- ]));
28
+ const loop = Animated.loop(
29
+ Animated.sequence([
30
+ Animated.timing(opacity, { toValue: 0.35, duration: 700, easing: Easing.inOut(Easing.ease), useNativeDriver: nativeDriver() }),
31
+ Animated.timing(opacity, { toValue: 1, duration: 700, easing: Easing.inOut(Easing.ease), useNativeDriver: nativeDriver() }),
32
+ ]),
33
+ );
30
34
  loop.start();
31
35
  return () => loop.stop();
32
36
  }, [opacity]);
@@ -9,13 +9,15 @@ type Colors = PluginWorkspacePanelProps["theme"]["colors"];
9
9
  // `waiting`: held for another ticket. Shown as calm as "Not started", whatever the status.
10
10
  export function StatusBadge({ colors, status, waiting = false }: { colors: Colors; status: TicketStatus; waiting?: boolean }) {
11
11
  const { messages: m } = useProgressLanguage();
12
- const tone = waiting ? { fg: colors.foregroundMuted, bg: "transparent", border: colors.border } : {
13
- done: { fg: colors.statusSuccess, bg: "transparent", border: colors.statusSuccess },
14
- working: { fg: colors.accentForeground, bg: colors.accent, border: colors.accent },
15
- blocked: { fg: colors.statusDanger, bg: "transparent", border: colors.statusDanger },
16
- skipped: { fg: colors.foregroundMuted, bg: "transparent", border: colors.border },
17
- not_started: { fg: colors.foregroundMuted, bg: "transparent", border: colors.border },
18
- }[status];
12
+ const tone = waiting
13
+ ? { fg: colors.foregroundMuted, bg: "transparent", border: colors.border }
14
+ : {
15
+ done: { fg: colors.statusSuccess, bg: "transparent", border: colors.statusSuccess },
16
+ working: { fg: colors.accentForeground, bg: colors.accent, border: colors.accent },
17
+ blocked: { fg: colors.statusDanger, bg: "transparent", border: colors.statusDanger },
18
+ skipped: { fg: colors.foregroundMuted, bg: "transparent", border: colors.border },
19
+ not_started: { fg: colors.foregroundMuted, bg: "transparent", border: colors.border },
20
+ }[status];
19
21
  return (
20
22
  <View style={{ borderWidth: 1, borderColor: tone.border, backgroundColor: tone.bg, borderRadius: 999, paddingHorizontal: 8, paddingVertical: 1 }}>
21
23
  <Text style={{ color: tone.fg, fontSize: 11, lineHeight: 16 }}>{waiting ? m.waiting : m.status[status]}</Text>