@stevecastaneda/paseo-progress 1.4.0 → 1.5.1

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.
@@ -9,12 +9,14 @@ import { spans } from "../shared/write-up";
9
9
  import { PressScale, Presence } from "./motion";
10
10
  import { isLight, raised, tint } from "./surfaces";
11
11
  import { BlockView, Spans } from "./write-up";
12
+ import { ProgressLanguageProvider, useProgressLanguage } from "./language";
12
13
 
13
14
  type Colors = PluginWorkspacePanelProps["theme"]["colors"];
14
15
 
15
16
  // Once after an update: this version's headline, with its full notes a press
16
17
  // away. Closing either hides both for this version.
17
18
  export function WhatsNewBanner({ colors, host }: { colors: Colors; host: PluginWorkspacePanelProps["host"] }) {
19
+ const { messages: m } = useProgressLanguage();
18
20
  const fetchWhatsNew = useRpc(getWhatsNew);
19
21
  const dismiss = useRpc(dismissWhatsNew);
20
22
  const queryClient = useQueryClient();
@@ -40,31 +42,33 @@ export function WhatsNewBanner({ colors, host }: { colors: Colors; host: PluginW
40
42
  <View style={{ flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 10, margin: 12, marginBottom: 0, padding: 8, borderRadius: 12, ...raised(colors), backgroundColor: colors.surface1 }}>
41
43
  <View style={{ alignSelf: "flex-start", height: 17, justifyContent: "center" }}><Icon name="Gift" size={14} color={colors.foregroundMuted} /></View>
42
44
  <Text selectable style={{ flex: 1, minWidth: 200, color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
43
- <Text style={{ color: colors.foreground, fontWeight: "500" }}>New in {whatsNew.version}:</Text> {whatsNew.headline}
45
+ <Text style={{ color: colors.foreground, fontWeight: "500" }}>{m.newVersion(whatsNew.version)}</Text> {whatsNew.headline}
44
46
  </Text>
45
47
  {/* 8px keeps the close button's 6px hit slop off the button beside it. */}
46
48
  <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
47
49
  <PressScale accessibilityRole="button" onPress={() => { setShown(whatsNew); setOpen(true); }}
48
50
  style={({ pressed, hovered }) => ({ paddingHorizontal: 10, paddingVertical: 5, borderRadius: 4, backgroundColor: colors.accent, opacity: pressed ? 0.7 : hovered ? 0.85 : 1 })}>
49
- <Text style={{ color: colors.accentForeground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>See what’s new</Text>
51
+ <Text style={{ color: colors.accentForeground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>{m.seeWhatsNew}</Text>
50
52
  </PressScale>
51
- <PressScale accessibilityRole="button" accessibilityLabel="Dismiss what's new" hitSlop={6} onPress={() => close(whatsNew.version)}
53
+ <PressScale accessibilityRole="button" accessibilityLabel={m.dismissWhatsNew} hitSlop={6} onPress={() => close(whatsNew.version)}
52
54
  style={({ pressed, hovered }) => ({ padding: 5, borderRadius: 4, backgroundColor: pressed ? colors.border : hovered ? colors.surface2 : "transparent" })}>
53
55
  <Icon name="X" size={16} color={colors.foregroundMuted} />
54
56
  </PressScale>
55
57
  </View>
56
58
  </View>
57
59
  ) : null}</Presence>
58
- <Modal title={shown ? `What’s new in Progress ${shown.version}` : ""} icon={<Icon name="Gift" size={16} color={colors.foregroundMuted} />}
60
+ <Modal title={shown ? m.whatsNewTitle(shown.version) : ""} icon={<Icon name="Gift" size={16} color={colors.foregroundMuted} />}
59
61
  open={open} onOpenChange={(next) => { if (!next) closeDialog(); }}>
60
62
  <Modal.Content>
63
+ <ProgressLanguageProvider>
61
64
  {shown ? <Notes colors={colors} notes={shown.notes} /> : null}
62
65
  <View style={{ flexDirection: "row", justifyContent: "flex-end", marginTop: 8 }}>
63
66
  <PressScale accessibilityRole="button" onPress={closeDialog}
64
67
  style={({ pressed, hovered }) => ({ paddingHorizontal: 12, paddingVertical: 6, borderRadius: 6, backgroundColor: colors.accent, opacity: pressed ? 0.7 : hovered ? 0.85 : 1 })}>
65
- <Text style={{ color: colors.accentForeground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>Got it</Text>
68
+ <Text style={{ color: colors.accentForeground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>{m.gotIt}</Text>
66
69
  </PressScale>
67
70
  </View>
71
+ </ProgressLanguageProvider>
68
72
  </Modal.Content>
69
73
  </Modal>
70
74
  </>
@@ -111,10 +115,11 @@ const TONE_COLOR: Record<NoteTone, (colors: Colors) => string> = {
111
115
  // Sits on the title's first line: 18px tall, 1px down in its 20px line. As wide
112
116
  // as "Improved" at least, so the titles beside the tags start at one edge.
113
117
  function KindTag({ colors, kind }: { colors: Colors; kind: NoteKind }) {
118
+ const { messages: m } = useProgressLanguage();
114
119
  const color = TONE_COLOR[NOTE_KINDS[kind].tone](colors);
115
120
  return (
116
121
  <View style={{ marginTop: 1, minWidth: 64, borderRadius: 4, paddingHorizontal: 7, backgroundColor: tint(color, colors) }}>
117
- <Text numberOfLines={1} style={{ textAlign: "center", color, fontSize: 11, lineHeight: 18, fontWeight: "500" }}>{NOTE_KINDS[kind].label}</Text>
122
+ <Text numberOfLines={1} style={{ textAlign: "center", color, fontSize: 11, lineHeight: 18, fontWeight: "500" }}>{m.noteKinds[kind]}</Text>
118
123
  </View>
119
124
  );
120
125
  }
package/client/when.tsx CHANGED
@@ -1,8 +1,9 @@
1
1
  import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
2
2
  import React from "react";
3
3
  import { Text } from "react-native";
4
- import { formatAgo, formatClock } from "../shared/format";
4
+ import { formatClientAgo, formatClientClock } from "./i18n";
5
5
  import { useNarrow } from "./narrow";
6
+ import { useProgressLanguage } from "./language";
6
7
 
7
8
  type Colors = PluginWorkspacePanelProps["theme"]["colors"];
8
9
 
@@ -15,11 +16,12 @@ export function unbroken(text: string): string {
15
16
  // "Sep 27, 8:14 PM (10h 2m ago)", inline inside other text. A narrow
16
17
  // panel shows only "10h 2m ago"; the ticket dialog keeps the clock time.
17
18
  export function When({ colors, iso, now }: { colors: Colors; iso: string; now: number }) {
18
- const ago = unbroken(formatAgo(iso, now));
19
+ const { locale, messages: m } = useProgressLanguage();
20
+ const ago = unbroken(formatClientAgo(locale, iso, now));
19
21
  if (useNarrow()) return <Text>{ago}</Text>;
20
22
  return (
21
23
  <Text>
22
- {formatClock(iso)} <Text style={{ color: colors.foregroundMuted, opacity: 0.7 }}>({ago})</Text>
24
+ {formatClientClock(locale, iso)}<Text style={{ color: colors.foregroundMuted, opacity: 0.7 }}>{m.parenthetical(ago)}</Text>
23
25
  </Text>
24
26
  );
25
27
  }
@@ -9,6 +9,7 @@ import { type Block, parseWriteUp, type Span, withoutTitle, writeUpExcerpt } fro
9
9
  import { type Attachment, attachmentIcon } from "./attachments";
10
10
  import { PressableRow } from "./row";
11
11
  import { errorText } from "./errors";
12
+ import { useProgressLanguage } from "./language";
12
13
 
13
14
  type Colors = PluginWorkspacePanelProps["theme"]["colors"];
14
15
  const MONO = Platform.OS === "ios" ? "Menlo" : "monospace";
@@ -22,6 +23,7 @@ export function WriteUp({ colors, source, workspaceId, workspaceDirectory, onOpe
22
23
  workspaceDirectory: string;
23
24
  onOpen(attachment: Attachment, group: Attachment[]): void;
24
25
  }) {
26
+ const { messages: m } = useProgressLanguage();
25
27
  const fetchPreview = useRpc(previewDeliverable);
26
28
  const readable = Boolean(source.path && previewKind(source.path) === "text");
27
29
  // The same query as the preview dialog, so opening the file after reading here is instant.
@@ -38,12 +40,12 @@ export function WriteUp({ colors, source, workspaceId, workspaceDirectory, onOpe
38
40
  return (
39
41
  <View style={{ gap: 12, padding: 14, borderRadius: 8, backgroundColor: colors.surface0 }}>
40
42
  {shown.length ? <View style={{ gap: 8 }}>{shown.map((block, index) => <BlockView key={index} colors={colors} block={block} first={index === 0} />)}</View> : null}
41
- {preview.error ? <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>Couldn't read it here: {errorText(preview.error, "something went wrong")}</Text> : null}
43
+ {preview.error ? <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>{m.readError(errorText(preview.error, m.unknownError))}</Text> : null}
42
44
  {/* The file on the left, opening it whole; expanding in place on the right. */}
43
45
  <View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
44
- <TextButton colors={colors} icon={attachmentIcon(source)} label={source.title} accessibilityLabel={`Open ${source.title}`} onPress={() => onOpen(source, [source])} grow />
46
+ <TextButton colors={colors} icon={attachmentIcon(source)} label={source.title} accessibilityLabel={m.openItem(source.title)} onPress={() => onOpen(source, [source])} grow />
45
47
  <View style={{ flex: 1 }} />
46
- {excerpt.more ? <TextButton colors={colors} label={all ? "Show less" : "Read more"} onPress={() => setAll(!all)} expanded={all} /> : null}
48
+ {excerpt.more ? <TextButton colors={colors} label={all ? m.showLess : m.readMore} onPress={() => setAll(!all)} expanded={all} /> : null}
47
49
  </View>
48
50
  </View>
49
51
  );
Binary file
package/index.client.tsx CHANGED
@@ -2,6 +2,7 @@ import type { PluginClientContext } from "@getpaseo/plugin/client";
2
2
  import { ProgressPanel } from "./client/panel";
3
3
  import { contributePills, progressPanel } from "./client/pills";
4
4
  import { QuestionSettings } from "./client/settings";
5
+ import { ProgressLanguageProvider } from "./client/language";
5
6
 
6
7
  const SETTINGS_ID = "settings";
7
8
 
@@ -14,7 +15,7 @@ export default function contribute(client: PluginClientContext) {
14
15
  icon: "ListChecks",
15
16
  context: "workspace",
16
17
  locations: [panel.location],
17
- Component: (props) => <ProgressPanel {...props} openSettings={() => client.openSettings(SETTINGS_ID)} />,
18
+ Component: (props) => <ProgressLanguageProvider><ProgressPanel {...props} openSettings={() => client.openSettings(SETTINGS_ID)} /></ProgressLanguageProvider>,
18
19
  });
19
20
  const stopCommand = client.addCommandCenterItem({
20
21
  id: "open-progress",
@@ -32,7 +33,7 @@ export default function contribute(client: PluginClientContext) {
32
33
  id: SETTINGS_ID,
33
34
  title: "Settings",
34
35
  icon: "Settings",
35
- Component: QuestionSettings,
36
+ Component: (props) => <ProgressLanguageProvider><QuestionSettings {...props} /></ProgressLanguageProvider>,
36
37
  });
37
38
 
38
39
  return () => {
package/index.server.ts CHANGED
@@ -6,7 +6,7 @@ import { mirrorQuestionMode } from "./server/question-mode";
6
6
  import { handleInstallSkill, handleSkillStatus } from "./server/skill";
7
7
  import { handleDismissWhatsNew, handleGetWhatsNew } from "./server/whats-new";
8
8
  import { canOpenOnHost, dismissWhatsNew, getAttention, getDashboard, getLauncherStatus, getSkillStatus, getWhatsNew, installLauncher, installSkill, listFolders, markPanelOpened, openDeliverable, previewDeliverable, readDeliverablePiece, setHistoryFolder } from "./shared/rpc";
9
- import { questionSettings } from "./shared/settings";
9
+ import { displaySettings, questionSettings } from "./shared/settings";
10
10
 
11
11
  export default function contribute(server: PluginServerContext) {
12
12
  server.handle(getDashboard, handleGetDashboard);
@@ -27,6 +27,7 @@ export default function contribute(server: PluginServerContext) {
27
27
  // Settles a new install before any panel opens. Opening one makes a
28
28
  // worktree folder, which would look like an earlier version's.
29
29
  void handleGetWhatsNew().catch(() => {});
30
+ server.registerSettings(displaySettings);
30
31
  const mirror = mirrorQuestionMode(server.registerSettings(questionSettings));
31
32
  return mirror.stop;
32
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stevecastaneda/paseo-progress",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "A live progress dashboard per worktree for Paseo, drawn from a file agents write with the paseo-progress command.",
5
5
  "license": "MIT",
6
6
  "author": "Steve Castaneda",
@@ -8,3 +8,13 @@ export const questionSettings = defineSettings({
8
8
  version: 1,
9
9
  schema: z.object({ mode: z.enum(QUESTION_MODES).default(DEFAULT_QUESTION_MODE) }),
10
10
  });
11
+
12
+ export const LANGUAGE_PREFERENCES = ["system", "en", "zh-CN"] as const;
13
+ export type LanguagePreference = (typeof LANGUAGE_PREFERENCES)[number];
14
+
15
+ export const displaySettings = defineSettings({
16
+ id: "display",
17
+ scope: "host",
18
+ version: 1,
19
+ schema: z.object({ language: z.enum(LANGUAGE_PREFERENCES).default("system") }),
20
+ });
@@ -1,12 +1,19 @@
1
1
  import type { Activity, Deliverable, Question, Ticket } from "./dashboard.ts";
2
2
  import type { TicketStatus } from "./events.ts";
3
3
 
4
+ export type TimelineLabel =
5
+ | { kind: "added" }
6
+ | { kind: "reset" }
7
+ | { kind: "stage"; stage: string }
8
+ | { kind: "status"; status: TicketStatus }
9
+ | { kind: "waiting"; id: string };
10
+
4
11
  // One span in a ticket's life: from a status or stage change to the next one.
5
12
  export interface TimelineStep {
6
13
  at: string;
7
14
  // When the next step began; null while this one is still going (or it's the end).
8
15
  until: string | null;
9
- label: string;
16
+ label: TimelineLabel;
10
17
  status: TicketStatus;
11
18
  notes: string[];
12
19
  // Length of the step; null for a finished ticket's last step, which has no end.
@@ -27,15 +34,10 @@ export interface TicketStory {
27
34
 
28
35
  const OPEN_ENDED: TicketStatus[] = ["working", "blocked", "not_started"];
29
36
 
30
- function labelFor(status: TicketStatus, stage: string | undefined, first: boolean): string {
31
- if (first && status === "not_started") return "Added";
32
- switch (status) {
33
- case "working": return stage ? `${stage} stage` : "Working";
34
- case "blocked": return "Blocked";
35
- case "done": return "Done";
36
- case "skipped": return "Skipped";
37
- default: return "Back to not started";
38
- }
37
+ function labelFor(status: TicketStatus, stage: string | undefined, first: boolean): TimelineLabel {
38
+ if (first && status === "not_started") return { kind: "added" };
39
+ if (status === "working" && stage) return { kind: "stage", stage };
40
+ return status === "not_started" ? { kind: "reset" } : { kind: "status", status };
39
41
  }
40
42
 
41
43
  export function ticketTimeline(ticket: Ticket, nowMs: number): TimelineStep[] {