@stevecastaneda/paseo-progress 1.3.1 → 1.5.0

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 (50) hide show
  1. package/OVERVIEW.md +1 -1
  2. package/README.md +28 -4
  3. package/client/attachments.tsx +9 -6
  4. package/client/deliverables.tsx +6 -4
  5. package/client/dialog-stack.tsx +3 -0
  6. package/client/history.tsx +23 -18
  7. package/client/i18n.ts +86 -0
  8. package/client/language.tsx +23 -0
  9. package/client/launcher.tsx +22 -17
  10. package/client/locales/en.ts +207 -0
  11. package/client/locales/zh-CN.ts +207 -0
  12. package/client/messages.ts +189 -0
  13. package/client/panel.tsx +77 -47
  14. package/client/pill-popover.tsx +8 -5
  15. package/client/pill-state.ts +4 -3
  16. package/client/pills.tsx +25 -11
  17. package/client/preview.tsx +18 -13
  18. package/client/questions.tsx +42 -18
  19. package/client/settings.tsx +58 -0
  20. package/client/show-more.tsx +4 -2
  21. package/client/spinner.tsx +7 -3
  22. package/client/status-badge.tsx +3 -9
  23. package/client/surfaces.ts +20 -8
  24. package/client/ticket-dialog.tsx +37 -30
  25. package/client/video.tsx +3 -1
  26. package/client/whats-new.tsx +125 -0
  27. package/client/when.tsx +5 -3
  28. package/client/write-up.tsx +8 -5
  29. package/dist/server/cli.js +85 -20
  30. package/images/1-panel.png +0 -0
  31. package/images/3-question.png +0 -0
  32. package/images/5-settings.png +0 -0
  33. package/index.client.tsx +13 -1
  34. package/index.server.ts +12 -2
  35. package/package.json +1 -1
  36. package/paseo-plugin.json +2 -1
  37. package/server/cli.ts +28 -11
  38. package/server/question-mode.ts +61 -0
  39. package/server/store.ts +21 -4
  40. package/server/whats-new.ts +127 -0
  41. package/shared/dashboard.ts +9 -2
  42. package/shared/events.ts +9 -4
  43. package/shared/question-modes.ts +45 -0
  44. package/shared/rpc.ts +18 -0
  45. package/shared/settings.ts +20 -0
  46. package/shared/show.ts +3 -1
  47. package/shared/ticket-story.ts +12 -10
  48. package/shared/whats-new.ts +16 -0
  49. package/shared/write-up.ts +9 -8
  50. package/skills/paseo-progress/SKILL.md +26 -68
package/OVERVIEW.md CHANGED
@@ -2,7 +2,7 @@ Progress gives each worktree a live dashboard for long agent jobs. The agent rec
2
2
 
3
3
  The panel shows how much of the estimated work is done, what the agent is doing right now, and where each ticket stands. A ticket turns stuck when it's blocked or runs past its estimate without an update. Press a ticket to see its timeline, activity, questions and deliverables. Images, videos and text files the agent records open in a preview inside Paseo, and a folder opens as a list of its files. Videos play only in the desktop app. Any other file opens in an app on the computer running the Paseo daemon, and so does a previewed file when you press Open. Open is hidden when that computer can't open files, like a Linux server with no desktop. That's true from your phone too. The app opens on that computer, not on the phone.
4
4
 
5
- When the agent needs a decision, it asks a numbered question with lettered options and says which one it recommends. A pill above the message box counts open questions and stuck work. Each option has a Copy button. Paste the answer into the chat and the agent records it.
5
+ When the agent needs a decision, it asks a numbered question with lettered options and says which one it recommends. A pill above the message box counts open questions and stuck work. Each option has a Copy button. Paste the answer into the chat and the agent records it. In Settings you choose what the agent does while it waits: stop, work around the question, or keep building on its recommendation.
6
6
 
7
7
  Nothing updates unless the agent runs the command, so agents need the plugin's skill. The panel has two setup buttons, and each one shows where it will write before you press it:
8
8
 
package/README.md CHANGED
@@ -13,12 +13,14 @@ The dashboard shows:
13
13
  - percent of estimated work done, time done of time estimated (like "1h 30m of 2h"; once everything is done, how long it really took), and a bar with one segment per ticket
14
14
  - **Stuck:** blocked tickets, working tickets with no update for longer than their estimate, and blockers the agent flags
15
15
  - **Tickets:** estimate, status, and the working ticket's stage. A ticket that waits for another (`--waits-for T03`) shows "Waits for Ticket 03" under its title until that one is done; waiting is the order of work, so it never counts as stuck, even if the agent also marked it blocked. Press a ticket for its story in a dialog. A summary on top shows its status and stage, time worked against the estimate as a bar that turns red past it, the agent's latest status line while it's working, and the agent on it. Tabs below hold the rest, each only when it has something: the ticket's write-up when it has a `--source` file (the start of it, with the rest a press away), a timeline of its stages and statuses with how long each took, and the activity, questions and deliverables that belong to it. The agent on a ticket shows by its chat title; pressing it opens that chat. Items tagged with `--ticket` belong exactly; older, untagged ones are matched by when they happened and marked "by time"
16
- - **Questions:** each with a permanent reference (Q1, Q2, ...), lettered options, and the option the agent recommends. The agent builds nothing a question affects until you answer; it works on other things meanwhile, and stops when nothing else is left. Press a question to open it in a dialog, then press **Copy Q2 B** to copy `Q2 (title): B` for your reply. Files and links the agent attached (`--file`) open like deliverables: images, videos and text files preview in Paseo (with **Back** to the question), a folder lists its files there, links open in Paseo's browser, and other files open in their default app. From the pill's popover, the preview opens over the popover, which stays open so you can go back to it. Answers stay listed under the same reference.
16
+ - **Questions:** each with a permanent reference (Q1, Q2, ...), lettered options, and the option the agent recommends. You pick what the agent does while you decide in Paseo's Settings (Settings, Plugins, Progress, Settings). **Stop**: it stops all work until you answer. **Work around**, the default: it holds what your answer affects and keeps working on the rest. **Keep building**: it builds its recommendation and changes course if you pick another, except for big questions, which wait. A question is big when building ahead would send something off your computer, when the answer sets something people or programs outside the repo rely on, or when most of the remaining work would be built on it. Each open question shows what the agent is doing: "Agent stopped until you answer", "Waiting for you" (with the reason when it's big), or "Building with B: <its label>". Press a question to open it in a dialog, then press **Copy Q2 B** to copy `Q2 (title): B` for your reply. Files and links the agent attached (`--file`) open like deliverables: images, videos and text files preview in Paseo (with **Back** to the question), a folder lists its files there, links open in Paseo's browser, and other files open in their default app. From the pill's popover, the preview opens over the popover, which stays open so you can go back to it. Answers stay listed under the same reference.
17
17
  - **Activity**, **Answered** questions and **Deliverables** (press one to open it), sharing one card with a tab for each; the panel remembers the tab you picked. Each shows the newest 10, with **Show 10 older** below
