@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
@@ -1,8 +1,7 @@
1
1
  import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
2
2
  import { Icon, Modal } from "@getpaseo/plugin/client/ui";
3
3
  import React, { useEffect, useRef, useState } from "react";
4
- import { Platform } from "react-native";
5
- import { opensAsPage } from "../shared/attachments";
4
+ import { isPage } from "../shared/attachments";
6
5
  import type { Dashboard, Question } from "../shared/dashboard";
7
6
  import { type Attachment, attachmentIcon, useAttachmentOpener } from "./attachments";
8
7
  import { NarrowProvider } from "./narrow";
@@ -25,7 +24,17 @@ export type Step =
25
24
 
26
25
  // Paseo gives each dialog its own backdrop, so opening a second one darkens
27
26
  // the screen while the first fades out; steps swap content inside this one instead.
28
- export function StackedDialog({ colors, root, onClose, questions = [], dashboard, now, live = true, compact = false, context }: {
27
+ export function StackedDialog({
28
+ colors,
29
+ root,
30
+ onClose,
31
+ questions = [],
32
+ dashboard,
33
+ now,
34
+ live = true,
35
+ compact = false,
36
+ context,
37
+ }: {
29
38
  colors: Colors;
30
39
  root: Step | null;
31
40
  onClose(): void;
@@ -50,18 +59,23 @@ export function StackedDialog({ colors, root, onClose, questions = [], dashboard
50
59
  const group = useRef<Attachment[]>([]);
51
60
  const push = (step: Step) => setSteps((current) => [...current, step]);
52
61
  const replaceTop = (step: Step) => setSteps((current) => [...current.slice(0, -1), step]);
53
- const opener = useAttachmentOpener({ ...context, onPreview: (attachment) => push({ kind: "preview", attachment, group: galleryFiles(group.current), fullScreen: false }) });
62
+ const opener = useAttachmentOpener({
63
+ ...context,
64
+ onPreview: (attachment) => push({ kind: "preview", attachment, group: galleryFiles(group.current), fullScreen: false }),
65
+ });
54
66
  const openAttachment = (attachment: Attachment, from: Attachment[]) => {
55
67
  group.current = from;
56
68
  // A link or a page opens outside the dialog; close first so it isn't left behind it.
57
- if (attachment.url || opensAsPage(attachment, { browser: Platform.OS === "web" })) onClose();
69
+ if (attachment.url || isPage(attachment)) onClose();
58
70
  void opener.open(attachment);
59
71
  };
60
72
  // Opens the agent's chat in its workspace, with the dialog out of the way.
61
- const openAgent = context.navigation ? (agentId: string) => {
62
- onClose();
63
- context.navigation!.openAgent({ agentId });
64
- } : undefined;
73
+ const openAgent = context.navigation
74
+ ? (agentId: string) => {
75
+ onClose();
76
+ context.navigation!.openAgent({ agentId });
77
+ }
78
+ : undefined;
65
79
  const copy = useCopy();
66
80
  const top = steps.at(-1);
67
81
  const back = steps.length > 1 ? () => setSteps((current) => current.slice(0, -1)) : undefined;
@@ -71,8 +85,13 @@ export function StackedDialog({ colors, root, onClose, questions = [], dashboard
71
85
  const preview = top?.kind === "preview" ? top : undefined;
72
86
  const setFullScreen = (on: boolean) => preview && replaceTop({ ...preview, fullScreen: on, leftFullScreen: !on });
73
87
  // The ticket or question it opened on went away (answered elsewhere, or a new run): close rather than show a blank dialog.
74
- const rootMissing = root !== null && (root.kind === "ticket" ? !dashboard?.tickets.some((candidate) => candidate.id === root.id)
75
- : root.kind === "question" ? !questions.some((candidate) => candidate.id === root.id) : false);
88
+ const rootMissing =
89
+ root !== null &&
90
+ (root.kind === "ticket"
91
+ ? !dashboard?.tickets.some((candidate) => candidate.id === root.id)
92
+ : root.kind === "question"
93
+ ? !questions.some((candidate) => candidate.id === root.id)
94
+ : false);
76
95
  useEffect(() => {
77
96
  if (rootMissing) onClose();
78
97
  }, [rootMissing]);
@@ -81,44 +100,88 @@ export function StackedDialog({ colors, root, onClose, questions = [], dashboard
81
100
  const icon = ticket ? "ListChecks" : question ? "MessageCircleQuestion" : preview ? attachmentIcon(preview.attachment) : "ListChecks";
82
101
 
83
102
  return (
84
- <Modal title={title} icon={<Icon name={icon} size={16} color={colors.foregroundMuted} />}
85
- open={Boolean(root)} onOpenChange={(next) => {
103
+ <Modal
104
+ title={title}
105
+ icon={<Icon name={icon} size={16} color={colors.foregroundMuted} />}
106
+ open={Boolean(root)}
107
+ onOpenChange={(next) => {
86
108
  if (next) return;
87
109
  // Esc leaves full screen first; the dialog stays open under it.
88
110
  if (preview?.fullScreen) setFullScreen(false);
89
111
  else onClose();
90
- }}>
112
+ }}
113
+ >
91
114
  {/* One Modal.Content for every step: replacing it would re-present the sheet on phones.
92
115
  Full screen turns its scrolling off. The image's fixed position reaches the window only
93
116
  when no ancestor has a transform, and a scroll view has one. */}
94
117
  <Modal.Content scrollable={!(preview && (previewLong || preview.fullScreen))}>
95
- <ProgressLanguageProvider>
96
- {/* A dialog has room for clock times, however narrow the panel that opened it. */}
97
- <NarrowProvider value={false}>
98
- {preview ? (
99
- <PreviewContent colors={colors} attachment={preview.attachment} workspaceId={context.workspaceId} workspaceDirectory={context.workspaceDirectory}
100
- onOpenOnHost={() => { onClose(); void opener.openOnHost(preview.attachment); }}
101
- onCopyPath={() => void opener.copyPath(preview.attachment)} onBack={back}
102
- gallery={{ items: preview.group, onSelect: (attachment) => replaceTop({ ...preview, attachment, fullScreen: preview.fullScreen && previewKind(attachment.path ?? "") === "image", leftFullScreen: false }) }}
103
- onLong={setPreviewLong} ownScroll={previewLong || preview.fullScreen} fullScreen={preview.fullScreen} leftFullScreen={preview.leftFullScreen === true} onFullScreen={setFullScreen}
104
- onOpenEntry={openAttachment} />
105
- ) : (
106
- <>
107
- {ticket && dashboard ? (
108
- <TicketStoryView colors={colors} dashboard={dashboard} ticket={ticket} now={now} live={live} compact={compact} context={context} onOpenAgent={openAgent}
109
- onOpenAttachment={openAttachment} onOpenQuestion={(id) => push({ kind: "question", id })} />
110
- ) : null}
111
- {question ? (
112
- <QuestionView colors={colors} question={question} now={now}
113
- onCopy={(letter) => { void copy(question, letter); onClose(); }}
114
- onOpenAttachment={openAttachment}>
115
- {back ? <BackButton colors={colors} onPress={back} /> : null}
116
- </QuestionView>
117
- ) : null}
118
- </>
119
- )}
120
- </NarrowProvider>
121
- </ProgressLanguageProvider>
118
+ <ProgressLanguageProvider>
119
+ {/* A dialog has room for clock times, however narrow the panel that opened it. */}
120
+ <NarrowProvider value={false}>
121
+ {preview ? (
122
+ <PreviewContent
123
+ colors={colors}
124
+ attachment={preview.attachment}
125
+ workspaceId={context.workspaceId}
126
+ workspaceDirectory={context.workspaceDirectory}
127
+ onOpenOnHost={() => {
128
+ onClose();
129
+ void opener.openOnHost(preview.attachment);
130
+ }}
131
+ onCopyPath={() => void opener.copyPath(preview.attachment)}
132
+ onBack={back}
133
+ gallery={{
134
+ items: preview.group,
135
+ onSelect: (attachment) =>
136
+ replaceTop({
137
+ ...preview,
138
+ attachment,
139
+ fullScreen: preview.fullScreen && previewKind(attachment.path ?? "") === "image",
140
+ leftFullScreen: false,
141
+ }),
142
+ }}
143
+ onLong={setPreviewLong}
144
+ ownScroll={previewLong || preview.fullScreen}
145
+ fullScreen={preview.fullScreen}
146
+ leftFullScreen={preview.leftFullScreen === true}
147
+ onFullScreen={setFullScreen}
148
+ onOpenEntry={openAttachment}
149
+ />
150
+ ) : (
151
+ <>
152
+ {ticket && dashboard ? (
153
+ <TicketStoryView
154
+ colors={colors}
155
+ dashboard={dashboard}
156
+ ticket={ticket}
157
+ now={now}
158
+ live={live}
159
+ compact={compact}
160
+ context={context}
161
+ onOpenAgent={openAgent}
162
+ onOpenAttachment={openAttachment}
163
+ onOpenQuestion={(id) => push({ kind: "question", id })}
164
+ />
165
+ ) : null}
166
+ {question ? (
167
+ <QuestionView
168
+ colors={colors}
169
+ question={question}
170
+ now={now}
171
+ root={context.workspaceDirectory}
172
+ onCopy={(letter) => {
173
+ void copy(question, letter);
174
+ onClose();
175
+ }}
176
+ onOpenAttachment={openAttachment}
177
+ >
178
+ {back ? <BackButton colors={colors} onPress={back} /> : null}
179
+ </QuestionView>
180
+ ) : null}
181
+ </>
182
+ )}
183
+ </NarrowProvider>
184
+ </ProgressLanguageProvider>
122
185
  </Modal.Content>
123
186
  </Modal>
124
187
  );
@@ -127,4 +190,3 @@ export function StackedDialog({ colors, root, onClose, questions = [], dashboard
127
190
  function stepKey(step: Step): string {
128
191
  return step.kind === "preview" ? `preview:${step.attachment.ref}` : `${step.kind}:${step.id}`;
129
192
  }
130
-
@@ -7,9 +7,7 @@ type PaseoApi = ReturnType<typeof getPaseoClient>;
7
7
  type Handler<Subscribe> = Subscribe extends (handler: infer H) => unknown ? H : never;
8
8
  export type PaseoAgentUpdate = Parameters<Handler<PaseoApi["agents"]["subscribe"]>>[0];
9
9
  export type PaseoWorkspaceUpdate = Parameters<Handler<PaseoApi["workspaces"]["subscribe"]>>[0];
10
- type Message = Parameters<
11
- Parameters<NonNullable<Awaited<ReturnType<PaseoApi["agents"]["list"]>>["subscription"]>["subscribe"]>[0]["update"]
12
- >[0];
10
+ type Message = Parameters<Parameters<NonNullable<Awaited<ReturnType<PaseoApi["agents"]["list"]>>["subscription"]>["subscribe"]>[0]["update"]>[0];
13
11
  type OwnedSubscription<T> = {
14
12
  subscribe(observer: { snapshot(snapshot: T): void; update(message: Message): void; error?(error: unknown): void }): () => void;
15
13
  release(): Promise<void>;
package/client/errors.ts CHANGED
@@ -2,5 +2,10 @@
2
2
  export function errorText(error: unknown, fallback: string): string {
3
3
  const message = (error as { message?: unknown } | null)?.message;
4
4
  if (typeof message !== "string") return fallback;
5
- return message.replace(/^Request failed: /, "").replace(/ requestType=\S+(?: code=\S+)?$/, "").trim() || fallback;
5
+ return (
6
+ message
7
+ .replace(/^Request failed: /, "")
8
+ .replace(/ requestType=\S+(?: code=\S+)?$/, "")
9
+ .trim() || fallback
10
+ );
6
11
  }
@@ -13,12 +13,7 @@ type Colors = PluginWorkspacePanelProps["theme"]["colors"];
13
13
 
14
14
  // Run history stays on this computer unless the repo opts in. This quiet row
15
15
  // is the only place that changes it; nothing in the repo changes until then.
16
- export function HistoryRow({ colors, workspaceId, directory, savedTo }: {
17
- colors: Colors;
18
- workspaceId: string;
19
- directory: string;
20
- savedTo: string | null;
21
- }) {
16
+ export function HistoryRow({ colors, workspaceId, directory, savedTo }: { colors: Colors; workspaceId: string; directory: string; savedTo: string | null }) {
22
17
  const { messages: m } = useProgressLanguage();
23
18
  const [picking, setPicking] = useState(false);
24
19
  const set = useRpc(setHistoryFolder);
@@ -35,35 +30,83 @@ export function HistoryRow({ colors, workspaceId, directory, savedTo }: {
35
30
  // While saving, the button that started it says so.
36
31
  const busy = mutation.isPending ? (mutation.variables === null ? "stop" : "pick") : null;
37
32
  const actions: { label: string; busyLabel: string; kind: "pick" | "stop"; onPress(): void }[] = savedTo
38
- ? [{ label: m.changeFolder, busyLabel: m.saving, kind: "pick", onPress: () => setPicking(true) }, { label: m.stopSaving, busyLabel: m.stopping, kind: "stop", onPress: () => mutation.mutate(null) }]
33
+ ? [
34
+ { label: m.changeFolder, busyLabel: m.saving, kind: "pick", onPress: () => setPicking(true) },
35
+ { label: m.stopSaving, busyLabel: m.stopping, kind: "stop", onPress: () => mutation.mutate(null) },
36
+ ]
39
37
  : [{ label: m.saveToRepo, busyLabel: m.saving, kind: "pick", onPress: () => setPicking(true) }];
40
38
 
41
39
  return (
42
40
  <View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "center", columnGap: 8, rowGap: 4, marginHorizontal: 12, marginTop: 16 }}>
43
41
  <View style={{ flexDirection: "row", alignItems: "flex-start", gap: 6, flex: 1, minWidth: 180 }}>
44
- <View style={{ paddingTop: 2 }}><Icon name="History" size={13} color={colors.foregroundMuted} /></View>
42
+ <View style={{ paddingTop: 2 }}>
43
+ <Icon name="History" size={13} color={colors.foregroundMuted} />
44
+ </View>
45
45
  <Text style={{ flex: 1, color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
46
- {savedTo ? <>{m.historyInRepo[0]}<Text style={{ color: colors.foreground }}>{savedTo}</Text>{m.historyInRepo[1]}</> : m.localHistory}
46
+ {savedTo ? (
47
+ <>
48
+ {m.historyInRepo[0]}
49
+ <Text style={{ color: colors.foreground }}>{savedTo}</Text>
50
+ {m.historyInRepo[1]}
51
+ </>
52
+ ) : (
53
+ m.localHistory
54
+ )}
47
55
  </Text>
48
56
  </View>
49
57
  <View style={{ flexDirection: "row", gap: 4 }}>
50
58
  {actions.map((action) => (
51
- <PressScale key={action.label} accessibilityRole="button" disabled={mutation.isPending} onPress={action.onPress}
52
- style={({ pressed, hovered }) => ({ paddingHorizontal: 8, paddingVertical: 4, borderRadius: 4, opacity: mutation.isPending ? 0.7 : 1,
53
- backgroundColor: pressed ? colors.border : hovered ? colors.surface2 : "transparent" })}>
54
- <Text style={{ color: colors.foreground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>{busy === action.kind ? action.busyLabel : action.label}</Text>
59
+ <PressScale
60
+ key={action.label}
61
+ accessibilityRole="button"
62
+ disabled={mutation.isPending}
63
+ onPress={action.onPress}
64
+ style={({ pressed, hovered }) => ({
65
+ paddingHorizontal: 8,
66
+ paddingVertical: 4,
67
+ borderRadius: 4,
68
+ opacity: mutation.isPending ? 0.7 : 1,
69
+ backgroundColor: pressed ? colors.border : hovered ? colors.surface2 : "transparent",
70
+ })}
71
+ >
72
+ <Text style={{ color: colors.foreground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>
73
+ {busy === action.kind ? action.busyLabel : action.label}
74
+ </Text>
55
75
  </PressScale>
56
76
  ))}
57
77
  </View>
58
- <FolderPicker key={picking ? "open" : "closed"} colors={colors} workspaceId={workspaceId} directory={directory} open={picking} start={savedTo ?? ""}
59
- onClose={() => setPicking(false)} onPick={(folder) => { setPicking(false); mutation.mutate(folder); }} />
78
+ <FolderPicker
79
+ key={picking ? "open" : "closed"}
80
+ colors={colors}
81
+ workspaceId={workspaceId}
82
+ directory={directory}
83
+ open={picking}
84
+ start={savedTo ?? ""}
85
+ onClose={() => setPicking(false)}
86
+ onPick={(folder) => {
87
+ setPicking(false);
88
+ mutation.mutate(folder);
89
+ }}
90
+ />
60
91
  </View>
61
92
  );
62
93
  }
63
94
 
95
+ function parentOf(path: string): string {
96
+ return path.split("/").slice(0, -1).join("/");
97
+ }
98
+
64
99
  // Browses the repo's folders on the daemon host. A new folder only needs a
65
100
  // name: it's made the first time a run is saved there.
66
- function FolderPicker({ colors, workspaceId, directory, open, start, onClose, onPick }: {
101
+ function FolderPicker({
102
+ colors,
103
+ workspaceId,
104
+ directory,
105
+ open,
106
+ start,
107
+ onClose,
108
+ onPick,
109
+ }: {
67
110
  colors: Colors;
68
111
  workspaceId: string;
69
112
  directory: string;
@@ -74,7 +117,6 @@ function FolderPicker({ colors, workspaceId, directory, open, start, onClose, on
74
117
  onPick(folder: string): void;
75
118
  }) {
76
119
  const { messages: m } = useProgressLanguage();
77
- const parentOf = (path: string) => path.split("/").slice(0, -1).join("/");
78
120
  const [at, setAt] = useState(() => parentOf(start));
79
121
  const [naming, setNaming] = useState<string | null>(null);
80
122
  const list = useRpc(listFolders);
@@ -85,78 +127,190 @@ function FolderPicker({ colors, workspaceId, directory, open, start, onClose, on
85
127
  });
86
128
  const join = (name: string) => (at ? `${at}/${name}` : name);
87
129
  const crumbs = at ? at.split("/") : [];
88
- const close = () => { setNaming(null); setAt(parentOf(start)); onClose(); };
130
+ const close = () => {
131
+ setNaming(null);
132
+ setAt(parentOf(start));
133
+ onClose();
134
+ };
89
135
  const newName = naming?.trim().replace(/\/+$/, "") ?? "";
90
136
 
91
137
  return (
92
- <Modal title={m.historyFolderTitle} icon={<Icon name="FolderOpen" size={16} color={colors.foregroundMuted} />} open={open} onOpenChange={(next) => { if (!next) close(); }}>
138
+ <Modal
139
+ title={m.historyFolderTitle}
140
+ icon={<Icon name="FolderOpen" size={16} color={colors.foregroundMuted} />}
141
+ open={open}
142
+ onOpenChange={(next) => {
143
+ if (!next) close();
144
+ }}
145
+ >
93
146
  {/* Paseo sizes a non-scrolling dialog to 85% of the window, so let it size to
94
147
  its content; the list's own height cap keeps the buttons in view. */}
95
148
  <Modal.Content>
96
- <ProgressLanguageProvider>
97
- <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
98
- {m.historyFolderHint}
99
- </Text>
100
- <View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "center", gap: 2, paddingHorizontal: 4 }}>
101
- {[m.repoTop, ...crumbs].map((crumb, index) => (
102
- <React.Fragment key={index}>
103
- {index ? <Icon name="ChevronRight" size={12} color={colors.foregroundMuted} /> : null}
104
- <PressScale accessibilityRole="button" disabled={index === crumbs.length} onPress={() => { setNaming(null); setAt(crumbs.slice(0, index).join("/")); }}
105
- style={({ pressed, hovered }) => ({ paddingHorizontal: 4, paddingVertical: 2, borderRadius: 4,
106
- backgroundColor: index === crumbs.length ? "transparent" : pressed ? colors.border : hovered ? colors.surface2 : "transparent" })}>
107
- <Text style={{ color: index === crumbs.length ? colors.foreground : colors.foregroundMuted, fontSize: 12, lineHeight: 16, fontWeight: index === crumbs.length ? "600" : "400" }}>{crumb}</Text>
108
- </PressScale>
109
- </React.Fragment>
110
- ))}
111
- </View>
149
+ <ProgressLanguageProvider>
150
+ <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>{m.historyFolderHint}</Text>
151
+ <View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "center", gap: 2, paddingHorizontal: 4 }}>
152
+ {[m.repoTop, ...crumbs].map((crumb, index) => (
153
+ <React.Fragment key={index}>
154
+ {index ? <Icon name="ChevronRight" size={12} color={colors.foregroundMuted} /> : null}
155
+ <PressScale
156
+ accessibilityRole="button"
157
+ disabled={index === crumbs.length}
158
+ onPress={() => {
159
+ setNaming(null);
160
+ setAt(crumbs.slice(0, index).join("/"));
161
+ }}
162
+ style={({ pressed, hovered }) => ({
163
+ paddingHorizontal: 4,
164
+ paddingVertical: 2,
165
+ borderRadius: 4,
166
+ backgroundColor: index === crumbs.length ? "transparent" : pressed ? colors.border : hovered ? colors.surface2 : "transparent",
167
+ })}
168
+ >
169
+ <Text
170
+ style={{
171
+ color: index === crumbs.length ? colors.foreground : colors.foregroundMuted,
172
+ fontSize: 12,
173
+ lineHeight: 16,
174
+ fontWeight: index === crumbs.length ? "600" : "400",
175
+ }}
176
+ >
177
+ {crumb}
178
+ </Text>
179
+ </PressScale>
180
+ </React.Fragment>
181
+ ))}
182
+ </View>
112
183
 
113
- {/* Only the list scrolls, so Use and Cancel stay in view in a big repo. */}
114
- <ScrollView style={{ maxHeight: 320, flexGrow: 0 }} contentContainerStyle={{ gap: 2 }}>
115
- {listed.isPending ? <ActivityIndicator color={colors.foregroundMuted} accessibilityLabel={m.loadingFolders} style={{ alignSelf: "flex-start", padding: 8 }} /> : null}
116
- {listed.data?.folders.map((name) => (
117
- <PressableRow key={name} colors={colors} accessibilityRole="button" accessibilityLabel={m.openItem(name)} onPress={() => { setNaming(null); setAt(join(name)); }}
118
- style={{ flexDirection: "row", alignItems: "center", gap: 8, paddingHorizontal: 8, paddingVertical: 6, borderRadius: 6 }}>
119
- <Icon name="Folder" size={14} color={colors.foregroundMuted} />
120
- <Text numberOfLines={1} ellipsizeMode="middle" style={{ flex: 1, color: colors.foreground, fontSize: 12, lineHeight: 16 }}>{name}</Text>
121
- <Icon name="ChevronRight" size={14} color={colors.foregroundMuted} />
122
- </PressableRow>
123
- ))}
124
- {listed.data && !listed.data.folders.length ? (
125
- <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 16, paddingHorizontal: 8, paddingVertical: 6 }}>{m.noFolders}</Text>
126
- ) : null}
127
- {listed.error ? <Text accessibilityRole="alert" style={{ color: colors.statusDanger, fontSize: 12, lineHeight: 17, paddingHorizontal: 8 }}>{errorText(listed.error, m.listFoldersFailure)}</Text> : null}
128
- </ScrollView>
184
+ {/* Only the list scrolls, so Use and Cancel stay in view in a big repo. */}
185
+ <ScrollView style={{ maxHeight: 320, flexGrow: 0 }} contentContainerStyle={{ gap: 2 }}>
186
+ {listed.isPending ? (
187
+ <ActivityIndicator color={colors.foregroundMuted} accessibilityLabel={m.loadingFolders} style={{ alignSelf: "flex-start", padding: 8 }} />
188
+ ) : null}
189
+ {listed.data?.folders.map((name) => (
190
+ <PressableRow
191
+ key={name}
192
+ colors={colors}
193
+ accessibilityRole="button"
194
+ accessibilityLabel={m.openItem(name)}
195
+ onPress={() => {
196
+ setNaming(null);
197
+ setAt(join(name));
198
+ }}
199
+ style={{ flexDirection: "row", alignItems: "center", gap: 8, paddingHorizontal: 8, paddingVertical: 6, borderRadius: 6 }}
200
+ >
201
+ <Icon name="Folder" size={14} color={colors.foregroundMuted} />
202
+ <Text numberOfLines={1} ellipsizeMode="middle" style={{ flex: 1, color: colors.foreground, fontSize: 12, lineHeight: 16 }}>
203
+ {name}
204
+ </Text>
205
+ <Icon name="ChevronRight" size={14} color={colors.foregroundMuted} />
206
+ </PressableRow>
207
+ ))}
208
+ {listed.data && !listed.data.folders.length ? (
209
+ <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 16, paddingHorizontal: 8, paddingVertical: 6 }}>{m.noFolders}</Text>
210
+ ) : null}
211
+ {listed.error ? (
212
+ <Text accessibilityRole="alert" style={{ color: colors.statusDanger, fontSize: 12, lineHeight: 17, paddingHorizontal: 8 }}>
213
+ {errorText(listed.error, m.listFoldersFailure)}
214
+ </Text>
215
+ ) : null}
216
+ </ScrollView>
129
217
 
130
- {naming === null ? (
131
- <PressScale accessibilityRole="button" onPress={() => setNaming("")}
132
- style={({ pressed, hovered }) => ({ alignSelf: "flex-start", flexDirection: "row", alignItems: "center", gap: 6, paddingHorizontal: 8, paddingVertical: 5, borderRadius: 6, backgroundColor: pressed ? colors.border : hovered ? colors.surface2 : "transparent" })}>
133
- <Icon name="FolderPlus" size={14} color={colors.foreground} />
134
- <Text style={{ color: colors.foreground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>{m.newFolder}</Text>
135
- </PressScale>
136
- ) : (
137
- <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
138
- <TextInput autoFocus value={naming} onChangeText={setNaming} onSubmitEditing={() => { if (newName) onPick(join(newName)); }}
139
- autoCapitalize="none" autoCorrect={false} spellCheck={false} accessibilityLabel={m.newFolderName} placeholder="progress-history" placeholderTextColor={colors.foregroundMuted}
140
- style={{ flex: 1, color: colors.foreground, fontSize: 12, lineHeight: 16, paddingHorizontal: 8, paddingVertical: 6, borderRadius: 6, borderWidth: 1, borderColor: colors.border, backgroundColor: colors.surface1 }} />
141
- <PressScale accessibilityRole="button" disabled={!newName} onPress={() => onPick(join(newName))}
142
- style={({ pressed, hovered }) => ({ paddingHorizontal: 10, paddingVertical: 6, borderRadius: 6, backgroundColor: colors.accent, opacity: !newName ? 0.5 : pressed ? 0.7 : hovered ? 0.85 : 1 })}>
143
- <Text style={{ color: colors.accentForeground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>{m.useNewFolder}</Text>
218
+ {naming === null ? (
219
+ <PressScale
220
+ accessibilityRole="button"
221
+ onPress={() => setNaming("")}
222
+ style={({ pressed, hovered }) => ({
223
+ alignSelf: "flex-start",
224
+ flexDirection: "row",
225
+ alignItems: "center",
226
+ gap: 6,
227
+ paddingHorizontal: 8,
228
+ paddingVertical: 5,
229
+ borderRadius: 6,
230
+ backgroundColor: pressed ? colors.border : hovered ? colors.surface2 : "transparent",
231
+ })}
232
+ >
233
+ <Icon name="FolderPlus" size={14} color={colors.foreground} />
234
+ <Text style={{ color: colors.foreground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>{m.newFolder}</Text>
144
235
  </PressScale>
145
- </View>
146
- )}
236
+ ) : (
237
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
238
+ <TextInput
239
+ autoFocus
240
+ value={naming}
241
+ onChangeText={setNaming}
242
+ onSubmitEditing={() => {
243
+ if (newName) onPick(join(newName));
244
+ }}
245
+ autoCapitalize="none"
246
+ autoCorrect={false}
247
+ spellCheck={false}
248
+ accessibilityLabel={m.newFolderName}
249
+ placeholder="progress-history"
250
+ placeholderTextColor={colors.foregroundMuted}
251
+ style={{
252
+ flex: 1,
253
+ color: colors.foreground,
254
+ fontSize: 12,
255
+ lineHeight: 16,
256
+ paddingHorizontal: 8,
257
+ paddingVertical: 6,
258
+ borderRadius: 6,
259
+ borderWidth: 1,
260
+ borderColor: colors.border,
261
+ backgroundColor: colors.surface1,
262
+ }}
263
+ />
264
+ <PressScale
265
+ accessibilityRole="button"
266
+ disabled={!newName}
267
+ onPress={() => onPick(join(newName))}
268
+ style={({ pressed, hovered }) => ({
269
+ paddingHorizontal: 10,
270
+ paddingVertical: 6,
271
+ borderRadius: 6,
272
+ backgroundColor: colors.accent,
273
+ opacity: !newName ? 0.5 : pressed ? 0.7 : hovered ? 0.85 : 1,
274
+ })}
275
+ >
276
+ <Text style={{ color: colors.accentForeground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>{m.useNewFolder}</Text>
277
+ </PressScale>
278
+ </View>
279
+ )}
147
280
 
148
- <View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "center", justifyContent: "flex-end", gap: 8 }}>
149
- {at ? null : <Text style={{ flex: 1, minWidth: 140, color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>{m.pickFolderHint}</Text>}
150
- <PressScale accessibilityRole="button" onPress={close}
151
- style={({ pressed, hovered }) => ({ paddingHorizontal: 12, paddingVertical: 6, borderRadius: 6, backgroundColor: pressed ? colors.border : hovered ? colors.surface2 : "transparent" })}>
152
- <Text style={{ color: colors.foreground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>{m.cancel}</Text>
153
- </PressScale>
154
- <PressScale accessibilityRole="button" disabled={!at} onPress={() => onPick(at)} outerStyle={{ flexShrink: 1 }}
155
- style={({ pressed, hovered }) => ({ paddingHorizontal: 12, paddingVertical: 6, borderRadius: 6, backgroundColor: colors.accent, opacity: !at ? 0.5 : pressed ? 0.7 : hovered ? 0.85 : 1 })}>
156
- <Text numberOfLines={1} ellipsizeMode="middle" style={{ color: colors.accentForeground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>{at ? m.useNamedFolder(crumbs[crumbs.length - 1]) : m.useFolder}</Text>
157
- </PressScale>
158
- </View>
159
- </ProgressLanguageProvider>
281
+ <View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "center", justifyContent: "flex-end", gap: 8 }}>
282
+ {at ? null : <Text style={{ flex: 1, minWidth: 140, color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>{m.pickFolderHint}</Text>}
283
+ <PressScale
284
+ accessibilityRole="button"
285
+ onPress={close}
286
+ style={({ pressed, hovered }) => ({
287
+ paddingHorizontal: 12,
288
+ paddingVertical: 6,
289
+ borderRadius: 6,
290
+ backgroundColor: pressed ? colors.border : hovered ? colors.surface2 : "transparent",
291
+ })}
292
+ >
293
+ <Text style={{ color: colors.foreground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>{m.cancel}</Text>
294
+ </PressScale>
295
+ <PressScale
296
+ accessibilityRole="button"
297
+ disabled={!at}
298
+ onPress={() => onPick(at)}
299
+ outerStyle={{ flexShrink: 1 }}
300
+ style={({ pressed, hovered }) => ({
301
+ paddingHorizontal: 12,
302
+ paddingVertical: 6,
303
+ borderRadius: 6,
304
+ backgroundColor: colors.accent,
305
+ opacity: !at ? 0.5 : pressed ? 0.7 : hovered ? 0.85 : 1,
306
+ })}
307
+ >
308
+ <Text numberOfLines={1} ellipsizeMode="middle" style={{ color: colors.accentForeground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>
309
+ {at ? m.useNamedFolder(crumbs[crumbs.length - 1]) : m.useFolder}
310
+ </Text>
311
+ </PressScale>
312
+ </View>
313
+ </ProgressLanguageProvider>
160
314
  </Modal.Content>
161
315
  </Modal>
162
316
  );
package/client/i18n.ts CHANGED
@@ -77,10 +77,15 @@ export function clientHeadline(locale: ProgressLocale, dashboard: Dashboard): He
77
77
 
78
78
  export function timelineLabel(m: ProgressMessages, label: TimelineLabel): string {
79
79
  switch (label.kind) {
80
- case "added": return m.added;
81
- case "reset": return m.resetStatus;
82
- case "stage": return m.stageLabel(label.stage);
83
- case "status": return m.status[label.status];
84
- case "waiting": return m.waitingForTicket(label.id);
80
+ case "added":
81
+ return m.added;
82
+ case "reset":
83
+ return m.resetStatus;
84
+ case "stage":
85
+ return m.stageLabel(label.stage);
86
+ case "status":
87
+ return m.status[label.status];
88
+ case "waiting":
89
+ return m.waitingForTicket(label.id);
85
90
  }
86
91
  }