@stevecastaneda/paseo-progress 0.2.1 → 0.3.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.
- package/README.md +18 -7
- package/client/attachments.tsx +1 -1
- package/client/dialog-stack.tsx +8 -1
- package/client/panel.tsx +5 -29
- package/client/pill-popover.tsx +5 -3
- package/client/questions.tsx +18 -31
- package/client/tabs.tsx +65 -0
- package/client/ticket-dialog.tsx +142 -112
- package/client/time-bar.tsx +19 -0
- package/client/write-up.tsx +95 -0
- package/dist/server/cli.js +59 -30
- package/dist/server/progress-file.js +11 -4
- package/dist/shared/attachments.js +12 -6
- package/dist/shared/dashboard.js +14 -9
- package/dist/shared/events.js +14 -6
- package/dist/shared/format.js +6 -0
- package/dist/shared/show.js +3 -1
- package/package.json +2 -2
- package/paseo-plugin.json +1 -2
- package/server/cli.ts +62 -32
- package/server/progress-file.ts +11 -4
- package/shared/attachments.ts +21 -11
- package/shared/dashboard.ts +23 -12
- package/shared/events.ts +14 -6
- package/shared/format.ts +7 -0
- package/shared/show.ts +2 -1
- package/shared/write-up.ts +92 -0
- package/skills/paseo-progress/SKILL.md +10 -7
- package/npm-shrinkwrap.json +0 -4041
package/README.md
CHANGED
|
@@ -10,8 +10,8 @@ The dashboard shows:
|
|
|
10
10
|
- a "possibly stale" warning after 15 minutes with no update while tickets remain, unless Paseo shows an agent in the workspace still running (for example one waiting on its subagents), and never once every ticket is done or skipped or the run is finished; working items swap their spinner for a pulsing amber warning
|
|
11
11
|
- percent of estimated work done, time done of time estimated (like "1 h 30 min of 2 h"; once everything is done, how long it really took), and a bar with one segment per ticket
|
|
12
12
|
- **Stuck:** blocked tickets, working tickets with no update for longer than their estimate, and blockers the agent flags
|
|
13
|
-
- **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 timeline of its stages and statuses with how long each took,
|
|
14
|
-
- **Questions:** each with a permanent reference (Q1, Q2, ...), lettered options, and the
|
|
13
|
+
- **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"
|
|
14
|
+
- **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 and text files preview in Paseo (with **Back** to the question), 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.
|
|
15
15
|
- **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
|
|
16
16
|
|
|
17
17
|
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 shows it (`3 questions · 1 stuck`). 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.
|
|
@@ -26,7 +26,7 @@ The first time a run starts in a worktree whose Progress panel has never been op
|
|
|
26
26
|
paseo plugin add npm:@stevecastaneda/paseo-progress
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
Or from
|
|
29
|
+
Or from Paseo's plugin registry: `paseo plugin add stevecastaneda/progress`.
|
|
30
30
|
|
|
31
31
|
Enable plugins under **Settings → Plugins** on the Paseo host. You need Node.js 22.18 or later on the daemon host.
|
|
32
32
|
|
|
@@ -72,14 +72,15 @@ Run `paseo-progress --help` for every command, and `paseo-progress <command> --h
|
|
|
72
72
|
|
|
73
73
|
```sh
|
|
74
74
|
paseo-progress start "Loan Options snapshots" --subtitle "4 tickets"
|
|
75
|
-
paseo-progress ticket add "Ticket 01: Saved table" --estimate 120
|
|
75
|
+
paseo-progress ticket add "Ticket 01: Saved table" --estimate 120 \
|
|
76
|
+
--source specs/01-saved-table.md # Added T01
|
|
76
77
|
paseo-progress ticket update T01 --status working --stage Build
|
|
77
78
|
paseo-progress ticket update T01 --stage Fixes
|
|
78
79
|
paseo-progress ticket update T01 --status done
|
|
79
80
|
paseo-progress question ask "Row spacing" "Even out the card spacing?" \
|
|
80
81
|
--option "A=Even it out | Cards look balanced" \
|
|
81
82
|
--option "B=Leave it | No change" \
|
|
82
|
-
--
|
|
83
|
+
--recommend A --raised-by "Ticket 01 design review" # Asked Q1
|
|
83
84
|
paseo-progress question answer Q1 A --words "Even it out."
|
|
84
85
|
paseo-progress deliverable add "Browser check screenshots" .scratch/shots/ --ticket T01
|
|
85
86
|
paseo-progress activity add "Saved table matches the design now." --ticket T01
|
|
@@ -90,18 +91,21 @@ paseo-progress finish "Snapshots ship for all four tables"
|
|
|
90
91
|
```
|
|
91
92
|
|
|
92
93
|
- Statuses are `not_started`, `working`, `blocked`, `done`, and `skipped`. Skipped tickets leave the totals.
|
|
94
|
+
- `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.
|
|
93
95
|
- 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.
|
|
94
|
-
- `question ask
|
|
96
|
+
- `question ask` needs `--recommend`, the agent's pick. The question dialog tags that option "Recommended".
|
|
95
97
|
- 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. Pressing any other local deliverable opens it with its default app on the machine running the Paseo daemon (for example HTML in your browser and folders in the file manager). 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.
|
|
98
|
+
- 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.
|
|
96
99
|
- `start` begins a fresh dashboard. Earlier runs stay in the file. Question references stay unique across runs.
|
|
97
100
|
- `finish "<outcome>"` closes the run once every ticket is done or skipped, no question is open, and nothing is flagged stuck; otherwise it says what is left. The run stays on the dashboard, marked Finished with its outcome, and takes no more updates, so the next agent in the worktree starts a new run instead of adding to it.
|
|
98
101
|
|
|
99
102
|
## The progress file
|
|
100
103
|
|
|
101
|
-
The progress file, `progress.jsonl`, holds one JSON event per line, each with a version (`v`), a UTC timestamp (`ts`) from the real clock, and a `type`. It is only ever appended to, so you can read or diff the history. A half-written last line is ignored. Other bad lines are skipped and listed in a small notice in the panel while the rest still renders. A lock keeps two commands from writing at once or handing out the same id.
|
|
104
|
+
The progress file, `progress.jsonl`, holds one JSON event per line, each with a version (`v`), a UTC timestamp (`ts`) from the real clock, and a `type`. Events written by a Paseo agent also carry its agent id (`by`). It is only ever appended to, so you can read or diff the history. A half-written last line is ignored. Other bad lines are skipped and listed in a small notice in the panel while the rest still renders. A lock keeps two commands from writing at once or handing out the same id.
|
|
102
105
|
|
|
103
106
|
## Limitations
|
|
104
107
|
|
|
108
|
+
- An agent hears about tickets other sessions added only when it next runs `paseo-progress`. One in the middle of a long step finds out when that step ends.
|
|
105
109
|
- Nothing updates unless the agent runs `paseo-progress`. Agents need the skill, and an agent that skips the command leaves the dashboard behind.
|
|
106
110
|
- The command runs on the machine that hosts the Paseo daemon, and it needs Node.js 22.18 or later there.
|
|
107
111
|
- Each worktree keeps its own dashboard. There's no view across worktrees.
|
|
@@ -120,3 +124,10 @@ npm run dev
|
|
|
120
124
|
```
|
|
121
125
|
|
|
122
126
|
`npm run dev` type-checks and tests the plugin, then points Paseo at this folder. The tests run the agent's commands in a throwaway worktree and read back the dashboard the panel would get.
|
|
127
|
+
|
|
128
|
+
## Changelog
|
|
129
|
+
|
|
130
|
+
### 0.3.1
|
|
131
|
+
|
|
132
|
+
- Installs again with npm 12. Installing failed with `npm ci` asking for a `package-lock.json`, because npm 12 no longer reads the lockfile this plugin shipped ([#53](https://github.com/stevecastaneda/paseo-plugins/issues/53)). The plugin no longer runs its own install step; Paseo's npm install already brings in what it needs.
|
|
133
|
+
- Install from npm or Paseo's plugin registry. Installing straight from GitHub no longer works.
|
package/client/attachments.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { type Attachment, attachmentIcon, canPreview, shortName } from "../share
|
|
|
6
6
|
import { PressableRow } from "./row";
|
|
7
7
|
import { openDeliverable } from "../shared/rpc";
|
|
8
8
|
|
|
9
|
-
export { type Attachment, attachmentIcon, deliverableAttachment, questionAttachments, shortName } from "../shared/attachments";
|
|
9
|
+
export { type Attachment, attachmentIcon, deliverableAttachment, questionAttachments, shortName, ticketSourceAttachment } from "../shared/attachments";
|
|
10
10
|
|
|
11
11
|
type Colors = PluginWorkspacePanelProps["theme"]["colors"];
|
|
12
12
|
type Navigation = PluginWorkspacePanelProps["navigation"];
|
package/client/dialog-stack.tsx
CHANGED
|
@@ -49,8 +49,15 @@ export function StackedDialog({ colors, root, onClose, questions, dashboard, now
|
|
|
49
49
|
const opener = useAttachmentOpener({ ...context, onPreview: (attachment) => push({ kind: "preview", attachment, group: previewable(group.current) }) });
|
|
50
50
|
const openAttachment = (attachment: Attachment, from: Attachment[]) => {
|
|
51
51
|
group.current = from;
|
|
52
|
+
// A link opens elsewhere in Paseo; close first so it isn't left behind the dialog.
|
|
53
|
+
if (attachment.url) onClose();
|
|
52
54
|
void opener.open(attachment);
|
|
53
55
|
};
|
|
56
|
+
// Opens the agent's chat in its workspace, with the dialog out of the way.
|
|
57
|
+
const openAgent = context.navigation ? (agentId: string) => {
|
|
58
|
+
onClose();
|
|
59
|
+
context.navigation!.openAgent({ agentId });
|
|
60
|
+
} : undefined;
|
|
54
61
|
const copy = useCopy();
|
|
55
62
|
const steps: Step[] = root ? [root, ...trail] : [];
|
|
56
63
|
// Keep drawing the last step while the dialog fades out.
|
|
@@ -85,7 +92,7 @@ export function StackedDialog({ colors, root, onClose, questions, dashboard, now
|
|
|
85
92
|
) : (
|
|
86
93
|
<>
|
|
87
94
|
{ticket && dashboard ? (
|
|
88
|
-
<TicketStoryView colors={colors} dashboard={dashboard} ticket={ticket} now={now} live={live}
|
|
95
|
+
<TicketStoryView colors={colors} dashboard={dashboard} ticket={ticket} now={now} live={live} context={context} onOpenAgent={openAgent}
|
|
89
96
|
onOpenAttachment={openAttachment} onOpenQuestion={(id) => push({ kind: "question", id })} />
|
|
90
97
|
) : null}
|
|
91
98
|
{question ? (
|
package/client/panel.tsx
CHANGED
|
@@ -10,7 +10,7 @@ import { SectionTitle } from "./section-title";
|
|
|
10
10
|
import { raised } from "./surfaces";
|
|
11
11
|
import { When } from "./when";
|
|
12
12
|
import React, { useEffect, useRef, useState } from "react";
|
|
13
|
-
import { ActivityIndicator, Animated, Platform,
|
|
13
|
+
import { ActivityIndicator, Animated, Platform, ScrollView, Text, View } from "react-native";
|
|
14
14
|
import { finishedToFold, isFinished, shortTitle, type Activity, type Dashboard, type FileIssue, type ProgressSegment, type StuckItem, type Ticket } from "../shared/dashboard";
|
|
15
15
|
import type { TicketStatus } from "../shared/events";
|
|
16
16
|
import { formatEstimate, formatMinutes, formatWorkDone, minutesSince } from "../shared/format";
|
|
@@ -19,6 +19,7 @@ import { useDashboard } from "./dashboard-query";
|
|
|
19
19
|
import { STATUS_LABEL } from "./status-badge";
|
|
20
20
|
import { TicketDialogs } from "./ticket-dialog";
|
|
21
21
|
import { PressableRow } from "./row";
|
|
22
|
+
import { TabStrip } from "./tabs";
|
|
22
23
|
import { ShowMoreRow, usePaged } from "./show-more";
|
|
23
24
|
import { notePanelOpened } from "./panel-opened";
|
|
24
25
|
import { NARROW_MAX, NarrowProvider, useNarrow } from "./narrow";
|
|
@@ -212,7 +213,7 @@ function DashboardView({ colors, dashboard, agentRunning, compact, workspaceId,
|
|
|
212
213
|
style={{ flexDirection: wide ? "row" : "column", alignItems: "flex-start" }}>
|
|
213
214
|
<View style={{ flex: wide ? 3 : undefined, alignSelf: "stretch", minWidth: 0 }}>
|
|
214
215
|
<Presence show={dashboard.questions.open.length > 0} order={3}>{dashboard.questions.open.length ? (
|
|
215
|
-
<QuestionsSection colors={colors} questions={dashboard.questions.open} answered={dashboard.questions.answered} now={now}
|
|
216
|
+
<QuestionsSection colors={colors} questions={dashboard.questions.open} answered={dashboard.questions.answered} now={now} context={attachmentContext} />
|
|
216
217
|
) : null}</Presence>
|
|
217
218
|
<Presence show={Boolean(dashboard.stuck.length)} order={3}>{dashboard.stuck.length ? <StuckSection colors={colors} items={dashboard.stuck} now={now} /> : null}</Presence>
|
|
218
219
|
<Presence show order={4}>
|
|
@@ -270,11 +271,8 @@ function HistoryCard({ colors, workspaceId, tabs }: { colors: Colors; workspaceI
|
|
|
270
271
|
return (
|
|
271
272
|
<View onLayout={(event) => setShowIcons(event.nativeEvent.layout.width >= TAB_ICONS_MIN)}
|
|
272
273
|
style={{ margin: 12, marginBottom: 0, ...raised(colors), borderRadius: 6, overflow: "hidden" }}>
|
|
273
|
-
<
|
|
274
|
-
{
|
|
275
|
-
return <Tab key={entry.id} colors={colors} narrow={narrow} label={entry.label} icon={showIcons ? entry.icon : null} count={entry.count} selected={entry.id === current.id} onPress={() => choose(entry.id)} />;
|
|
276
|
-
})}
|
|
277
|
-
</View>
|
|
274
|
+
<TabStrip colors={colors} narrow={narrow} tabs={tabs.map((entry) => ({ ...entry, icon: showIcons ? entry.icon : null }))} selected={current.id} onSelect={choose}
|
|
275
|
+
style={{ paddingHorizontal: narrow ? 8 : 10 }} />
|
|
278
276
|
{current.count === 0 ? (
|
|
279
277
|
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, paddingHorizontal: 10, paddingVertical: 8 }}>{current.empty}</Text>
|
|
280
278
|
) : current.content}
|
|
@@ -282,28 +280,6 @@ function HistoryCard({ colors, workspaceId, tabs }: { colors: Colors; workspaceI
|
|
|
282
280
|
);
|
|
283
281
|
}
|
|
284
282
|
|
|
285
|
-
// Instant feedback: tabs switch often, so hover and press change color only.
|
|
286
|
-
// Narrow tightens the spacing so all three tabs stay on one line at the sidebar's 320px.
|
|
287
|
-
function Tab({ colors, narrow, label, icon, count, selected, onPress }: { colors: Colors; narrow: boolean; label: string; icon: string | null; count: number; selected: boolean; onPress(): void }) {
|
|
288
|
-
const [hovered, setHovered] = useState(false);
|
|
289
|
-
return (
|
|
290
|
-
<Pressable accessibilityRole="tab" accessibilityState={{ selected }} onPress={onPress}
|
|
291
|
-
onHoverIn={() => setHovered(true)} onHoverOut={() => setHovered(false)}
|
|
292
|
-
style={({ pressed }) => ({
|
|
293
|
-
flexDirection: "row", alignItems: "center", gap: narrow ? 4 : 6, paddingTop: 8, paddingBottom: 6, marginBottom: -1,
|
|
294
|
-
borderBottomWidth: 2,
|
|
295
|
-
borderBottomColor: selected ? colors.accent : pressed || hovered ? colors.border : "transparent",
|
|
296
|
-
})}>
|
|
297
|
-
{icon ? <Icon name={icon} size={14} color={selected ? colors.accent : colors.foregroundMuted} /> : null}
|
|
298
|
-
<Text style={{ color: selected || hovered ? colors.foreground : colors.foregroundMuted, fontSize: 13, lineHeight: 18, fontWeight: "600" }}>{label}</Text>
|
|
299
|
-
{/* Count badge: muted on both tabs; the chosen tab's number reads darker. */}
|
|
300
|
-
<View style={{ minWidth: 16, paddingHorizontal: 4, borderRadius: 4, alignItems: "center", backgroundColor: colors.surface2 }}>
|
|
301
|
-
<Text style={{ color: selected ? colors.foreground : colors.foregroundMuted, fontSize: 10, lineHeight: 14, fontWeight: "600", fontVariant: ["tabular-nums"] }}>{count}</Text>
|
|
302
|
-
</View>
|
|
303
|
-
</Pressable>
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
283
|
// Newest first, 10 at a time (see usePaged).
|
|
308
284
|
function ActivityList({ colors, activity, now }: { colors: Colors; activity: Activity[]; now: number }) {
|
|
309
285
|
const paged = usePaged(activity);
|
package/client/pill-popover.tsx
CHANGED
|
@@ -8,7 +8,7 @@ import { progressPanel } from "./pills";
|
|
|
8
8
|
import { PressScale } from "./motion";
|
|
9
9
|
import { type Attachment, isPreviewable, useAttachmentOpener } from "./attachments";
|
|
10
10
|
import { PreviewDialog, previewable } from "./preview";
|
|
11
|
-
import {
|
|
11
|
+
import { QuestionDetail, QuestionState, useCopy } from "./questions";
|
|
12
12
|
import { raised } from "./surfaces";
|
|
13
13
|
|
|
14
14
|
type Colors = PluginButtonContentProps["theme"]["colors"];
|
|
@@ -97,8 +97,10 @@ export function AttentionList({ colors, dashboard, onSurface1, onCopy, onOpenAtt
|
|
|
97
97
|
<View key={question.id} style={{ gap: 6 }}>
|
|
98
98
|
<View style={{ flexDirection: "row", alignItems: "flex-start", gap: 8 }}>
|
|
99
99
|
<Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 19, fontWeight: "600" }}>{question.id}</Text>
|
|
100
|
-
<
|
|
101
|
-
|
|
100
|
+
<View style={{ flex: 1, minWidth: 0, gap: 2 }}>
|
|
101
|
+
<Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 19 }}>{question.question}</Text>
|
|
102
|
+
<QuestionState colors={colors} question={question} />
|
|
103
|
+
</View>
|
|
102
104
|
</View>
|
|
103
105
|
<QuestionDetail colors={colors} question={question} now={now} onSurface1={onSurface1} onCopy={(letter) => onCopy(question, letter)} onOpenAttachment={onOpenAttachment} />
|
|
104
106
|
</View>
|
package/client/questions.tsx
CHANGED
|
@@ -8,7 +8,6 @@ import { PressableRow } from "./row";
|
|
|
8
8
|
import { ShowMoreRow, usePaged } from "./show-more";
|
|
9
9
|
import { PressScale } from "./motion";
|
|
10
10
|
import { StackedDialog } from "./dialog-stack";
|
|
11
|
-
import { useNarrow } from "./narrow";
|
|
12
11
|
import { raised } from "./surfaces";
|
|
13
12
|
import { When } from "./when";
|
|
14
13
|
|
|
@@ -43,13 +42,12 @@ export interface AttachmentContext {
|
|
|
43
42
|
navigation?: PluginWorkspacePanelProps["navigation"];
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
export function QuestionsSection({ colors, questions, answered, now,
|
|
45
|
+
export function QuestionsSection({ colors, questions, answered, now, context }: {
|
|
47
46
|
colors: Colors;
|
|
48
47
|
questions: Question[];
|
|
49
48
|
// So a question answered while its dialog is open shows the answer instead of vanishing.
|
|
50
49
|
answered: Question[];
|
|
51
50
|
now: number;
|
|
52
|
-
compact: boolean;
|
|
53
51
|
context: AttachmentContext;
|
|
54
52
|
}) {
|
|
55
53
|
const [openId, setOpenId] = useState<string | null>(null);
|
|
@@ -62,9 +60,6 @@ export function QuestionsSection({ colors, questions, answered, now, compact, co
|
|
|
62
60
|
<View style={{ borderWidth: 1, borderColor: colors.accent, backgroundColor: colors.accent, borderRadius: 999, paddingHorizontal: 8, paddingVertical: 1 }}>
|
|
63
61
|
<Text style={{ color: colors.accentForeground, fontSize: 11, lineHeight: 16 }}>{questions.length} waiting</Text>
|
|
64
62
|
</View>
|
|
65
|
-
<Text style={{ flexBasis: compact ? "100%" : undefined, flex: compact ? undefined : 1, textAlign: compact ? "left" : "right", color: colors.foregroundMuted, fontSize: 11, lineHeight: 16 }}>
|
|
66
|
-
Work continues on each default until you answer, except where it waits. Press a question to see its choices and copy an answer.
|
|
67
|
-
</Text>
|
|
68
63
|
</View>
|
|
69
64
|
{questions.map((question, index) => (
|
|
70
65
|
<QuestionRow key={question.id} colors={colors} question={question} first={index === 0}
|
|
@@ -124,7 +119,7 @@ function AnswerSummary({ colors, question, now }: { colors: Colors; question: Qu
|
|
|
124
119
|
{chosen ? ` ${chosen.label}.` : ""}
|
|
125
120
|
{answer.words ? <Text style={{ color: colors.foregroundMuted }}> “{answer.words}”</Text> : null}
|
|
126
121
|
<Text style={{ color: colors.foregroundMuted }}>
|
|
127
|
-
{answer.changedCourse ?
|
|
122
|
+
{answer.changedCourse ? " The agent had already built another option, so work changed course." : ""}
|
|
128
123
|
</Text>
|
|
129
124
|
</Text>
|
|
130
125
|
<Text style={{ color: colors.foregroundMuted, fontSize: 11, lineHeight: 16 }}>
|
|
@@ -141,14 +136,14 @@ function QuestionRow({ colors, question, first, onOpen, onCopy }: {
|
|
|
141
136
|
onOpen(): void;
|
|
142
137
|
onCopy(): void;
|
|
143
138
|
}) {
|
|
144
|
-
// Narrow: the badge goes under the question, so the question keeps the row's width.
|
|
145
|
-
const narrow = useNarrow();
|
|
146
|
-
const badge = <DefaultBadge colors={colors} value={question.default} waits={question.waits} />;
|
|
147
139
|
return (
|
|
148
140
|
<View style={{ flexDirection: "row", gap: 8, paddingHorizontal: 10, paddingVertical: 10, borderTopWidth: first ? 0 : 1, borderTopColor: colors.border }}>
|
|
149
141
|
<PressScale accessibilityRole="button" accessibilityLabel={`Copy ${question.id} for your reply`} onPress={onCopy} hitSlop={6}
|
|
150
142
|
outerStyle={{ alignSelf: "flex-start", marginTop: 1 }}
|
|
151
143
|
style={({ pressed }) => ({
|
|
144
|
+
// One width for every id, so the questions beside them start on one edge.
|
|
145
|
+
minWidth: 34,
|
|
146
|
+
alignItems: "center",
|
|
152
147
|
paddingHorizontal: 6,
|
|
153
148
|
borderRadius: 999,
|
|
154
149
|
backgroundColor: pressed ? colors.surface2 : colors.surface1,
|
|
@@ -158,11 +153,7 @@ function QuestionRow({ colors, question, first, onOpen, onCopy }: {
|
|
|
158
153
|
</PressScale>
|
|
159
154
|
<PressableRow colors={colors} accessibilityRole="button" accessibilityLabel={`${question.id} choices`} onPress={onOpen}
|
|
160
155
|
style={{ flex: 1, minWidth: 0, flexDirection: "row", alignItems: "flex-start", gap: 8, borderRadius: 4, margin: -4, padding: 4 }}>
|
|
161
|
-
<
|
|
162
|
-
<Text style={{ alignSelf: "stretch", color: colors.foreground, fontSize: 13, lineHeight: 19 }}>{question.question}</Text>
|
|
163
|
-
{narrow ? badge : null}
|
|
164
|
-
</View>
|
|
165
|
-
{narrow ? null : badge}
|
|
156
|
+
<Text style={{ flex: 1, minWidth: 0, color: colors.foreground, fontSize: 13, lineHeight: 19 }}>{question.question}</Text>
|
|
166
157
|
{/* Optical: centers the 14px chevron on the 19px first line. */}
|
|
167
158
|
<View style={{ paddingTop: 2.5 }}><Icon name="ChevronRight" size={14} color={colors.foregroundMuted} /></View>
|
|
168
159
|
</PressableRow>
|
|
@@ -181,9 +172,9 @@ export function QuestionView({ colors, question, now, onCopy, onOpenAttachment,
|
|
|
181
172
|
}) {
|
|
182
173
|
return (
|
|
183
174
|
<View style={{ gap: 10 }}>
|
|
184
|
-
<View style={{
|
|
185
|
-
<Text style={{
|
|
186
|
-
{!question.answer ? <
|
|
175
|
+
<View style={{ gap: 2 }}>
|
|
176
|
+
<Text style={{ color: colors.foreground, fontSize: 14, lineHeight: 20 }}>{question.question}</Text>
|
|
177
|
+
{!question.answer ? <QuestionState colors={colors} question={question} /> : null}
|
|
187
178
|
</View>
|
|
188
179
|
{question.answer ? <AnswerSummary colors={colors} question={question} now={now} /> : null}
|
|
189
180
|
<QuestionDetail colors={colors} question={question} now={now} onSurface1 onCopy={onCopy} onOpenAttachment={onOpenAttachment} />
|
|
@@ -192,15 +183,10 @@ export function QuestionView({ colors, question, now, onCopy, onOpenAttachment,
|
|
|
192
183
|
);
|
|
193
184
|
}
|
|
194
185
|
|
|
195
|
-
//
|
|
196
|
-
export function
|
|
197
|
-
return
|
|
198
|
-
|
|
199
|
-
<Text style={{ color: waits ? colors.statusWarning : colors.foregroundMuted, fontSize: 11, lineHeight: 16 }}>
|
|
200
|
-
Default <Text style={{ color: waits ? colors.statusWarning : colors.foreground, fontWeight: "600" }}>{value}</Text>{waits ? ", waits for you" : ""}
|
|
201
|
-
</Text>
|
|
202
|
-
</View>
|
|
203
|
-
);
|
|
186
|
+
// The agent's pick, under an open question that has no lettered options to tag.
|
|
187
|
+
export function QuestionState({ colors, question }: { colors: Colors; question: Question }) {
|
|
188
|
+
if (question.options.length || !question.recommended) return null;
|
|
189
|
+
return <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>Recommended: <Text style={{ color: colors.foreground }}>{question.recommended}</Text></Text>;
|
|
204
190
|
}
|
|
205
191
|
|
|
206
192
|
// `onSurface1` when drawn on a surface1 background (Paseo's dialogs), so the
|
|
@@ -217,10 +203,11 @@ export function QuestionDetail({ colors, question, now, onCopy, onOpenAttachment
|
|
|
217
203
|
return (
|
|
218
204
|
<View style={{ gap: 8, paddingTop: 2 }}>
|
|
219
205
|
{question.options.map((option) => {
|
|
220
|
-
const
|
|
221
|
-
// Once answered, the border marks the choice made; before that, the
|
|
206
|
+
const recommended = option.letter === question.recommended;
|
|
207
|
+
// Once answered, the border marks the choice made; before that, the agent's recommendation.
|
|
222
208
|
const chosen = question.answer?.choice === option.letter;
|
|
223
|
-
const borderColor = question.answer ? (chosen ? colors.statusSuccess : colors.border) :
|
|
209
|
+
const borderColor = question.answer ? (chosen ? colors.statusSuccess : colors.border) : recommended ? colors.accent : colors.border;
|
|
210
|
+
const settled = Boolean(question.answer);
|
|
224
211
|
return (
|
|
225
212
|
<View key={option.letter} style={{ flexDirection: "row", alignItems: "flex-start", gap: 8, padding: 8, borderRadius: 12, borderWidth: 1, borderColor }}>
|
|
226
213
|
<Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 18, fontWeight: "600", width: 14 }}>{option.letter}</Text>
|
|
@@ -228,7 +215,7 @@ export function QuestionDetail({ colors, question, now, onCopy, onOpenAttachment
|
|
|
228
215
|
<Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 18 }}>
|
|
229
216
|
{option.label}
|
|
230
217
|
{chosen ? <Text style={{ color: colors.statusSuccess }}> Your answer</Text> : null}
|
|
231
|
-
{
|
|
218
|
+
{recommended ? <Text style={{ color: settled ? colors.foregroundMuted : colors.accent }}> Recommended</Text> : null}
|
|
232
219
|
</Text>
|
|
233
220
|
{option.consequence ? <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>{option.consequence}</Text> : null}
|
|
234
221
|
</View>
|
package/client/tabs.tsx
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
|
|
2
|
+
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import { Pressable, Text, View } from "react-native";
|
|
5
|
+
|
|
6
|
+
type Colors = PluginWorkspacePanelProps["theme"]["colors"];
|
|
7
|
+
|
|
8
|
+
export interface TabEntry<Id extends string> {
|
|
9
|
+
id: Id;
|
|
10
|
+
label: string;
|
|
11
|
+
icon?: string | null;
|
|
12
|
+
// A count badge beside the label; none when undefined.
|
|
13
|
+
count?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// A row of tabs over a bottom rule, as in the panel's history card and the
|
|
17
|
+
// ticket dialog. Narrow tightens the spacing so three tabs fit the 320px sidebar.
|
|
18
|
+
// `fit` drops the icons when the tabs would otherwise wrap onto a second line.
|
|
19
|
+
export function TabStrip<Id extends string>({ colors, tabs, selected, onSelect, narrow = false, fit = false, style }: {
|
|
20
|
+
colors: Colors;
|
|
21
|
+
tabs: Array<TabEntry<Id>>;
|
|
22
|
+
selected: Id;
|
|
23
|
+
onSelect(id: Id): void;
|
|
24
|
+
narrow?: boolean;
|
|
25
|
+
fit?: boolean;
|
|
26
|
+
style?: object;
|
|
27
|
+
}) {
|
|
28
|
+
// Set once the tabs wrapped with icons, for this set of tabs.
|
|
29
|
+
const ids = tabs.map((entry) => entry.id).join();
|
|
30
|
+
const [wrappedFor, setWrappedFor] = useState<string | null>(null);
|
|
31
|
+
const iconless = fit && wrappedFor === ids;
|
|
32
|
+
return (
|
|
33
|
+
<View accessibilityRole="tablist"
|
|
34
|
+
// One row is 32px (8 + 18 + 6); anything taller wrapped.
|
|
35
|
+
onLayout={fit ? (event) => { if (!iconless && event.nativeEvent.layout.height > 40) setWrappedFor(ids); } : undefined}
|
|
36
|
+
style={[{ flexDirection: "row", flexWrap: "wrap", columnGap: narrow ? 10 : 16, borderBottomWidth: 1, borderBottomColor: colors.border }, style]}>
|
|
37
|
+
{tabs.map((entry) => (
|
|
38
|
+
<Tab key={entry.id} colors={colors} narrow={narrow} entry={iconless ? { ...entry, icon: null } : entry} selected={entry.id === selected} onPress={() => onSelect(entry.id)} />
|
|
39
|
+
))}
|
|
40
|
+
</View>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Instant feedback: tabs switch often, so hover and press change color only.
|
|
45
|
+
function Tab<Id extends string>({ colors, narrow, entry, selected, onPress }: { colors: Colors; narrow: boolean; entry: TabEntry<Id>; selected: boolean; onPress(): void }) {
|
|
46
|
+
const [hovered, setHovered] = useState(false);
|
|
47
|
+
return (
|
|
48
|
+
<Pressable accessibilityRole="tab" accessibilityState={{ selected }} onPress={onPress}
|
|
49
|
+
onHoverIn={() => setHovered(true)} onHoverOut={() => setHovered(false)}
|
|
50
|
+
style={({ pressed }) => ({
|
|
51
|
+
flexDirection: "row", alignItems: "center", gap: narrow ? 4 : 6, paddingTop: 8, paddingBottom: 6, marginBottom: -1,
|
|
52
|
+
borderBottomWidth: 2,
|
|
53
|
+
borderBottomColor: selected ? colors.accent : pressed || hovered ? colors.border : "transparent",
|
|
54
|
+
})}>
|
|
55
|
+
{entry.icon ? <Icon name={entry.icon} size={14} color={selected ? colors.accent : colors.foregroundMuted} /> : null}
|
|
56
|
+
<Text style={{ color: selected || hovered ? colors.foreground : colors.foregroundMuted, fontSize: 13, lineHeight: 18, fontWeight: "600" }}>{entry.label}</Text>
|
|
57
|
+
{/* Count badge: muted on both tabs; the chosen tab's number reads darker. */}
|
|
58
|
+
{entry.count !== undefined ? (
|
|
59
|
+
<View style={{ minWidth: 16, paddingHorizontal: 4, borderRadius: 4, alignItems: "center", backgroundColor: colors.surface2 }}>
|
|
60
|
+
<Text style={{ color: selected ? colors.foreground : colors.foregroundMuted, fontSize: 10, lineHeight: 14, fontWeight: "600", fontVariant: ["tabular-nums"] }}>{entry.count}</Text>
|
|
61
|
+
</View>
|
|
62
|
+
) : null}
|
|
63
|
+
</Pressable>
|
|
64
|
+
);
|
|
65
|
+
}
|