18
18
 
19
19
  <img src="images/2-ticket.png" alt="A ticket's story: its status, time against the estimate, the agent on it, and its timeline" width="400"> <img src="images/3-question.png" alt="A question waiting for an answer, with lettered options, the agent's recommendation and Copy buttons" width="400">
20
20
 
21
- The panel lives in Explorer, beside the agent chat, so you can watch both. Open it from Command Center with **Open Progress**. When questions are waiting or something is stuck, a pill above the message box says so, for example `3 questions`. With something stuck too, it shortens to `3Q · 1 stuck` to fit. Pressing it opens a popover with each open question, its choices and their Copy buttons, and what is stuck, so you can answer without leaving the chat. **Open Progress** at the bottom opens the panel in Explorer.
21
+ <img src="images/5-settings.png" alt="The Questions setting in Paseo's Settings, set to Work around" width="600">
22
+
23
+ The panel lives in Explorer, beside the agent chat, so you can watch both. Open it from Command Center with **Open Progress**. The cog at the top right of the panel opens the plugin's Settings. After an update, the panel shows what's new in that version once. When questions are waiting or something is stuck, a pill above the message box says so, for example `3 questions`. With something stuck too, it shortens to `3Q · 1 stuck` to fit. Pressing it opens a popover with each open question, its choices and their Copy buttons, and what is stuck, so you can answer without leaving the chat. **Open Progress** at the bottom opens the panel in Explorer.
22
24
 
23
25
  <img src="images/4-pill.png" alt="The pill above the message box, showing two questions waiting" width="600">
24
26
 
@@ -56,6 +58,14 @@ The command, and **Install skill** in the panel, look for Paseo's own command in
56
58
 
57
59
  Setting `PASEO_PROGRESS_PLUGIN_DIR` to a plugin folder makes the command run that copy without asking Paseo. Use it only for testing, because an installed plugin's folder changes with every update.
58
60
 
61
+ ## Language
62
+
63
+ Progress comes in English and Simplified Chinese. To pick one, open the cog in the Progress panel and choose **Display → Language**. **System** follows each device's language and falls back to English for anything other than Simplified Chinese, so Traditional Chinese stays in English for now.
64
+
65
+ The choice is saved on the Paseo host, so every device connected to it shows the same language. Progress can't follow Paseo's own language setting, because Paseo doesn't share it with plugins. Open dialogs and pills switch as soon as you change it. The **Progress** name and its **Open Progress** and **Settings** entries stay in English.
66
+
67
+ Progress translates its own labels, not what agents write. Titles, questions, choices, stages, notes, item labels like `Ticket`, file paths and release notes stay as written, and so do error messages from the host. The `paseo-progress` command and the reply you copy for an agent stay in English too.
68
+
59
69
  ## Where progress is kept
60
70
 
61
71
  Each worktree's progress lives outside the repo, in its own folder under `~/.local/state/paseo-progress/` on the daemon host. There's nothing to set up and nothing to hide: git never sees these files, and the plugin never edits a `.gitignore` or git's exclude file. The panel's empty state shows the exact path.
