@stevecastaneda/paseo-progress 0.1.8 → 0.2.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 -5
- package/client/dialog-stack.tsx +4 -0
- package/client/history.tsx +157 -0
- package/client/narrow.tsx +13 -0
- package/client/panel.tsx +41 -23
- package/client/questions.tsx +9 -2
- package/client/when.tsx +4 -1
- package/dist/server/cli.js +8 -5
- package/dist/server/progress-file.js +30 -45
- package/dist/server/store.js +205 -0
- package/dist/shared/events.js +7 -5
- package/index.server.ts +4 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/server/cli.ts +8 -5
- package/server/dashboard.ts +30 -7
- package/server/open.ts +2 -1
- package/server/progress-file.ts +30 -46
- package/server/store.ts +205 -0
- package/shared/events.ts +7 -5
- package/shared/rpc.ts +30 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Progress
|
|
2
2
|
|
|
3
|
-
A plugin for Paseo 0.9 and later that gives each worktree a live progress dashboard. Agents record progress with the `paseo-progress` command, which adds one line per change to
|
|
3
|
+
A plugin for Paseo 0.9 and later that gives each worktree a live progress dashboard. Agents record progress with the `paseo-progress` command, which adds one line per change to the worktree's progress file. That file lives outside the repo, so git never sees it. The **Progress** panel draws the dashboard from that file and updates by itself.
|
|
4
4
|
|
|
5
5
|
<img src="images/1-panel.png" alt="The Progress panel in Explorer" width="400">
|
|
6
6
|
|
|
@@ -18,7 +18,7 @@ The panel lives in Explorer, beside the agent chat, so you can watch both. Open
|
|
|
18
18
|
|
|
19
19
|
On phones, where Paseo has no Explorer pane, Progress opens as a tab of its own, and the pill stays above the message box while a run is open so there's always a way in. SVG files preview on desktop; on phones they open on the daemon host.
|
|
20
20
|
|
|
21
|
-
The first time a run starts in a worktree whose Progress panel has never been opened, the pill shows a spinner and reads **Progress** instead; pressing it opens the panel. Once the panel has been opened there, the plugin writes an empty
|
|
21
|
+
The first time a run starts in a worktree whose Progress panel has never been opened, the pill shows a spinner and reads **Progress** instead; pressing it opens the panel. Once the panel has been opened there, the plugin writes an empty `progress-panel-opened` file next to the progress file and the nudge never comes back, even for later runs. It doesn't open the panel by itself because Paseo switches to a workspace to open its panel, which would pull you away from whatever you're looking at.
|
|
22
22
|
|
|
23
23
|
## Install
|
|
24
24
|
|
|
@@ -41,7 +41,19 @@ node "$([ -f "$dir/dist/server/cli.js" ] && echo "$dir/dist/server/cli.js" || ec
|
|
|
41
41
|
|
|
42
42
|
If `paseo` isn't on your `PATH`, use `/Applications/Paseo.app/Contents/Resources/bin/paseo`. The command doesn't name a plugin folder: each run asks Paseo (`paseo plugin ls`) which copy of the plugin it is running and runs that copy, so it keeps working after updates and `npm run dev` switches. The npm package carries the command compiled to JavaScript in `dist/`, because Node won't run TypeScript from `node_modules`.
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
## Where progress is kept
|
|
45
|
+
|
|
46
|
+
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.
|
|
47
|
+
|
|
48
|
+
### Saving run history to the repo
|
|
49
|
+
|
|
50
|
+
If you want to keep runs, at the bottom of the panel press **Save to repo…** and pick a folder (or name a new one). From then on, each run is saved as its own file there, like `history/20261001-064512-export-feature.jsonl`, and updated as the run goes. Commit them like any other file. One file per run means two branches never write the same file, so merges don't conflict. The lock and the panel marker never go there.
|
|
51
|
+
|
|
52
|
+
The choice covers every worktree of the repo and is kept in the repo's local git config (`paseo-progress.saveRuns`), which nothing commits. **Change…** picks another folder for future runs; **Stop saving** turns it off. Files already saved are never moved or deleted.
|
|
53
|
+
|
|
54
|
+
### Upgrading from 0.1.x
|
|
55
|
+
|
|
56
|
+
Earlier versions kept progress in `.scratch/` in each worktree, hidden by a `.scratch/.gitignore` they wrote. The first time the command or the panel touches a worktree after the upgrade, the plugin moves its files out of `.scratch/` and removes that `.gitignore` if it's exactly the one the plugin wrote. If `.scratch/` is then empty, it goes too; anything else you keep there stays. If git tracks the old progress file (for example, an agent committed it), the plugin leaves it where it is and keeps using it, so nothing changes in that repo.
|
|
45
57
|
|
|
46
58
|
## Agent skill
|
|
47
59
|
|
|
@@ -86,13 +98,14 @@ paseo-progress finish "Snapshots ship for all four tables"
|
|
|
86
98
|
|
|
87
99
|
## The progress file
|
|
88
100
|
|
|
89
|
-
|
|
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.
|
|
90
102
|
|
|
91
103
|
## Limitations
|
|
92
104
|
|
|
93
105
|
- Nothing updates unless the agent runs `paseo-progress`. Agents need the skill, and an agent that skips the command leaves the dashboard behind.
|
|
94
106
|
- The command runs on the machine that hosts the Paseo daemon, and it needs Node.js 22.18 or later there.
|
|
95
|
-
- Each worktree keeps its own dashboard
|
|
107
|
+
- Each worktree keeps its own dashboard. There's no view across worktrees.
|
|
108
|
+
- Progress folders outside the repo stay behind when a worktree is deleted. They're small, and you can delete them from `~/.local/state/paseo-progress/`.
|
|
96
109
|
- Paseo can't open a panel without switching you to its workspace. So a new run shows a "Progress" pill, and you open the panel yourself.
|
|
97
110
|
- Pills check busy worktrees every 5 seconds and quiet ones every 30. In a quiet worktree, a new question can take up to half a minute to show.
|
|
98
111
|
- Previews cover images up to 10 MB and the first 256 KB of a text file. Other files open in their default app.
|
package/client/dialog-stack.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import React, { useEffect, useRef, useState } from "react";
|
|
|
4
4
|
import type { Dashboard, Question } from "../shared/dashboard";
|
|
5
5
|
import { type Attachment, attachmentIcon, useAttachmentOpener } from "./attachments";
|
|
6
6
|
import { useLastPresent } from "./motion";
|
|
7
|
+
import { NarrowProvider } from "./narrow";
|
|
7
8
|
import { BackButton, PreviewContent, previewable } from "./preview";
|
|
8
9
|
import { QuestionView, useCopy } from "./questions";
|
|
9
10
|
import { TicketStoryView } from "./ticket-dialog";
|
|
@@ -75,6 +76,8 @@ export function StackedDialog({ colors, root, onClose, questions, dashboard, now
|
|
|
75
76
|
open={Boolean(root)} onOpenChange={(next) => { if (!next) onClose(); }}>
|
|
76
77
|
{/* One Modal.Content for every step: replacing it would re-present the sheet on phones. */}
|
|
77
78
|
<Modal.Content scrollable={!(preview && previewLong)}>
|
|
79
|
+
{/* A dialog has room for clock times, however narrow the panel that opened it. */}
|
|
80
|
+
<NarrowProvider value={false}>
|
|
78
81
|
{preview ? (
|
|
79
82
|
<PreviewContent colors={colors} attachment={preview.attachment} workspaceId={context.workspaceId} workspaceDirectory={context.workspaceDirectory}
|
|
80
83
|
onOpenOnHost={() => { onClose(); void opener.openOnHost(preview.attachment); }} backLabel={back?.label} onBack={back?.onPress}
|
|
@@ -94,6 +97,7 @@ export function StackedDialog({ colors, root, onClose, questions, dashboard, now
|
|
|
94
97
|
) : null}
|
|
95
98
|
</>
|
|
96
99
|
)}
|
|
100
|
+
</NarrowProvider>
|
|
97
101
|
</Modal.Content>
|
|
98
102
|
</Modal>
|
|
99
103
|
);
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { type PluginWorkspacePanelProps, useRpc } from "@getpaseo/plugin/client";
|
|
2
|
+
import { Icon, Modal, useToast } from "@getpaseo/plugin/client/react-native";
|
|
3
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import React, { useState } from "react";
|
|
5
|
+
import { ActivityIndicator, ScrollView, Text, TextInput, View } from "react-native";
|
|
6
|
+
import { PressScale } from "./motion";
|
|
7
|
+
import { PressableRow } from "./row";
|
|
8
|
+
import { listFolders, setHistoryFolder } from "../shared/rpc";
|
|
9
|
+
|
|
10
|
+
type Colors = PluginWorkspacePanelProps["theme"]["colors"];
|
|
11
|
+
|
|
12
|
+
// Run history stays on this computer unless the repo opts in. This quiet row
|
|
13
|
+
// is the only place that changes it; nothing in the repo changes until then.
|
|
14
|
+
export function HistoryRow({ colors, workspaceId, directory, savedTo }: {
|
|
15
|
+
colors: Colors;
|
|
16
|
+
workspaceId: string;
|
|
17
|
+
directory: string;
|
|
18
|
+
savedTo: string | null;
|
|
19
|
+
}) {
|
|
20
|
+
const [picking, setPicking] = useState(false);
|
|
21
|
+
const set = useRpc(setHistoryFolder);
|
|
22
|
+
const queryClient = useQueryClient();
|
|
23
|
+
const toast = useToast();
|
|
24
|
+
const mutation = useMutation({
|
|
25
|
+
mutationFn: (folder: string | null) => set({ workspaceId, workspaceDirectory: directory, folder }),
|
|
26
|
+
onSuccess: ({ savedTo: next }) => {
|
|
27
|
+
queryClient.invalidateQueries({ queryKey: ["progress", "dashboard"] });
|
|
28
|
+
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" });
|
|
29
|
+
},
|
|
30
|
+
onError: (error) => toast.error(error instanceof Error ? error.message : "Could not change where runs are saved."),
|
|
31
|
+
});
|
|
32
|
+
// While saving, the button that started it says so.
|
|
33
|
+
const busy = mutation.isPending ? (mutation.variables === null ? "stop" : "pick") : null;
|
|
34
|
+
const actions: { label: string; busyLabel: string; kind: "pick" | "stop"; onPress(): void }[] = savedTo
|
|
35
|
+
? [{ label: "Change…", busyLabel: "Saving…", kind: "pick", onPress: () => setPicking(true) }, { label: "Stop saving", busyLabel: "Stopping…", kind: "stop", onPress: () => mutation.mutate(null) }]
|
|
36
|
+
: [{ label: "Save to repo…", busyLabel: "Saving…", kind: "pick", onPress: () => setPicking(true) }];
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "center", columnGap: 8, rowGap: 4, marginHorizontal: 12, marginTop: 16 }}>
|
|
40
|
+
<View style={{ flexDirection: "row", alignItems: "flex-start", gap: 6, flex: 1, minWidth: 180 }}>
|
|
41
|
+
<View style={{ paddingTop: 2 }}><Icon name="History" size={13} color={colors.foregroundMuted} /></View>
|
|
42
|
+
<Text style={{ flex: 1, color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
|
|
43
|
+
{savedTo ? <>Each run is saved to <Text style={{ color: colors.foreground }}>{savedTo}</Text> in this repo.</> : "Run history stays on this computer."}
|
|
44
|
+
</Text>
|
|
45
|
+
</View>
|
|
46
|
+
<View style={{ flexDirection: "row", gap: 4 }}>
|
|
47
|
+
{actions.map((action) => (
|
|
48
|
+
<PressScale key={action.label} accessibilityRole="button" disabled={mutation.isPending} onPress={action.onPress}
|
|
49
|
+
style={({ pressed, hovered }) => ({ paddingHorizontal: 8, paddingVertical: 4, borderRadius: 4, opacity: mutation.isPending ? 0.7 : 1,
|
|
50
|
+
backgroundColor: pressed ? colors.border : hovered ? colors.surface2 : "transparent" })}>
|
|
51
|
+
<Text style={{ color: colors.foreground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>{busy === action.kind ? action.busyLabel : action.label}</Text>
|
|
52
|
+
</PressScale>
|
|
53
|
+
))}
|
|
54
|
+
</View>
|
|
55
|
+
<FolderPicker key={picking ? "open" : "closed"} colors={colors} workspaceId={workspaceId} directory={directory} open={picking} start={savedTo ?? ""}
|
|
56
|
+
onClose={() => setPicking(false)} onPick={(folder) => { setPicking(false); mutation.mutate(folder); }} />
|
|
57
|
+
</View>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Browses the repo's folders on the daemon host. A new folder only needs a
|
|
62
|
+
// name: it's made the first time a run is saved there.
|
|
63
|
+
function FolderPicker({ colors, workspaceId, directory, open, start, onClose, onPick }: {
|
|
64
|
+
colors: Colors;
|
|
65
|
+
workspaceId: string;
|
|
66
|
+
directory: string;
|
|
67
|
+
open: boolean;
|
|
68
|
+
// The folder chosen so far; browsing starts in the folder that holds it.
|
|
69
|
+
start: string;
|
|
70
|
+
onClose(): void;
|
|
71
|
+
onPick(folder: string): void;
|
|
72
|
+
}) {
|
|
73
|
+
const parentOf = (path: string) => path.split("/").slice(0, -1).join("/");
|
|
74
|
+
const [at, setAt] = useState(() => parentOf(start));
|
|
75
|
+
const [naming, setNaming] = useState<string | null>(null);
|
|
76
|
+
const list = useRpc(listFolders);
|
|
77
|
+
const listed = useQuery({
|
|
78
|
+
queryKey: ["progress", "folders", workspaceId, directory, at],
|
|
79
|
+
queryFn: () => list({ workspaceId, workspaceDirectory: directory, folder: at }),
|
|
80
|
+
enabled: open,
|
|
81
|
+
});
|
|
82
|
+
const join = (name: string) => (at ? `${at}/${name}` : name);
|
|
83
|
+
const crumbs = at ? at.split("/") : [];
|
|
84
|
+
const close = () => { setNaming(null); setAt(parentOf(start)); onClose(); };
|
|
85
|
+
const newName = naming?.trim().replace(/\/+$/, "") ?? "";
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<Modal title="Save run history to a folder" icon={<Icon name="FolderOpen" size={16} color={colors.foregroundMuted} />} open={open} onOpenChange={(next) => { if (!next) close(); }}>
|
|
89
|
+
{/* Paseo sizes a non-scrolling dialog to 85% of the window, so let it size to
|
|
90
|
+
its content; the list's own height cap keeps the buttons in view. */}
|
|
91
|
+
<Modal.Content>
|
|
92
|
+
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
|
|
93
|
+
Each run becomes its own file in this folder, in every worktree of this repo. Commit them like any other file.
|
|
94
|
+
</Text>
|
|
95
|
+
<View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "center", gap: 2, paddingHorizontal: 4 }}>
|
|
96
|
+
{["Top of the repo", ...crumbs].map((crumb, index) => (
|
|
97
|
+
<React.Fragment key={index}>
|
|
98
|
+
{index ? <Icon name="ChevronRight" size={12} color={colors.foregroundMuted} /> : null}
|
|
99
|
+
<PressScale accessibilityRole="button" disabled={index === crumbs.length} onPress={() => { setNaming(null); setAt(crumbs.slice(0, index).join("/")); }}
|
|
100
|
+
style={({ pressed, hovered }) => ({ paddingHorizontal: 4, paddingVertical: 2, borderRadius: 4,
|
|
101
|
+
backgroundColor: index === crumbs.length ? "transparent" : pressed ? colors.border : hovered ? colors.surface2 : "transparent" })}>
|
|
102
|
+
<Text style={{ color: index === crumbs.length ? colors.foreground : colors.foregroundMuted, fontSize: 12, lineHeight: 16, fontWeight: index === crumbs.length ? "600" : "400" }}>{crumb}</Text>
|
|
103
|
+
</PressScale>
|
|
104
|
+
</React.Fragment>
|
|
105
|
+
))}
|
|
106
|
+
</View>
|
|
107
|
+
|
|
108
|
+
{/* Only the list scrolls, so Use and Cancel stay in view in a big repo. */}
|
|
109
|
+
<ScrollView style={{ maxHeight: 320, flexGrow: 0 }} contentContainerStyle={{ gap: 2 }}>
|
|
110
|
+
{listed.isPending ? <ActivityIndicator color={colors.foregroundMuted} accessibilityLabel="Loading folders" style={{ alignSelf: "flex-start", padding: 8 }} /> : null}
|
|
111
|
+
{listed.data?.folders.map((name) => (
|
|
112
|
+
<PressableRow key={name} colors={colors} accessibilityRole="button" accessibilityLabel={`Open ${name}`} onPress={() => { setNaming(null); setAt(join(name)); }}
|
|
113
|
+
style={{ flexDirection: "row", alignItems: "center", gap: 8, paddingHorizontal: 8, paddingVertical: 6, borderRadius: 6 }}>
|
|
114
|
+
<Icon name="Folder" size={14} color={colors.foregroundMuted} />
|
|
115
|
+
<Text numberOfLines={1} ellipsizeMode="middle" style={{ flex: 1, color: colors.foreground, fontSize: 12, lineHeight: 16 }}>{name}</Text>
|
|
116
|
+
<Icon name="ChevronRight" size={14} color={colors.foregroundMuted} />
|
|
117
|
+
</PressableRow>
|
|
118
|
+
))}
|
|
119
|
+
{listed.data && !listed.data.folders.length ? (
|
|
120
|
+
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 16, paddingHorizontal: 8, paddingVertical: 6 }}>No folders in here.</Text>
|
|
121
|
+
) : null}
|
|
122
|
+
{listed.error ? <Text accessibilityRole="alert" style={{ color: colors.statusDanger, fontSize: 12, lineHeight: 17, paddingHorizontal: 8 }}>{listed.error.message}</Text> : null}
|
|
123
|
+
</ScrollView>
|
|
124
|
+
|
|
125
|
+
{naming === null ? (
|
|
126
|
+
<PressScale accessibilityRole="button" onPress={() => setNaming("")}
|
|
127
|
+
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" })}>
|
|
128
|
+
<Icon name="FolderPlus" size={14} color={colors.foreground} />
|
|
129
|
+
<Text style={{ color: colors.foreground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>New folder</Text>
|
|
130
|
+
</PressScale>
|
|
131
|
+
) : (
|
|
132
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
|
133
|
+
<TextInput autoFocus value={naming} onChangeText={setNaming} onSubmitEditing={() => { if (newName) onPick(join(newName)); }}
|
|
134
|
+
autoCapitalize="none" autoCorrect={false} spellCheck={false} accessibilityLabel="New folder name" placeholder="progress-history" placeholderTextColor={colors.foregroundMuted}
|
|
135
|
+
style={{ flex: 1, color: colors.foreground, fontSize: 12, lineHeight: 16, paddingHorizontal: 8, paddingVertical: 6, borderRadius: 6, borderWidth: 1, borderColor: colors.border, backgroundColor: colors.surface1 }} />
|
|
136
|
+
<PressScale accessibilityRole="button" disabled={!newName} onPress={() => onPick(join(newName))}
|
|
137
|
+
style={({ pressed, hovered }) => ({ paddingHorizontal: 10, paddingVertical: 6, borderRadius: 6, backgroundColor: colors.accent, opacity: !newName ? 0.5 : pressed ? 0.7 : hovered ? 0.85 : 1 })}>
|
|
138
|
+
<Text style={{ color: colors.accentForeground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>Use new folder</Text>
|
|
139
|
+
</PressScale>
|
|
140
|
+
</View>
|
|
141
|
+
)}
|
|
142
|
+
|
|
143
|
+
<View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "center", justifyContent: "flex-end", gap: 8 }}>
|
|
144
|
+
{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>}
|
|
145
|
+
<PressScale accessibilityRole="button" onPress={close}
|
|
146
|
+
style={({ pressed, hovered }) => ({ paddingHorizontal: 12, paddingVertical: 6, borderRadius: 6, backgroundColor: pressed ? colors.border : hovered ? colors.surface2 : "transparent" })}>
|
|
147
|
+
<Text style={{ color: colors.foreground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>Cancel</Text>
|
|
148
|
+
</PressScale>
|
|
149
|
+
<PressScale accessibilityRole="button" disabled={!at} onPress={() => onPick(at)} outerStyle={{ flexShrink: 1 }}
|
|
150
|
+
style={({ pressed, hovered }) => ({ paddingHorizontal: 12, paddingVertical: 6, borderRadius: 6, backgroundColor: colors.accent, opacity: !at ? 0.5 : pressed ? 0.7 : hovered ? 0.85 : 1 })}>
|
|
151
|
+
<Text numberOfLines={1} ellipsizeMode="middle" style={{ color: colors.accentForeground, fontSize: 12, lineHeight: 16, fontWeight: "600" }}>{at ? `Use ${crumbs[crumbs.length - 1]}` : "Use folder"}</Text>
|
|
152
|
+
</PressScale>
|
|
153
|
+
</View>
|
|
154
|
+
</Modal.Content>
|
|
155
|
+
</Modal>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
|
|
3
|
+
// Paseo opens the Explorer sidebar at 320px and plugins can't ask for more,
|
|
4
|
+
// so at or below this width the panel trims what it shows to fit.
|
|
5
|
+
export const NARROW_MAX = 400;
|
|
6
|
+
|
|
7
|
+
const Narrow = createContext(false);
|
|
8
|
+
|
|
9
|
+
export const NarrowProvider = Narrow.Provider;
|
|
10
|
+
|
|
11
|
+
export function useNarrow(): boolean {
|
|
12
|
+
return useContext(Narrow);
|
|
13
|
+
}
|
package/client/panel.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { type PluginWorkspacePanelProps, useRpc, useWorkspace } from "@getpaseo/
|
|
|
2
2
|
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
3
3
|
import { DeliverablesList } from "./deliverables";
|
|
4
4
|
import { LauncherBanner, SkillBanner } from "./launcher";
|
|
5
|
+
import { HistoryRow } from "./history";
|
|
5
6
|
import { AnsweredQuestionsList, QuestionsSection } from "./questions";
|
|
6
7
|
import { Spinner, StalledPulse, WaitingDot } from "./spinner";
|
|
7
8
|
import { IconSwap, Presence, StaggerRoot, nativeDriver } from "./motion";
|
|
@@ -11,7 +12,7 @@ import { When } from "./when";
|
|
|
11
12
|
import React, { useEffect, useRef, useState } from "react";
|
|
12
13
|
import { ActivityIndicator, Animated, Platform, Pressable, ScrollView, Text, View } from "react-native";
|
|
13
14
|
import { finishedToFold, isFinished, shortTitle, type Activity, type Dashboard, type FileIssue, type ProgressSegment, type StuckItem, type Ticket } from "../shared/dashboard";
|
|
14
|
-
import {
|
|
15
|
+
import type { TicketStatus } from "../shared/events";
|
|
15
16
|
import { formatEstimate, formatMinutes, formatWorkDone, minutesSince } from "../shared/format";
|
|
16
17
|
import { markPanelOpened } from "../shared/rpc";
|
|
17
18
|
import { useDashboard } from "./dashboard-query";
|
|
@@ -20,6 +21,7 @@ import { TicketDialogs } from "./ticket-dialog";
|
|
|
20
21
|
import { PressableRow } from "./row";
|
|
21
22
|
import { ShowMoreRow, usePaged } from "./show-more";
|
|
22
23
|
import { notePanelOpened } from "./panel-opened";
|
|
24
|
+
import { NARROW_MAX, NarrowProvider, useNarrow } from "./narrow";
|
|
23
25
|
|
|
24
26
|
type Colors = PluginWorkspacePanelProps["theme"]["colors"];
|
|
25
27
|
|
|
@@ -39,9 +41,12 @@ function WorkspaceProgress({ theme, workspaceId, host, layout, navigation }: Plu
|
|
|
39
41
|
const query = useDashboard(host.id, workspaceId, directory);
|
|
40
42
|
const result = query.data ?? null;
|
|
41
43
|
useMarkPanelOpened(workspaceId, directory, Boolean(result?.configured && !result.panelOpened));
|
|
44
|
+
const [narrow, setNarrow] = useState(false);
|
|
42
45
|
|
|
43
46
|
return (
|
|
44
|
-
<
|
|
47
|
+
<NarrowProvider value={narrow}>
|
|
48
|
+
<ScrollView onLayout={(event) => setNarrow(event.nativeEvent.layout.width <= NARROW_MAX)}
|
|
49
|
+
style={{ flex: 1, backgroundColor: colors.surface0 }} contentContainerStyle={{ paddingBottom: 16 }}>
|
|
45
50
|
{query.isPending ? <ActivityIndicator color={colors.foregroundMuted} accessibilityLabel="Loading progress" style={{ padding: 12 }} /> : null}
|
|
46
51
|
{query.error ? (
|
|
47
52
|
<Text accessibilityRole="alert" selectable style={{ color: colors.statusDanger, fontSize: 12, lineHeight: 18, padding: 12 }}>
|
|
@@ -51,11 +56,13 @@ function WorkspaceProgress({ theme, workspaceId, host, layout, navigation }: Plu
|
|
|
51
56
|
<LauncherBanner colors={colors} host={host} />
|
|
52
57
|
<SkillBanner colors={colors} host={host} />
|
|
53
58
|
{/* Fades in like the dashboard's sections instead of popping in after the spinner. */}
|
|
54
|
-
<Presence show={Boolean(result && !result.configured)}>{result && !result.configured ? <EmptyState colors={colors} /> : null}</Presence>
|
|
59
|
+
<Presence show={Boolean(result && !result.configured)}>{result && !result.configured ? <EmptyState colors={colors} file={result.file} /> : null}</Presence>
|
|
55
60
|
{/* Keyed by run: a new run starts with fresh tabs, pages and dialogs. */}
|
|
56
61
|
{result?.configured ? <DashboardView key={result.dashboard.run?.startedAt ?? "none"} colors={colors} dashboard={result.dashboard} agentRunning={agentRunning} compact={layout.compact}
|
|
57
|
-
workspaceId={workspaceId} workspaceDirectory={result.root} navigation={navigation} /> : null}
|
|
62
|
+
workspaceId={workspaceId} workspaceDirectory={result.root} file={result.file} navigation={navigation} /> : null}
|
|
63
|
+
{result?.inRepo && directory ? <HistoryRow colors={colors} workspaceId={workspaceId} directory={directory} savedTo={result.savedTo} /> : null}
|
|
58
64
|
</ScrollView>
|
|
65
|
+
</NarrowProvider>
|
|
59
66
|
);
|
|
60
67
|
}
|
|
61
68
|
|
|
@@ -74,7 +81,7 @@ function useMarkPanelOpened(workspaceId: string, directory: string | null, neede
|
|
|
74
81
|
}, [needed, directory, workspaceId, mark]);
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
function EmptyState({ colors }: { colors: Colors }) {
|
|
84
|
+
function EmptyState({ colors, file }: { colors: Colors; file: string }) {
|
|
78
85
|
return (
|
|
79
86
|
<View style={{ padding: 12, paddingTop: 20, gap: 10 }}>
|
|
80
87
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
@@ -91,9 +98,9 @@ function EmptyState({ colors }: { colors: Colors }) {
|
|
|
91
98
|
{`paseo-progress start "Build the export feature"\npaseo-progress ticket add "Ticket 01: Export button" --estimate 60`}
|
|
92
99
|
</Text>
|
|
93
100
|
</View>
|
|
94
|
-
{/* So
|
|
101
|
+
{/* So people know nothing lands in the repo. */}
|
|
95
102
|
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
|
|
96
|
-
Progress is saved in <Text style={{ color: colors.foreground }}
|
|
103
|
+
Progress is saved outside the repo, in <Text selectable style={{ color: colors.foreground }}>{file}</Text>, so git never sees it.
|
|
97
104
|
</Text>
|
|
98
105
|
</View>
|
|
99
106
|
);
|
|
@@ -122,13 +129,14 @@ function useNow(intervalMs: number): number {
|
|
|
122
129
|
|
|
123
130
|
const WIDE_MIN = 760;
|
|
124
131
|
|
|
125
|
-
function DashboardView({ colors, dashboard, agentRunning, compact, workspaceId, workspaceDirectory, navigation }: {
|
|
132
|
+
function DashboardView({ colors, dashboard, agentRunning, compact, workspaceId, workspaceDirectory, file, navigation }: {
|
|
126
133
|
colors: Colors;
|
|
127
134
|
dashboard: Dashboard;
|
|
128
135
|
agentRunning: boolean;
|
|
129
136
|
compact: boolean;
|
|
130
137
|
workspaceId: string;
|
|
131
138
|
workspaceDirectory: string;
|
|
139
|
+
file: string;
|
|
132
140
|
navigation: PluginWorkspacePanelProps["navigation"];
|
|
133
141
|
}) {
|
|
134
142
|
const now = useNow(15_000);
|
|
@@ -198,7 +206,7 @@ function DashboardView({ colors, dashboard, agentRunning, compact, workspaceId,
|
|
|
198
206
|
</Text>
|
|
199
207
|
</View>
|
|
200
208
|
) : null}</Presence>
|
|
201
|
-
<Presence show={Boolean(dashboard.issues.length)} order={1}>{dashboard.issues.length ? <IssuesNotice colors={colors} issues={dashboard.issues} /> : null}</Presence>
|
|
209
|
+
<Presence show={Boolean(dashboard.issues.length)} order={1}>{dashboard.issues.length ? <IssuesNotice colors={colors} issues={dashboard.issues} file={file} /> : null}</Presence>
|
|
202
210
|
<Presence show={Boolean(dashboard.progress.totalMin > 0)} order={2}>{dashboard.progress.totalMin > 0 ? <ProgressBar colors={colors} progress={dashboard.progress} live={live} /> : null}</Presence>
|
|
203
211
|
<View onLayout={(event) => setWide(event.nativeEvent.layout.width >= WIDE_MIN)}
|
|
204
212
|
style={{ flexDirection: wide ? "row" : "column", alignItems: "flex-start" }}>
|
|
@@ -253,6 +261,7 @@ const historyTabs = new Map<string, HistoryTab>();
|
|
|
253
261
|
function HistoryCard({ colors, workspaceId, tabs }: { colors: Colors; workspaceId: string; tabs: HistoryTabEntry[] }) {
|
|
254
262
|
const [tab, setTab] = useState<HistoryTab>(() => historyTabs.get(workspaceId) ?? "activity");
|
|
255
263
|
const [showIcons, setShowIcons] = useState(true);
|
|
264
|
+
const narrow = useNarrow();
|
|
256
265
|
const choose = (next: HistoryTab) => {
|
|
257
266
|
historyTabs.set(workspaceId, next);
|
|
258
267
|
setTab(next);
|
|
@@ -261,9 +270,9 @@ function HistoryCard({ colors, workspaceId, tabs }: { colors: Colors; workspaceI
|
|
|
261
270
|
return (
|
|
262
271
|
<View onLayout={(event) => setShowIcons(event.nativeEvent.layout.width >= TAB_ICONS_MIN)}
|
|
263
272
|
style={{ margin: 12, marginBottom: 0, ...raised(colors), borderRadius: 6, overflow: "hidden" }}>
|
|
264
|
-
<View accessibilityRole="tablist" style={{ flexDirection: "row", flexWrap: "wrap", columnGap: 16, paddingHorizontal: 10, borderBottomWidth: 1, borderBottomColor: colors.border }}>
|
|
273
|
+
<View accessibilityRole="tablist" style={{ flexDirection: "row", flexWrap: "wrap", columnGap: narrow ? 10 : 16, paddingHorizontal: narrow ? 8 : 10, borderBottomWidth: 1, borderBottomColor: colors.border }}>
|
|
265
274
|
{tabs.map((entry) => {
|
|
266
|
-
return <Tab key={entry.id} colors={colors} label={entry.label} icon={showIcons ? entry.icon : null} count={entry.count} selected={entry.id === current.id} onPress={() => choose(entry.id)} />;
|
|
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)} />;
|
|
267
276
|
})}
|
|
268
277
|
</View>
|
|
269
278
|
{current.count === 0 ? (
|
|
@@ -274,13 +283,14 @@ function HistoryCard({ colors, workspaceId, tabs }: { colors: Colors; workspaceI
|
|
|
274
283
|
}
|
|
275
284
|
|
|
276
285
|
// Instant feedback: tabs switch often, so hover and press change color only.
|
|
277
|
-
|
|
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 }) {
|
|
278
288
|
const [hovered, setHovered] = useState(false);
|
|
279
289
|
return (
|
|
280
290
|
<Pressable accessibilityRole="tab" accessibilityState={{ selected }} onPress={onPress}
|
|
281
291
|
onHoverIn={() => setHovered(true)} onHoverOut={() => setHovered(false)}
|
|
282
292
|
style={({ pressed }) => ({
|
|
283
|
-
flexDirection: "row", alignItems: "center", gap: 6, paddingTop: 8, paddingBottom: 6, marginBottom: -1,
|
|
293
|
+
flexDirection: "row", alignItems: "center", gap: narrow ? 4 : 6, paddingTop: 8, paddingBottom: 6, marginBottom: -1,
|
|
284
294
|
borderBottomWidth: 2,
|
|
285
295
|
borderBottomColor: selected ? colors.accent : pressed || hovered ? colors.border : "transparent",
|
|
286
296
|
})}>
|
|
@@ -314,13 +324,13 @@ function ActivityList({ colors, activity, now }: { colors: Colors; activity: Act
|
|
|
314
324
|
}
|
|
315
325
|
|
|
316
326
|
// Small and non-blocking: the rest of the dashboard still renders.
|
|
317
|
-
function IssuesNotice({ colors, issues }: { colors: Colors; issues: FileIssue[] }) {
|
|
327
|
+
function IssuesNotice({ colors, issues, file }: { colors: Colors; issues: FileIssue[]; file: string }) {
|
|
318
328
|
const shown = issues.slice(0, 5);
|
|
319
329
|
return (
|
|
320
330
|
<View style={{ flexDirection: "row", gap: 8, margin: 12, marginBottom: 0, padding: 8, borderRadius: 6, backgroundColor: colors.surface1 }}>
|
|
321
331
|
<View style={{ paddingTop: 1.5 }}><Icon name="FileWarning" size={14} color={colors.statusWarning} /></View>
|
|
322
332
|
<Text selectable style={{ flex: 1, color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
|
|
323
|
-
Skipped {issues.length === 1 ? "1 line" : `${issues.length} lines`} in {
|
|
333
|
+
Skipped {issues.length === 1 ? "1 line" : `${issues.length} lines`} in {file}:{" "}
|
|
324
334
|
{shown.map((issue) => `line ${issue.line} (${issue.reason})`).join(", ")}
|
|
325
335
|
{issues.length > shown.length ? `, and ${issues.length - shown.length} more` : ""}.
|
|
326
336
|
</Text>
|
|
@@ -338,11 +348,12 @@ function Card({ colors, icon, title, children }: { colors: Colors; icon: string;
|
|
|
338
348
|
}
|
|
339
349
|
|
|
340
350
|
function ProgressBar({ colors, progress, live }: { colors: Colors; progress: Dashboard["progress"]; live: boolean }) {
|
|
351
|
+
const narrow = useNarrow();
|
|
341
352
|
return (
|
|
342
353
|
<View style={{ paddingHorizontal: 12, paddingTop: 12, paddingBottom: 12, gap: 8, borderBottomWidth: 1, borderBottomColor: colors.border }}>
|
|
343
354
|
<View style={{ flexDirection: "row", alignItems: "baseline", gap: 8 }}>
|
|
344
355
|
<Text style={{ flex: 1, color: colors.foregroundMuted, fontSize: 13, lineHeight: 18 }}>
|
|
345
|
-
<Text style={{ color: colors.foreground, fontWeight: "600" }}>{progress.percent}%</Text> of estimated work done
|
|
356
|
+
<Text style={{ color: colors.foreground, fontWeight: "600" }}>{progress.percent}%</Text> {narrow ? "done" : "of estimated work done"}
|
|
346
357
|
</Text>
|
|
347
358
|
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 18 }}>
|
|
348
359
|
{formatWorkDone(progress.doneMin, progress.totalMin, progress.tookMin)}
|
|
@@ -390,6 +401,7 @@ function Shimmer() {
|
|
|
390
401
|
const STUCK_ICON: Record<StuckItem["kind"], string> = { blocked: "Ban", overdue: "Hourglass", manual: "Flag" };
|
|
391
402
|
|
|
392
403
|
function StuckSection({ colors, items, now }: { colors: Colors; items: StuckItem[]; now: number }) {
|
|
404
|
+
const narrow = useNarrow();
|
|
393
405
|
return (
|
|
394
406
|
<View style={{ margin: 12, marginBottom: 0, borderWidth: 1, borderColor: colors.statusDanger, borderRadius: 6, overflow: "hidden" }}>
|
|
395
407
|
<SectionTitle colors={colors} icon="OctagonAlert" title="Stuck" color={colors.statusDanger} style={{ borderBottomWidth: 1, borderBottomColor: colors.border }} />
|
|
@@ -401,7 +413,7 @@ function StuckSection({ colors, items, now }: { colors: Colors; items: StuckItem
|
|
|
401
413
|
<Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 18 }}>{item.title}</Text>
|
|
402
414
|
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
|
|
403
415
|
{item.kind === "overdue" ? `No update for longer than its ${formatMinutes(item.estimateMin)} estimate. Last update ` : null}
|
|
404
|
-
{item.kind === "blocked" ? `Blocked${item.note ? `: ${item.note}` : ""}. Since ` : null}
|
|
416
|
+
{item.kind === "blocked" ? `Blocked${item.note ? `: ${item.note}` : ""}${narrow ? ", " : ". Since "}` : null}
|
|
405
417
|
{item.kind === "manual" ? `${item.ticketId ? `${item.reason}. ` : ""}Flagged ` : null}
|
|
406
418
|
<When colors={colors} iso={item.since} now={now} />
|
|
407
419
|
</Text>
|
|
@@ -447,6 +459,7 @@ function FinishedRow({ colors, tickets, shown, onPress }: { colors: Colors; tick
|
|
|
447
459
|
const label = [done ? `${done} done` : "", skipped ? `${skipped} skipped` : ""].filter(Boolean).join(", ");
|
|
448
460
|
const estimateMin = tickets.reduce((sum, ticket) => sum + (ticket.status === "done" ? ticket.estimateMin : 0), 0);
|
|
449
461
|
const chevron = shown ? "ChevronUp" : "ChevronDown";
|
|
462
|
+
const narrow = useNarrow();
|
|
450
463
|
return (
|
|
451
464
|
<PressableRow colors={colors} accessibilityRole="button" accessibilityState={{ expanded: shown }}
|
|
452
465
|
accessibilityLabel={`${shown ? "Hide" : "Show"} ${label}`} onPress={onPress}
|
|
@@ -455,8 +468,11 @@ function FinishedRow({ colors, tickets, shown, onPress }: { colors: Colors; tick
|
|
|
455
468
|
<Icon name={done ? "Check" : "CircleSlash"} size={14} color={done ? colors.statusSuccess : colors.foregroundMuted} />
|
|
456
469
|
</View>
|
|
457
470
|
<Text style={{ flex: 1, minWidth: 0, color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontWeight: "600", fontVariant: ["tabular-nums"] }}>{label}</Text>
|
|
458
|
-
|
|
459
|
-
|
|
471
|
+
{/* Narrow ticket rows drop their chevron, so this total would sit off their estimate column. */}
|
|
472
|
+
{narrow ? null : <>
|
|
473
|
+
<View style={{ width: 12 }} />
|
|
474
|
+
<Text style={{ minWidth: 40, flexShrink: 0, textAlign: "right", color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontVariant: ["tabular-nums"] }}>{estimateMin ? formatEstimate(estimateMin) : ""}</Text>
|
|
475
|
+
</>}
|
|
460
476
|
<View style={{ paddingTop: 2 }}>
|
|
461
477
|
<IconSwap swapKey={chevron} size={14}><Icon name={chevron} size={14} color={colors.foregroundMuted} /></IconSwap>
|
|
462
478
|
</View>
|
|
@@ -468,12 +484,14 @@ function FinishedRow({ colors, tickets, shown, onPress }: { colors: Colors; tick
|
|
|
468
484
|
function TicketRow({ colors, ticket, live, now, onOpen, deliverables }: { colors: Colors; ticket: Ticket; live: boolean; now: number; onOpen(): void; deliverables: number }) {
|
|
469
485
|
const muted = ticket.status === "skipped";
|
|
470
486
|
const working = ticket.status === "working";
|
|
487
|
+
// Narrow: tighter gaps and no chevron (the whole row still presses), so the title keeps its width.
|
|
488
|
+
const narrow = useNarrow();
|
|
471
489
|
return (
|
|
472
490
|
<PressableRow colors={colors} onSurface1={working} transparentAtRest={!working}
|
|
473
491
|
accessibilityRole="button" accessibilityLabel={`${ticket.id} details`} onPress={onOpen}
|
|
474
|
-
style={{ flexDirection: "row", alignItems: "flex-start", gap: 10, paddingHorizontal: 10, paddingVertical: 8, borderTopWidth: 1, borderTopColor: colors.border }}>
|
|
492
|
+
style={{ flexDirection: "row", alignItems: "flex-start", gap: narrow ? 6 : 10, paddingHorizontal: 10, paddingVertical: 8, borderTopWidth: 1, borderTopColor: colors.border }}>
|
|
475
493
|
{/* The icon is the only status mark on the row; the ticket dialog spells it out in a badge. */}
|
|
476
|
-
<View accessible accessibilityLabel={ticket.waitingFor ? "Waiting" : STATUS_LABEL[ticket.status]} style={{ width: 16, paddingTop: 2, alignItems: "center" }}>
|
|
494
|
+
<View accessible accessibilityLabel={ticket.waitingFor ? "Waiting" : STATUS_LABEL[ticket.status]} style={{ width: 16, paddingTop: 2, alignItems: "center", marginRight: narrow ? 2 : 0 }}>
|
|
477
495
|
<IconSwap swapKey={ticket.waitingFor || ticket.status === "not_started" ? "waiting" : ticket.status === "working" ? `working-${live}` : ticket.status} size={14}><StatusIcon colors={colors} status={ticket.status} live={live} waiting={Boolean(ticket.waitingFor)} /></IconSwap>
|
|
478
496
|
</View>
|
|
479
497
|
<View style={{ flex: 1, minWidth: 0, gap: 2 }}>
|
|
@@ -487,7 +505,7 @@ function TicketRow({ colors, ticket, live, now, onOpen, deliverables }: { colors
|
|
|
487
505
|
) : null}
|
|
488
506
|
{ticket.status === "working" && ticket.workingSince ? (
|
|
489
507
|
<Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 }}>
|
|
490
|
-
Working since <When colors={colors} iso={ticket.workingSince} now={now} />
|
|
508
|
+
{narrow ? "Started working " : "Working since "}<When colors={colors} iso={ticket.workingSince} now={now} />
|
|
491
509
|
</Text>
|
|
492
510
|
) : null}
|
|
493
511
|
{ticket.waitingFor ? (
|
|
@@ -500,7 +518,7 @@ function TicketRow({ colors, ticket, live, now, onOpen, deliverables }: { colors
|
|
|
500
518
|
{deliverables ? <Icon name="Package" size={12} color={colors.foregroundMuted} /> : null}
|
|
501
519
|
</View>
|
|
502
520
|
<Text style={{ minWidth: 40, flexShrink: 0, textAlign: "right", color: colors.foregroundMuted, fontSize: 12, lineHeight: 18, fontVariant: ["tabular-nums"] }}>{formatEstimate(ticket.estimateMin)}</Text>
|
|
503
|
-
<View style={{ paddingTop: 2 }}><Icon name="ChevronRight" size={14} color={colors.foregroundMuted} /></View>
|
|
521
|
+
{narrow ? null : <View style={{ paddingTop: 2 }}><Icon name="ChevronRight" size={14} color={colors.foregroundMuted} /></View>}
|
|
504
522
|
</PressableRow>
|
|
505
523
|
);
|
|
506
524
|
}
|
package/client/questions.tsx
CHANGED
|
@@ -8,6 +8,7 @@ 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";
|
|
11
12
|
import { raised } from "./surfaces";
|
|
12
13
|
import { When } from "./when";
|
|
13
14
|
|
|
@@ -140,6 +141,9 @@ function QuestionRow({ colors, question, first, onOpen, onCopy }: {
|
|
|
140
141
|
onOpen(): void;
|
|
141
142
|
onCopy(): void;
|
|
142
143
|
}) {
|
|
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} />;
|
|
143
147
|
return (
|
|
144
148
|
<View style={{ flexDirection: "row", gap: 8, paddingHorizontal: 10, paddingVertical: 10, borderTopWidth: first ? 0 : 1, borderTopColor: colors.border }}>
|
|
145
149
|
<PressScale accessibilityRole="button" accessibilityLabel={`Copy ${question.id} for your reply`} onPress={onCopy} hitSlop={6}
|
|
@@ -154,8 +158,11 @@ function QuestionRow({ colors, question, first, onOpen, onCopy }: {
|
|
|
154
158
|
</PressScale>
|
|
155
159
|
<PressableRow colors={colors} accessibilityRole="button" accessibilityLabel={`${question.id} choices`} onPress={onOpen}
|
|
156
160
|
style={{ flex: 1, minWidth: 0, flexDirection: "row", alignItems: "flex-start", gap: 8, borderRadius: 4, margin: -4, padding: 4 }}>
|
|
157
|
-
<
|
|
158
|
-
|
|
161
|
+
<View style={{ flex: 1, minWidth: 0, gap: 6, alignItems: "flex-start" }}>
|
|
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}
|
|
159
166
|
{/* Optical: centers the 14px chevron on the 19px first line. */}
|
|
160
167
|
<View style={{ paddingTop: 2.5 }}><Icon name="ChevronRight" size={14} color={colors.foregroundMuted} /></View>
|
|
161
168
|
</PressableRow>
|
package/client/when.tsx
CHANGED
|
@@ -2,11 +2,14 @@ import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Text } from "react-native";
|
|
4
4
|
import { formatAgo, formatClock } from "../shared/format";
|
|
5
|
+
import { useNarrow } from "./narrow";
|
|
5
6
|
|
|
6
7
|
type Colors = PluginWorkspacePanelProps["theme"]["colors"];
|
|
7
8
|
|
|
8
|
-
// "Sep 27, 8:14 PM (10 h 2 min ago)", inline inside other text.
|
|
9
|
+
// "Sep 27, 8:14 PM (10 h 2 min ago)", inline inside other text. A narrow
|
|
10
|
+
// panel shows only "10 h 2 min ago"; the ticket dialog keeps the clock time.
|
|
9
11
|
export function When({ colors, iso, now }: { colors: Colors; iso: string; now: number }) {
|
|
12
|
+
if (useNarrow()) return <Text>{formatAgo(iso, now)}</Text>;
|
|
10
13
|
return (
|
|
11
14
|
<Text>
|
|
12
15
|
{formatClock(iso)} <Text style={{ color: colors.foregroundMuted, opacity: 0.7 }}>({formatAgo(iso, now)})</Text>
|
package/dist/server/cli.js
CHANGED
|
@@ -5,10 +5,11 @@ import { relative, resolve } from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
7
7
|
import { isSettled } from "../shared/dashboard.js";
|
|
8
|
-
import { DELIVERABLE_KINDS,
|
|
8
|
+
import { DELIVERABLE_KINDS, TICKET_STATUSES } from "../shared/events.js";
|
|
9
9
|
import { appendProgress, InvalidEvent, readProgress } from "./progress-file.js";
|
|
10
10
|
import { findRoot, outside } from "./paths.js";
|
|
11
11
|
import { installLauncher } from "./launcher.js";
|
|
12
|
+
import { findStore, saveRunCopy } from "./store.js";
|
|
12
13
|
import { dashboardText } from "../shared/show.js";
|
|
13
14
|
import { imageMimeType } from "../shared/attachments.js";
|
|
14
15
|
import { homedir } from "node:os";
|
|
@@ -426,7 +427,7 @@ function help() {
|
|
|
426
427
|
return [
|
|
427
428
|
`Usage: ${USAGE_NAME} <command>`,
|
|
428
429
|
"",
|
|
429
|
-
`Records progress
|
|
430
|
+
`Records progress for this worktree, shown by the Progress panel in Paseo. The file lives outside the repo, so git never sees it.`,
|
|
430
431
|
`Add --help after any command for its options.`,
|
|
431
432
|
"",
|
|
432
433
|
...Object.values(commands).flatMap((command) => [` ${command.usage}`, ` ${command.summary}`]),
|
|
@@ -451,7 +452,8 @@ export async function runCli(argv, options) {
|
|
|
451
452
|
}
|
|
452
453
|
if (first === "show") {
|
|
453
454
|
const root = findRoot(options.cwd);
|
|
454
|
-
|
|
455
|
+
const store = await findStore(root);
|
|
456
|
+
out(`Worktree: ${root}\n\n${dashboardText((await readProgress(store.directory, now())).dashboard, now().getTime())}`);
|
|
455
457
|
return 0;
|
|
456
458
|
}
|
|
457
459
|
const target = resolve(options.cwd, (argv[1] ?? DEFAULT_LAUNCHER).replace(/^~(?=$|\/)/, homedir()));
|
|
@@ -478,14 +480,15 @@ export async function runCli(argv, options) {
|
|
|
478
480
|
throw new UsageError(error.message);
|
|
479
481
|
}
|
|
480
482
|
const root = findRoot(options.cwd);
|
|
481
|
-
const
|
|
483
|
+
const store = await findStore(root);
|
|
484
|
+
const message = await appendProgress(store.directory, now, (state) => {
|
|
482
485
|
const finished = state.dashboard.run?.finished;
|
|
483
486
|
if (finished && command !== commands.start) {
|
|
484
487
|
throw new UsageError(`The run "${state.dashboard.run.title}" is finished. Start new work with: ${USAGE_NAME} start "<title>"`);
|
|
485
488
|
}
|
|
486
489
|
const { event, message } = command.run({ positionals: parsed.positionals, values: parsed.values, state, cwd: options.cwd, root });
|
|
487
490
|
return { event, result: message };
|
|
488
|
-
});
|
|
491
|
+
}, (text) => saveRunCopy(root, text));
|
|
489
492
|
out(message);
|
|
490
493
|
return 0;
|
|
491
494
|
}
|