@@ -72,7 +82,7 @@ Earlier versions kept progress in `.scratch/` in each worktree, hidden by a `.sc
72
82
 
73
83
  ## Agent skill
74
84
 
75
- The plugin ships an agent skill, `paseo-progress`, that tells agents when to run each command during a multi-ticket job: set up the run, move tickets through their stages, ask questions without stopping, flag stuck work, and finish. Press **Install skill** in the Progress panel. It links `paseo-progress` into `~/.agents/skills`, `~/.claude/skills`, and `~/.codex/skills` (the folders Paseo installs its own skills into), pointing at the copy of the plugin Paseo runs, so plugin updates reach agents. On Windows the links are junctions, which don't need admin rights or Developer Mode. If Paseo later runs the plugin from somewhere else, the button changes to **Update skill**. It never replaces a skill it didn't link; that folder is skipped.
85
+ The plugin ships an agent skill, `paseo-progress`, that tells agents when to run each command during a multi-ticket job: set up the run, move tickets through their stages, ask questions, and finish. It stays short and leaves the details to `paseo-progress --help`. Press **Install skill** in the Progress panel. It links `paseo-progress` into `~/.agents/skills`, `~/.claude/skills`, and `~/.codex/skills` (the folders Paseo installs its own skills into), pointing at the copy of the plugin Paseo runs, so plugin updates reach agents. On Windows the links are junctions, which don't need admin rights or Developer Mode. If Paseo later runs the plugin from somewhere else, the button changes to **Update skill**. It never replaces a skill it didn't link; that folder is skipped.
76
86
 
77
87
  From a terminal instead:
78
88
 
@@ -108,7 +118,8 @@ paseo-progress finish "Snapshots ship for all four tables"
108
118
  - Statuses are `not_started`, `working`, `blocked`, `done`, and `skipped`. Skipped tickets leave the totals.
109
119
  - `ticket add` and `ticket update` take an optional `--source <path or link>[=<label>]`: where the ticket is written up, like its spec file or issue. It shows under the ticket's status in its dialog and opens like a deliverable. `--source ""` clears it. Tickets without one are fine.
110
120
  - A working ticket is stuck once it goes longer than its estimate without an update, timed from its last status, stage or note change, or activity tagged to it. The ticket's dialog still shows total time worked against the estimate.
111
- - `question ask` needs `--recommend`, the agent's pick. The question dialog tags that option "Recommended".
121
+ - `question ask` needs `--recommend`, the agent's pick. The question dialog tags that option "Recommended". Its output tells the agent the question mode and what to do until the answer. `--big "<why>"` marks a big question, which waits for the answer even in Keep building. `start` and `show` also print the mode.
122
+ - Paseo keeps the question mode in its own settings, which only the plugin's server can read, so the server copies it to `~/.local/state/paseo-progress/settings.json` for the command. Without that file, for example while the plugin is off, the command uses Work around.
112
123
  - Deliverable paths are stored relative to the worktree root. Web links open in Paseo's browser. Pressing an image (PNG, JPEG, GIF, WebP, up to 10 MB) or text deliverable (Markdown, text, logs, JSON, YAML, CSV; the first 256 KB) previews it in a dialog inside Paseo, which also works from a phone; **Open** there opens the file in its default app. **Open** shows only when the computer running Paseo can open files. A Linux server with no desktop can't, so there a file Paseo can't preview copies its path and says why. A video (MP4, MOV, WebM, up to 200 MB) plays in the same dialog in the desktop app. When there are several files, **Previous** and **Next** step through them, and so do the left and right arrow keys. Pressing a folder lists its files in the same dialog. Press a file to preview it, or a folder inside to list that one, and **Back** returns to the list. Pressing any other local deliverable opens it with its default app on the machine running the Paseo daemon (for example HTML in your browser). On macOS, **Open** sends text files to your default browser. Only recorded deliverables inside the worktree open this way; otherwise the path is copied. The copy icon on each row copies the path.
113
124
  - Several agents can share one run. When another session adds a ticket, each agent sees it at the end of its next command's output (and in `show`), and the skill has it work that ticket in order after its current one. An agent only hears about tickets added since its own last update in the run, and only once.
114
125
  - Subagents record their own progress. Since Paseo 0.11, an agent waiting on background subagents shows as idle, so only the subagents' updates show the work is still going. The skill has the agent give each subagent the worktree path and ticket id, and the subagent records its stages and a ticker line on that worktree's dashboard.
@@ -134,6 +145,19 @@ The progress file, `progress.jsonl`, holds one JSON event per line, each with a
134
145
 
135
146
  ## Changelog
136
147
 
148
+ ### 1.5.0
149
+
150
+ - [New] **Simplified Chinese.** Progress now comes in English and Simplified Chinese. Pick one under Display in Progress settings, or let it follow each device's language. Thanks to @xzthemeofsss for the translation.
151
+
152
+ ### 1.4.0
153
+
154
+ - [New] **Choose what agents do while a question waits.** Open Settings with the cog at the top right of the Progress panel. **Stop** halts all work until you answer. **Work around**, the default, holds only what your answer affects, which is how questions already worked. **Keep building** builds the agent's pick and changes course if you choose another.
155
+ - [New] **Big questions always wait.** In Keep building, the agent still waits when building ahead would send something off your computer, set something people outside the repo rely on, or carry most of the remaining work. The question says which.
156
+ - [Improved] **Every open question shows what the agent is doing.** It reads "Agent stopped until you answer", "Waiting for you", or "Building with B" and the name of option B.
157
+ - [Improved] **A shorter agent skill.** It's about half as long, and agents get the rule for your mode each time they ask.
158
+ - [New] **What's new, once per update.** After an update, a banner in the panel shows that version's changes. Closing it hides it until the next version.
159
+ - [Fixed] **Bold text in ticket write-ups.** Words marked bold in a ticket's write-up used to show as plain text.
160
+
137
161
  ### 1.3.1
138
162
 
139
163
  - Progress works on Windows (#84). Before, `paseo-progress` stopped with "The progress plugin is not installed in Paseo", and **Install skill** couldn't find the plugin. On Windows, Paseo's command is `paseo.cmd`, and Node can start a `.cmd` file only through cmd. Both now go through cmd on Windows, and both also check where Paseo installs its command.
@@ -6,6 +6,7 @@ import { type Attachment, attachmentIcon, canPreview, shortName } from "../share
6
6
  import { errorText } from "./errors";
7
7
  import { PressableRow } from "./row";
8
8
  import { openDeliverable } from "../shared/rpc";
9
+ import { useProgressLanguage } from "./language";
9
10
 
10
11
  export { type Attachment, attachmentIcon, deliverableAttachment, questionAttachments, shortName, ticketSourceAttachment } from "../shared/attachments";
11
12
 
@@ -31,22 +32,23 @@ export function useAttachmentOpener({ workspaceId, workspaceDirectory, navigatio
31
32
  navigation?: Navigation;
32
33
  onPreview?(attachment: Attachment): void;
33
34
  }) {
35
+ const { messages: m } = useProgressLanguage();
34
36
  const toast = useToast();
35
37
  const openOnHostRpc = useRpc(openDeliverable);
36
38
  async function copyPath(attachment: Attachment, prefix = "") {
37
39
  try {
38
40
  await copyText(absolutePath(attachment.path!, workspaceDirectory));
39
- toast.show(`${prefix}Copied path`, { variant: prefix ? "warning" : "success" });
41
+ toast.show(`${prefix}${m.copyPathDone}`, { variant: prefix ? "warning" : "success" });
40
42
  } catch {
41
- toast.error("Could not copy. Select the path and use Copy.");
43
+ toast.error(m.copyPathFailure);
42
44
  }
43
45
  }
44
46
  async function openOnHost(attachment: Attachment) {
45
47
  try {
46
48
  await openOnHostRpc({ workspaceId, workspaceDirectory, ref: attachment.ref });
47
- toast.show(`Opened ${attachment.title}`, { variant: "success" });
49
+ toast.show(m.opened(attachment.title), { variant: "success" });
48
50
  } catch (error) {
49
- await copyPath(attachment, `${errorText(error, "Could not open it")}. `);
51
+ await copyPath(attachment, `${errorText(error, m.openFailure)}. `);
50
52
  }
51
53
  }
52
54
  async function open(attachment: Attachment) {
@@ -55,7 +57,7 @@ export function useAttachmentOpener({ workspaceId, workspaceDirectory, navigatio
55
57
  if (navigation?.openBrowser) navigation.openBrowser({ url: attachment.url, workspaceId });
56
58
  else await openExternalUrl(attachment.url);
57
59
  } catch {
58
- toast.error("Could not open this link.");
60
+ toast.error(m.linkFailure);
59
61
  }
60
62
  return;
61
63
  }
@@ -74,11 +76,12 @@ export const ATTACHMENT_ROW = { gap: 2, paddingVertical: 3, lineHeight: 17 };
74
76
  // Drawn in dialogs and the popover (surface1) unless `onSurface1` is false.
75
77
  // `onOpen` also gets the whole list, so a preview can step through its siblings.
76
78
  export function AttachmentList({ colors, attachments, onOpen, onSurface1 = true }: { colors: Colors; attachments: Attachment[]; onOpen(attachment: Attachment, group: Attachment[]): void; onSurface1?: boolean }) {
79
+ const { messages: m } = useProgressLanguage();
77
80
  if (!attachments.length) return null;
78
81
  return (
79
82
  <View style={{ gap: ATTACHMENT_ROW.gap }}>
80
83
  {attachments.map((attachment) => (
81
- <PressableRow key={attachment.ref} colors={colors} onSurface1={onSurface1} accessibilityRole={attachment.url ? "link" : "button"} accessibilityLabel={`Open ${attachment.title}`}
84
+ <PressableRow key={attachment.ref} colors={colors} onSurface1={onSurface1} accessibilityRole={attachment.url ? "link" : "button"} accessibilityLabel={m.openItem(attachment.title)}
82
85
  onPress={() => onOpen(attachment, attachments)}
83
86
  style={{ flexDirection: "row", gap: 6, alignItems: "center", paddingVertical: ATTACHMENT_ROW.paddingVertical, paddingHorizontal: 4, marginHorizontal: -4, borderRadius: 4 }}>
84
87
  {/* A 14px slot, like a section icon, so the title lines up with the heading above. */}
@@ -4,12 +4,13 @@ import React, { useState } from "react";
4
4
  import { Text, View } from "react-native";
5
5
  import type { Deliverable } from "../shared/dashboard";
6
6
  import { type Attachment, attachmentIcon, deliverableAttachment, shortName, useAttachmentOpener } from "./attachments";
7
- import { formatAgo } from "../shared/format";
7
+ import { formatClientAgo } from "./i18n";
8
8
  import { PressScale } from "./motion";
9
9
  import { StackedDialog } from "./dialog-stack";
10
10
  import { galleryFiles } from "./preview";
11
11
  import { PressableRow } from "./row";
12
12
  import { ShowMoreRow, usePaged } from "./show-more";
13
+ import { useProgressLanguage } from "./language";
13
14
 
14
15
  export { absolutePath } from "./attachments";
15
16
 
@@ -25,6 +26,7 @@ export function DeliverablesList({ colors, deliverables, now, workspaceId, works
25
26
  workspaceDirectory: string;
26
27
  navigation: Navigation;
27
28
  }) {
29
+ const { messages: m, locale } = useProgressLanguage();
28
30
  const [previewing, setPreviewing] = useState<Attachment | null>(null);
29
31
  const { open, copyPath } = useAttachmentOpener({ workspaceId, workspaceDirectory, navigation, onPreview: setPreviewing });
30
32
  const paged = usePaged(deliverables);
@@ -32,7 +34,7 @@ export function DeliverablesList({ colors, deliverables, now, workspaceId, works
32
34
  <View>
33
35
  {paged.shown.map((deliverable, index) => (
34
36
  <PressableRow key={deliverable.id} colors={colors} accessibilityRole={deliverable.url ? "link" : "button"}
35
- accessibilityLabel={`Open ${deliverable.title}`}
37
+ accessibilityLabel={m.openItem(deliverable.title)}
36
38
  onPress={() => void open(deliverableAttachment(deliverable))}
37
39
  style={{ flexDirection: "row", gap: 8, paddingHorizontal: 10, paddingVertical: 8, borderTopWidth: index ? 1 : 0, borderTopColor: colors.border }}>
38
40
  {/* Optical: centers the 14px icon on the 18px title line. */}
@@ -42,12 +44,12 @@ export function DeliverablesList({ colors, deliverables, now, workspaceId, works
42
44
  <Text numberOfLines={3} style={{ color: colors.foreground, fontSize: 13, lineHeight: 18 }}>{deliverable.title}</Text>
43
45
  {/* Just the name, ticket and age. The full path is one press of Copy away. */}
44
46
  <Text numberOfLines={1} style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
45
- {[shortName(deliverableAttachment(deliverable)), deliverable.ticketLabel ?? deliverable.ticketId, formatAgo(deliverable.addedAt, now)].filter(Boolean).join(" · ")}
47
+ {[shortName(deliverableAttachment(deliverable)), deliverable.ticketLabel ?? deliverable.ticketId, formatClientAgo(locale, deliverable.addedAt, now)].filter(Boolean).join(" · ")}
46
48
  </Text>
47
49
  </View>
48
50
  {deliverable.path ? (
49
51
  // Optical: the 22px button centers on the 18px title line.
50
- <PressScale accessibilityRole="button" accessibilityLabel={`Copy the path of ${deliverable.title}`} hitSlop={6} outerStyle={{ marginTop: -2 }}
52
+ <PressScale accessibilityRole="button" accessibilityLabel={m.copyItemPath(deliverable.title)} hitSlop={6} outerStyle={{ marginTop: -2 }}
51
53
  onPress={() => void copyPath(deliverableAttachment(deliverable))}
52
54
  // A step above the row's own hover tint (surface1), so it reads as its own target.
53
55
  style={({ pressed, hovered }) => ({ padding: 4, borderRadius: 4, backgroundColor: pressed ? colors.border : hovered ? colors.surface2 : "transparent" })}>
@@ -7,6 +7,7 @@ import { NarrowProvider } from "./narrow";
7
7
  import { BackButton, galleryFiles, PreviewContent } from "./preview";
8
8
  import { QuestionView, useCopy } from "./questions";
9
9
  import { TicketStoryView } from "./ticket-dialog";
10
+ import { ProgressLanguageProvider } from "./language";
10
11
 
11
12
  type Colors = PluginWorkspacePanelProps["theme"]["colors"];
12
13
  type AttachmentContext = { workspaceId: string; workspaceDirectory: string; navigation?: PluginWorkspacePanelProps["navigation"] };
@@ -76,6 +77,7 @@ export function StackedDialog({ colors, root, onClose, questions = [], dashboard
76
77
  open={Boolean(root)} onOpenChange={(next) => { if (!next) onClose(); }}>
77
78
  {/* One Modal.Content for every step: replacing it would re-present the sheet on phones. */}
78
79
  <Modal.Content scrollable={!(preview && previewLong)}>
80
+ <ProgressLanguageProvider>
79
81
  {/* A dialog has room for clock times, however narrow the panel that opened it. */}
80
82
  <NarrowProvider value={false}>
81
83
  {preview ? (
@@ -100,6 +102,7 @@ export function StackedDialog({ colors, root, onClose, questions = [], dashboard
100
102
  </>
101
103
  )}
102
104
  </NarrowProvider>
105
+ </ProgressLanguageProvider>
103
106
  </Modal.Content>
104
107
  </Modal>
105
108
  );
@@ -7,6 +7,7 @@ import { PressScale } from "./motion";
7
7
  import { PressableRow } from "./row";
8
8
  import { listFolders, setHistoryFolder } from "../shared/rpc";
9
9
  import { errorText } from "./errors";
10
+ import { ProgressLanguageProvider, useProgressLanguage } from "./language";
10
11
 
11
12
  type Colors = PluginWorkspacePanelProps["theme"]["colors"];
12
13
 
@@ -18,6 +19,7 @@ export function HistoryRow({ colors, workspaceId, directory, savedTo }: {
18
19
  directory: string;
19
20
  savedTo: string | null;
20
21
  }) {
22
+ const { messages: m } = useProgressLanguage();
21
23
  const [picking, setPicking] = useState(false);
22
24
  const set = useRpc(setHistoryFolder);
23
25
  const queryClient = useQueryClient();
@@ -26,22 +28,22 @@ export function HistoryRow({ colors, workspaceId, directory, savedTo }: {
26
28
  mutationFn: (folder: string | null) => set({ workspaceId, workspaceDirectory: directory, folder }),
27
29
  onSuccess: ({ savedTo: next }) => {
28
30
  queryClient.invalidateQueries({ queryKey: ["progress", "dashboard"] });
29
- toast.show(next ? `Each run will be saved to ${next}.` : "Runs will stay on this computer. Files already saved stay in the repo.", { variant: "success" });
31
+ toast.show(next ? m.historySaved(next) : m.historyStopped, { variant: "success" });
30
32
  },
31
- onError: (error) => toast.error(errorText(error, "Could not change where runs are saved.")),
33
+ onError: (error) => toast.error(errorText(error, m.historyFailure)),
32
34
  });
33
35
  // While saving, the button that started it says so.
34
36
  const busy = mutation.isPending ? (mutation.variables === null ? "stop" : "pick") : null;
35
37
  const actions: { label: string; busyLabel: string; kind: "pick" | "stop"; onPress(): void }[] = savedTo
36
- ? [{ label: "Change…", busyLabel: "Saving…", kind: "pick", onPress: () => setPicking(true) }, { label: "Stop saving", busyLabel: "Stopping…", kind: "stop", onPress: () => mutation.mutate(null) }]
37
- : [{ label: "Save to repo…", busyLabel: "Saving…", kind: "pick", onPress: () => setPicking(true) }];
38
+ ? [{ label: m.changeFolder, busyLabel: m.saving, kind: "pick", onPress: () => setPicking(true) }, { label: m.stopSaving, busyLabel: m.stopping, kind: "stop", onPress: () => mutation.mutate(null) }]
39
+ : [{ label: m.saveToRepo, busyLabel: m.saving, kind: "pick", onPress: () => setPicking(true) }];
38
40
 
39
41
  return (
40
42
  <View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "center", columnGap: 8, rowGap: 4, marginHorizontal: 12, marginTop: 16 }}>
41
43
  <View style={{ flexDirection: "row", alignItems: "flex-start", gap: 6, flex: 1, minWidth: 180 }}>
42
44
  <View style={{ paddingTop: 2 }}><Icon name="History" size={13} color={colors.foregroundMuted} /></View>
43
45
  <Text style={{ flex: 1, color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
44
- {savedTo ? <>Each run is saved to <Text style={{ color: colors.foreground }}>{savedTo}</Text> in this repo.</> : "Run history stays on this computer."}
46
+ {savedTo ? <>{m.historyInRepo[0]}<Text style={{ color: colors.foreground }}>{savedTo}</Text>{m.historyInRepo[1]}</> : m.localHistory}
45
47
  </Text>
46
48
  </View>
47
49
  <View style={{ flexDirection: "row", gap: 4 }}>
@@ -71,6 +73,7 @@ function FolderPicker({ colors, workspaceId, directory, open, start, onClose, on
71
73
  onClose(): void;
72
74
  onPick(folder: string): void;
73
75
  }) {
76
+ const { messages: m } = useProgressLanguage();
74
77
  const parentOf = (path: string) => path.split("/").slice(0, -1).join("/");
75
78
  const [at, setAt] = useState(() => parentOf(start));
76
79
  const [naming, setNaming] = useState<string | null>(null);
@@ -86,15 +89,16 @@ function FolderPicker({ colors, workspaceId, directory, open, start, onClose, on
86
89
  const newName = naming?.trim().replace(/\/+$/, "") ?? "";
87
90
 
88
91
  return (
89
- <Modal title="Save run history to a folder" icon={<Icon name="FolderOpen" size={16} color={colors.foregroundMuted} />} open={open} onOpenChange={(next) => { if (!next) close(); }}>
92
+ <Modal title={m.historyFolderTitle} icon={<Icon name="FolderOpen" size={16} color={colors.foregroundMuted} />} open={open} onOpenChange={(next) => { if (!next) close(); }}>
90
93
  {/* Paseo sizes a non-scrolling dialog to 85% of the window, so let it size to
91
94
  its content; the list's own height cap keeps the buttons in view. */}
92
95
  <Modal.Content>
96
+ <ProgressLanguageProvider>
93
97
  <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
94
- Each run becomes its own file in this folder, in every worktree of this repo. Commit them like any other file.
98
+ {m.historyFolderHint}
95
99
  </Text>
96
100
  <View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "center", gap: 2, paddingHorizontal: 4 }}>
97
- {["Top of the repo", ...crumbs].map((crumb, index) => (
101
+ {[m.repoTop, ...crumbs].map((crumb, index) => (
98
102
  <React.Fragment key={index}>
99
103
  {index ? <Icon name="ChevronRight" size={12} color={colors.foregroundMuted} /> : null}
100
104
  <PressScale accessibilityRole="button" disabled={index === crumbs.length} onPress={() => { setNaming(null); setAt(crumbs.slice(0, index).join("/")); }}
@@ -108,9 +112,9 @@ function FolderPicker({ colors, workspaceId, directory, open, start, onClose, on
108
112
 
109
113
  {/* Only the list scrolls, so Use and Cancel stay in view in a big repo. */}
110
114
  <ScrollView style={{ maxHeight: 320, flexGrow: 0 }} contentContainerStyle={{ gap: 2 }}>
111
- {listed.isPending ? <ActivityIndicator color={colors.foregroundMuted} accessibilityLabel="Loading folders" style={{ alignSelf: "flex-start", padding: 8 }} /> : null}
115
+ {listed.isPending ? <ActivityIndicator color={colors.foregroundMuted} accessibilityLabel={m.loadingFolders} style={{ alignSelf: "flex-start", padding: 8 }} /> : null}
112
116
  {listed.data?.folders.map((name) => (
113
- <PressableRow key={name} colors={colors} accessibilityRole="button" accessibilityLabel={`Open ${name}`} onPress={() => { setNaming(null); setAt(join(name)); }}
117
+ <PressableRow key={name} colors={colors} accessibilityRole="button" accessibilityLabel={m.openItem(name)} onPress={() => { setNaming(null); setAt(join(name)); }}
114
118
  style={{ flexDirection: "row", alignItems: "center", gap: 8, paddingHorizontal: 8, paddingVertical: 6, borderRadius: 6 }}>
115
119
  <Icon name="Folder" size={14} color={colors.foregroundMuted} />
116
120
  <Text numberOfLines={1} ellipsizeMode="middle" style={{ flex: 1, color: colors.foreground, fontSize: 12, lineHeight: 16 }}>{name}</Text>
@@ -118,40 +122,41 @@ function FolderPicker({ colors, workspaceId, directory, open, start, onClose, on
118
122
  </PressableRow>
119
123
  ))}
120
124
  {listed.data && !listed.data.folders.length ? (
121
- <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 16, paddingHorizontal: 8, paddingVertical: 6 }}>No folders in here.</Text>
125
+ <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 16, paddingHorizontal: 8, paddingVertical: 6 }}>{m.noFolders}</Text>
122
126
  ) : null}
123
- {listed.error ? <Text accessibilityRole="alert" style={{ color: colors.statusDanger, fontSize: 12, lineHeight: 17, paddingHorizontal: 8 }}>{errorText(listed.error, "Could not list the folders.")}</Text> : null}
127
+ {listed.error ? <Text accessibilityRole="alert" style={{ color: colors.statusDanger, fontSize: 12, lineHeight: 17, paddingHorizontal: 8 }}>{errorText(listed.error, m.listFoldersFailure)}</Text> : null}
124
128
  </ScrollView>
125
129
 
126
130
  {naming === null ? (
127
131
  <PressScale accessibilityRole="button" onPress={() => setNaming("")}
128
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" })}>
129
133
  <Icon name="FolderPlus" size={14} color={colors.foreground} />
130
- <Text style={{ color: colors.foreground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>New folder</Text>
134
+ <Text style={{ color: colors.foreground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>{m.newFolder}</Text>
131
135
  </PressScale>
132
136
  ) : (
133
137
  <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
134
138
  <TextInput autoFocus value={naming} onChangeText={setNaming} onSubmitEditing={() => { if (newName) onPick(join(newName)); }}
135
- autoCapitalize="none" autoCorrect={false} spellCheck={false} accessibilityLabel="New folder name" placeholder="progress-history" placeholderTextColor={colors.foregroundMuted}
139
+ autoCapitalize="none" autoCorrect={false} spellCheck={false} accessibilityLabel={m.newFolderName} placeholder="progress-history" placeholderTextColor={colors.foregroundMuted}
136
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 }} />
137
141
  <PressScale accessibilityRole="button" disabled={!newName} onPress={() => onPick(join(newName))}
138
142
  style={({ pressed, hovered }) => ({ paddingHorizontal: 10, paddingVertical: 6, borderRadius: 6, backgroundColor: colors.accent, opacity: !newName ? 0.5 : pressed ? 0.7 : hovered ? 0.85 : 1 })}>
139
- <Text style={{ color: colors.accentForeground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>Use new folder</Text>
143
+ <Text style={{ color: colors.accentForeground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>{m.useNewFolder}</Text>
140
144
  </PressScale>
141
145
  </View>
142
146
  )}
143
147
 
144
148
  <View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "center", justifyContent: "flex-end", gap: 8 }}>
145
- {at ? null : <Text style={{ flex: 1, minWidth: 140, color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>Open a folder to use it, or make a new one.</Text>}
149
+ {at ? null : <Text style={{ flex: 1, minWidth: 140, color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>{m.pickFolderHint}</Text>}
146
150
  <PressScale accessibilityRole="button" onPress={close}
147
151
  style={({ pressed, hovered }) => ({ paddingHorizontal: 12, paddingVertical: 6, borderRadius: 6, backgroundColor: pressed ? colors.border : hovered ? colors.surface2 : "transparent" })}>
148
- <Text style={{ color: colors.foreground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>Cancel</Text>
152
+ <Text style={{ color: colors.foreground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>{m.cancel}</Text>
149
153
  </PressScale>
150
154
  <PressScale accessibilityRole="button" disabled={!at} onPress={() => onPick(at)} outerStyle={{ flexShrink: 1 }}
151
155
  style={({ pressed, hovered }) => ({ paddingHorizontal: 12, paddingVertical: 6, borderRadius: 6, backgroundColor: colors.accent, opacity: !at ? 0.5 : pressed ? 0.7 : hovered ? 0.85 : 1 })}>
152
- <Text numberOfLines={1} ellipsizeMode="middle" style={{ color: colors.accentForeground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>{at ? `Use ${crumbs[crumbs.length - 1]}` : "Use folder"}</Text>
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>
153
157
  </PressScale>
154
158
  </View>
159
+ </ProgressLanguageProvider>
155
160
  </Modal.Content>
156
161
  </Modal>
157
162
  );
package/client/i18n.ts ADDED
@@ -0,0 +1,86 @@
1
+ import type { Dashboard, HeadlinePart } from "../shared/dashboard";
2
+ import type { LanguagePreference } from "../shared/settings";
3
+ import type { TimelineLabel } from "../shared/ticket-story";
4
+ import type { ProgressMessages } from "./messages";
5
+ import { messages as en } from "./locales/en";
6
+ import { messages as zhCN } from "./locales/zh-CN";
7
+
8
+ export type { ProgressMessages } from "./messages";
9
+ export type ProgressLocale = "en" | "zh-CN";
10
+
11
+ export function systemLocale(): string {
12
+ try {
13
+ return Intl.DateTimeFormat().resolvedOptions().locale;
14
+ } catch {
15
+ return "en";
16
+ }
17
+ }
18
+
19
+ export function resolveLocale(preference: LanguagePreference, system = systemLocale()): ProgressLocale {
20
+ if (preference !== "system") return preference;
21
+ const [language, ...rest] = system.replaceAll("_", "-").toLowerCase().split("-");
22
+ // Extensions like "-u-nu-hanidec" say nothing about the script.
23
+ const singleton = rest.findIndex((part) => part.length === 1);
24
+ const subtags = singleton === -1 ? rest : rest.slice(0, singleton);
25
+ if (language !== "zh") return "en";
26
+ // A script tag beats the region: zh-Hans-HK is Simplified.
27
+ if (subtags.includes("hans")) return "zh-CN";
28
+ if (subtags.includes("hant")) return "en";
29
+ return subtags.length === 0 || subtags.some((part) => ["cn", "sg"].includes(part)) ? "zh-CN" : "en";
30
+ }
31
+
32
+ export function messagesFor(locale: ProgressLocale): ProgressMessages {
33
+ return locale === "zh-CN" ? zhCN : en;
34
+ }
35
+
36
+ export function formatClientMinutes(locale: ProgressLocale, totalMin: number): string {
37
+ const m = messagesFor(locale);
38
+ const whole = Math.max(0, Math.floor(totalMin));
39
+ if (whole < 1) return m.lessThanMinute;
40
+ const hours = Math.floor(whole / 60);
41
+ const minutes = whole % 60;
42
+ return !hours ? m.minutes(minutes) : minutes ? m.hoursMinutes(hours, minutes) : m.hours(hours);
43
+ }
44
+
45
+ export function formatClientWorkDone(locale: ProgressLocale, doneMin: number, totalMin: number, tookMin?: number): string {
46
+ if (tookMin !== undefined) return formatClientMinutes(locale, tookMin);
47
+ const m = messagesFor(locale);
48
+ return m.workDone(doneMin < 1 ? m.zeroMinutes : formatClientMinutes(locale, doneMin), formatClientMinutes(locale, totalMin));
49
+ }
50
+
51
+ export function formatClientClock(locale: ProgressLocale, iso: string): string {
52
+ return new Intl.DateTimeFormat(locale, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }).format(new Date(iso));
53
+ }
54
+
55
+ export function formatClientTimeOfDay(locale: ProgressLocale, iso: string, nowMs: number): string {
56
+ const date = new Date(iso);
57
+ const now = new Date(nowMs);
58
+ const sameDay = date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate();
59
+ return sameDay ? new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "2-digit" }).format(date) : formatClientClock(locale, iso);
60
+ }
61
+
62
+ export function formatClientAgo(locale: ProgressLocale, iso: string, nowMs: number): string {
63
+ const m = messagesFor(locale);
64
+ const minutes = (nowMs - Date.parse(iso)) / 60_000;
65
+ return minutes < 1 ? m.justNow : m.ago(formatClientMinutes(locale, minutes));
66
+ }
67
+
68
+ export function clientHeadline(locale: ProgressLocale, dashboard: Dashboard): HeadlinePart[] {
69
+ const m = messagesFor(locale);
70
+ const counted = dashboard.tickets.filter((ticket) => ticket.status !== "skipped");
71
+ const done = counted.filter((ticket) => ticket.status === "done").length;
72
+ const parts: HeadlinePart[] = [{ text: m.headlineDone(done, counted.length, dashboard.run?.itemLabel ?? "Ticket"), tone: "normal" }];
73
+ if (dashboard.questions.open.length) parts.push({ text: m.questionsWaiting(dashboard.questions.open.length), tone: "normal" });
74
+ if (dashboard.stuck.length) parts.push({ text: m.stuckCount(dashboard.stuck.length), tone: "danger" });
75
+ return parts;
76
+ }
77
+
78
+ export function timelineLabel(m: ProgressMessages, label: TimelineLabel): string {
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);
85
+ }
86
+ }
@@ -0,0 +1,23 @@
1
+ import { useSettings } from "@getpaseo/plugin/client";
2
+ import React, { createContext, useContext } from "react";
3
+ import { displaySettings } from "../shared/settings";
4
+ import { messagesFor, resolveLocale } from "./i18n";
5
+
6
+ function useLanguageSettings() {
7
+ const settings = useSettings(displaySettings);
8
+ const locale = resolveLocale(settings.status === "ready" ? settings.values.language : "system");
9
+ return { locale, messages: messagesFor(locale), settings };
10
+ }
11
+
12
+ const LanguageContext = createContext<ReturnType<typeof useLanguageSettings> | null>(null);
13
+
14
+ export function ProgressLanguageProvider({ children }: { children: React.ReactNode }) {
15
+ const language = useLanguageSettings();
16
+ return <LanguageContext.Provider value={language}>{children}</LanguageContext.Provider>;
17
+ }
18
+
19
+ export function useProgressLanguage() {
20
+ const language = useContext(LanguageContext);
21
+ if (!language) throw new Error("ProgressLanguageProvider is missing");
22
+ return language;
23
+ }
@@ -6,7 +6,9 @@ import { Text, View } from "react-native";
6
6
  import { PressScale, Presence } from "./motion";
7
7
  import { raised } from "./surfaces";
8
8
  import { getLauncherStatus, getSkillStatus, installLauncher, installSkill, type LauncherStatus, type SkillStatus } from "../shared/rpc";
9
+ import { messagesFor, type ProgressMessages } from "./i18n";
9
10
  import { errorText } from "./errors";
11
+ import { useProgressLanguage } from "./language";
10
12
 
11
13
  type Colors = PluginWorkspacePanelProps["theme"]["colors"];
12
14
 
@@ -19,30 +21,30 @@ export function skillQueryKey(hostId: string) {
19
21
  }
20
22
 
21
23
  // What the banner says, or null when agents can already run the command.
22
- export function launcherNotice(status: LauncherStatus): { text: string; action?: string } | null {
24
+ export function launcherNotice(status: LauncherStatus, m: ProgressMessages = messagesFor("en")): { text: string; action?: string } | null {
23
25
  switch (status.state) {
24
26
  case "missing":
25
- return { text: `Agents record progress with the paseo-progress command, which isn't installed yet. Installing adds it at ${status.path}`, action: "Install command" };
27
+ return { text: m.launcherMissing(status.path), action: m.installCommand };
26
28
  case "outdated":
27
- return { text: `An older paseo-progress command is installed at ${status.path}.`, action: "Update command" };
29
+ return { text: m.launcherOutdated(status.path), action: m.updateCommand };
28
30
  case "foreign":
29
- return { text: `Something else already exists at ${status.path}, so the plugin won't install there. The README shows how to install it elsewhere.` };
31
+ return { text: m.launcherForeign(status.path) };
30
32
  case "current":
31
- return status.onPath ? null : { text: `paseo-progress is installed, but its folder isn't on the PATH Paseo uses, so agents need the full path: ${status.path}` };
33
+ return status.onPath ? null : { text: m.launcherOffPath(status.path) };
32
34
  }
33
35
  }
34
36
 
35
37
  // What the skill banner says, or null when every agent folder has the skill.
36
- export function skillNotice(status: SkillStatus): { text: string; action?: string } | null {
38
+ export function skillNotice(status: SkillStatus, m: ProgressMessages = messagesFor("en")): { text: string; action?: string } | null {
37
39
  const folders = (state: string) => status.targets.filter((target) => target.state === state).map((target) => target.path.replace(/[\\/]paseo-progress$/, ""));
38
- const skipped = folders("foreign").length ? ` Something else is already at ${folders("foreign").join(", ")}, so the plugin leaves it alone.` : "";
40
+ const skipped = folders("foreign").length ? m.skillSkipped(folders("foreign").join(m.listSeparator)) : "";
39
41
  switch (status.state) {
40
42
  case "missing":
41
- return { text: `Agents learn when to record progress from the paseo-progress skill, which isn't installed yet. Installing adds a link in ${folders("missing").join(", ")}.${skipped}`, action: "Install skill" };
43
+ return { text: m.skillMissing(folders("missing").join(m.listSeparator), skipped), action: m.installSkill };
42
44
  case "outdated":
43
- return { text: `The paseo-progress skill in ${folders("outdated").join(", ")} points to a copy of the plugin Paseo no longer runs.${skipped}`, action: "Update skill" };
45
+ return { text: m.skillOutdated(folders("outdated").join(m.listSeparator), skipped), action: m.updateSkill };
44
46
  case "foreign":
45
- return { text: `Something else already exists at every paseo-progress skill path, so the plugin won't install the skill.` };
47
+ return { text: m.skillForeign };
46
48
  case "current":
47
49
  return null;
48
50
  }
@@ -50,6 +52,7 @@ export function skillNotice(status: SkillStatus): { text: string; action?: strin
50
52
 
51
53
  // Nothing is written until the user presses the button.
52
54
  export function LauncherBanner({ colors, host }: { colors: Colors; host: PluginWorkspacePanelProps["host"] }) {
55
+ const { messages: m } = useProgressLanguage();
53
56
  const fetchStatus = useRpc(getLauncherStatus);
54
57
  const install = useRpc(installLauncher);
55
58
  const queryClient = useQueryClient();
@@ -59,15 +62,16 @@ export function LauncherBanner({ colors, host }: { colors: Colors; host: PluginW
59
62
  mutationFn: () => install({}),
60
63
  onSuccess: (next) => {
61
64
  queryClient.setQueryData(launcherQueryKey(host.id), next);
62
- toast.show(`Installed paseo-progress at ${next.path}`, { variant: "success" });
65
+ toast.show(m.launcherInstalled(next.path), { variant: "success" });
63
66
  },
64
- onError: (error) => toast.error(errorText(error, "Could not install the command.")),
67
+ onError: (error) => toast.error(errorText(error, m.installCommandFailure)),
65
68
  });
66
- const notice = status.data ? launcherNotice(status.data) : null;
69
+ const notice = status.data ? launcherNotice(status.data, m) : null;
67
70
  return <SetupBanner colors={colors} icon="Terminal" notice={notice} pending={mutation.isPending} onPress={() => mutation.mutate()} />;
68
71
  }
69
72
 
70
73
  export function SkillBanner({ colors, host }: { colors: Colors; host: PluginWorkspacePanelProps["host"] }) {
74
+ const { messages: m } = useProgressLanguage();
71
75
  const fetchStatus = useRpc(getSkillStatus);
72
76
  const install = useRpc(installSkill);
73
77
  const queryClient = useQueryClient();
@@ -77,11 +81,11 @@ export function SkillBanner({ colors, host }: { colors: Colors; host: PluginWork
77
81
  mutationFn: () => install({}),
78
82
  onSuccess: (next) => {
79
83
  queryClient.setQueryData(skillQueryKey(host.id), next);
80
- toast.show("Installed the paseo-progress skill. New agent sessions will use it.", { variant: "success" });
84
+ toast.show(m.skillInstalled, { variant: "success" });
81
85
  },
82
- onError: (error) => toast.error(errorText(error, "Could not install the skill.")),
86
+ onError: (error) => toast.error(errorText(error, m.installSkillFailure)),
83
87
  });
84
- const notice = status.data ? skillNotice(status.data) : null;
88
+ const notice = status.data ? skillNotice(status.data, m) : null;
85
89
  return <SetupBanner colors={colors} icon="Sparkles" notice={notice} pending={mutation.isPending} onPress={() => mutation.mutate()} />;
86
90
  }
87
91
 
@@ -92,6 +96,7 @@ function SetupBanner({ colors, icon, notice, pending, onPress }: {
92
96
  pending: boolean;
93
97
  onPress(): void;
94
98
  }) {
99
+ const { messages: m } = useProgressLanguage();
95
100
  return (
96
101
  <Presence show={Boolean(notice)}>{notice ? (
97
102
  <View style={{ flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 10, margin: 12, marginBottom: 0, padding: 8, borderRadius: 12, ...raised(colors), backgroundColor: colors.surface1 }}>
@@ -102,7 +107,7 @@ function SetupBanner({ colors, icon, notice, pending, onPress }: {
102
107
  <PressScale accessibilityRole="button" disabled={pending} onPress={onPress}
103
108
  style={({ pressed, hovered }) => ({ paddingHorizontal: 10, paddingVertical: 5, borderRadius: 4, backgroundColor: colors.accent, opacity: pressed || pending ? 0.7 : hovered ? 0.85 : 1 })}>
104
109
  <Text style={{ color: colors.accentForeground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>
105
- {pending ? "Installing…" : notice.action}
110
+ {pending ? m.installing : notice.action}
106
111
  </Text>
107
112
  </PressScale>
108
113
  ) : null}