@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.
- package/OVERVIEW.md +1 -1
- package/README.md +28 -4
- package/client/attachments.tsx +9 -6
- package/client/deliverables.tsx +6 -4
- package/client/dialog-stack.tsx +3 -0
- package/client/history.tsx +23 -18
- package/client/i18n.ts +86 -0
- package/client/language.tsx +23 -0
- package/client/launcher.tsx +22 -17
- package/client/locales/en.ts +207 -0
- package/client/locales/zh-CN.ts +207 -0
- package/client/messages.ts +189 -0
- package/client/panel.tsx +77 -47
- package/client/pill-popover.tsx +8 -5
- package/client/pill-state.ts +4 -3
- package/client/pills.tsx +25 -11
- package/client/preview.tsx +18 -13
- package/client/questions.tsx +42 -18
- package/client/settings.tsx +58 -0
- package/client/show-more.tsx +4 -2
- package/client/spinner.tsx +7 -3
- package/client/status-badge.tsx +3 -9
- package/client/surfaces.ts +20 -8
- package/client/ticket-dialog.tsx +37 -30
- package/client/video.tsx +3 -1
- package/client/whats-new.tsx +125 -0
- package/client/when.tsx +5 -3
- package/client/write-up.tsx +8 -5
- package/dist/server/cli.js +85 -20
- package/images/1-panel.png +0 -0
- package/images/3-question.png +0 -0
- package/images/5-settings.png +0 -0
- package/index.client.tsx +13 -1
- package/index.server.ts +12 -2
- package/package.json +1 -1
- package/paseo-plugin.json +2 -1
- package/server/cli.ts +28 -11
- package/server/question-mode.ts +61 -0
- package/server/store.ts +21 -4
- package/server/whats-new.ts +127 -0
- package/shared/dashboard.ts +9 -2
- package/shared/events.ts +9 -4
- package/shared/question-modes.ts +45 -0
- package/shared/rpc.ts +18 -0
- package/shared/settings.ts +20 -0
- package/shared/show.ts +3 -1
- package/shared/ticket-story.ts +12 -10
- package/shared/whats-new.ts +16 -0
- package/shared/write-up.ts +9 -8
- package/skills/paseo-progress/SKILL.md +26 -68
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// After an update, the panel shows what changed in this version, once. The
|
|
2
|
+
// notes come from the changelog in the plugin's own README, so they're
|
|
3
|
+
// written in one place. Whether they've been seen is kept per user on this
|
|
4
|
+
// computer.
|
|
5
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { NOTE_KINDS, type NoteKind, type WhatsNew, type WhatsNewNote } from "../shared/whats-new.ts";
|
|
10
|
+
import { spans } from "../shared/write-up.ts";
|
|
11
|
+
import { findPluginDirectory } from "./skill.ts";
|
|
12
|
+
import { stateRoot, writeAtomic } from "./store.ts";
|
|
13
|
+
|
|
14
|
+
export type Release = { version: string; readme: string | null };
|
|
15
|
+
|
|
16
|
+
const seenSchema = z.object({ seen: z.string() });
|
|
17
|
+
const packageSchema = z.object({ version: z.string() });
|
|
18
|
+
|
|
19
|
+
export function whatsNewPath(home = homedir()): string {
|
|
20
|
+
return join(stateRoot(home), "whats-new.json");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// The top-level bullets under `### <version>` in the README's `## Changelog`,
|
|
24
|
+
// or null when that version has none.
|
|
25
|
+
export function changelogNotes(readme: string, version: string): WhatsNewNote[] | null {
|
|
26
|
+
let section: "other" | "changelog" | "version" = "other";
|
|
27
|
+
const notes: string[] = [];
|
|
28
|
+
for (const line of readme.replace(/\r\n?/g, "\n").split("\n")) {
|
|
29
|
+
if (section === "version" && /^#{2,3}\s/.test(line)) break;
|
|
30
|
+
if (/^##\s/.test(line)) section = line.trim() === "## Changelog" ? "changelog" : "other";
|
|
31
|
+
else if (section === "changelog" && line.trim() === `### ${version}`) section = "version";
|
|
32
|
+
else if (section === "version" && /^[-*]\s/.test(line)) notes.push(line.slice(2).trim());
|
|
33
|
+
else if (section === "version" && notes.length && /^\s+\S/.test(line) && !/^\s+([-*+]|\d+[.)])\s/.test(line)) {
|
|
34
|
+
notes[notes.length - 1] += ` ${line.trim()}`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return notes.length ? notes.map(note) : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// A bullet can open with a kind tag, like `[Fixed]`, and then a bold
|
|
41
|
+
// sentence, like `**Title.** Detail.`, which is its title.
|
|
42
|
+
function note(bullet: string): WhatsNewNote {
|
|
43
|
+
const tag = /^\[([a-z]+)\]\s*(.*)$/i.exec(bullet);
|
|
44
|
+
const kind = tag ? noteKind(tag[1]) : undefined;
|
|
45
|
+
const rest = kind ? tag![2] : bullet;
|
|
46
|
+
const text = rest.split(/(`[^`]+`)/).map((part, index) => index % 2 ? part : curly(part)).join("");
|
|
47
|
+
const lead = /^\*\*([^*]+?[.!?])\*\*\s*(.*)$/.exec(text);
|
|
48
|
+
return { ...(kind && { kind }), ...(lead ? { title: plain(lead[1]), body: lead[2] } : { body: text }) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function noteKind(tag: string): NoteKind | undefined {
|
|
52
|
+
const key = tag.toLowerCase();
|
|
53
|
+
return Object.hasOwn(NOTE_KINDS, key) ? key as NoteKind : undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The README is typed with straight quotes; the panel shows typeset ones. Code keeps its own.
|
|
57
|
+
function curly(text: string): string {
|
|
58
|
+
return text.replace(/(^|[\s([{])"/g, "$1\u201c").replace(/"/g, "\u201d").replace(/(^|[\s([{])'/g, "$1\u2018").replace(/'/g, "\u2019");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function plain(markdown: string): string {
|
|
62
|
+
return spans(markdown).map((span) => span.text).join("");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A note's title, or else the first sentence of its body, without Markdown.
|
|
66
|
+
export function headline(note: WhatsNewNote): string {
|
|
67
|
+
if (note.title) return note.title;
|
|
68
|
+
const body = plain(note.body);
|
|
69
|
+
return /^.*?[.!?](?=\s|$)/.exec(body)?.[0] ?? body;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function readSeen(home: string): Promise<string | null> {
|
|
73
|
+
try {
|
|
74
|
+
return seenSchema.parse(JSON.parse(await readFile(whatsNewPath(home), "utf8"))).seen;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function markWhatsNewSeen(version: string, home = homedir()): Promise<void> {
|
|
81
|
+
await writeAtomic(whatsNewPath(home), `${JSON.stringify({ seen: version })}\n`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// A worktree's folder (see outsideDirectory) means the plugin was in use
|
|
85
|
+
// before this version, so this is an update, not a new install.
|
|
86
|
+
async function usedBefore(home: string): Promise<boolean> {
|
|
87
|
+
const entries = await readdir(stateRoot(home), { withFileTypes: true }).catch(() => []);
|
|
88
|
+
return entries.some((entry) => entry.isDirectory() && /-[0-9a-f]{8}$/.test(entry.name));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// What to show, or null. A new install has nothing to catch up on, so it
|
|
92
|
+
// records this version as seen.
|
|
93
|
+
export async function whatsNew({ version, readme }: Release, home = homedir()): Promise<WhatsNew | null> {
|
|
94
|
+
const notes = readme === null ? null : changelogNotes(readme, version);
|
|
95
|
+
if (!notes) return null;
|
|
96
|
+
const seen = await readSeen(home);
|
|
97
|
+
if (seen === version) return null;
|
|
98
|
+
if (seen === null && !(await usedBefore(home))) {
|
|
99
|
+
await markWhatsNewSeen(version, home);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return { version, headline: headline(notes[0]), notes };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The server bundle can't read files beside it, so ask Paseo where the plugin
|
|
106
|
+
// is. Read once per load: an update reloads the bundle, which starts over.
|
|
107
|
+
let release: Promise<Release> | null = null;
|
|
108
|
+
|
|
109
|
+
function currentRelease(): Promise<Release> {
|
|
110
|
+
release ??= findPluginDirectory().then(async (directory) => ({
|
|
111
|
+
version: packageSchema.parse(JSON.parse(await readFile(join(directory, "package.json"), "utf8"))).version,
|
|
112
|
+
readme: await readFile(join(directory, "README.md"), "utf8").catch(() => null),
|
|
113
|
+
}));
|
|
114
|
+
release.catch(() => {
|
|
115
|
+
release = null;
|
|
116
|
+
});
|
|
117
|
+
return release;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function handleGetWhatsNew(): Promise<WhatsNew | null> {
|
|
121
|
+
return whatsNew(await currentRelease());
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function handleDismissWhatsNew(input: { version: string }) {
|
|
125
|
+
await markWhatsNewSeen(input.version);
|
|
126
|
+
return {};
|
|
127
|
+
}
|
package/shared/dashboard.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EVENT_VERSION, eventSchema, type DELIVERABLE_KINDS, type FileLink, type ProgressEvent, type QuestionOption, type TicketStatus } from "./events.ts";
|
|
2
|
+
import { QUESTION_MODE_RULES, type QuestionState } from "./question-modes.ts";
|
|
2
3
|
|
|
3
4
|
// One recorded change to a ticket, oldest first. `stage: ""` clears the stage.
|
|
4
5
|
export interface TicketChange {
|
|
@@ -43,14 +44,18 @@ export interface Question {
|
|
|
43
44
|
title: string;
|
|
44
45
|
question: string;
|
|
45
46
|
options: QuestionOption[];
|
|
46
|
-
// The option the agent leans toward.
|
|
47
|
+
// The option the agent leans toward.
|
|
47
48
|
recommended: string;
|
|
49
|
+
// What the agent does while the question is open.
|
|
50
|
+
state: QuestionState;
|
|
51
|
+
// Why the question holds even in Keep building mode.
|
|
52
|
+
big?: string;
|
|
48
53
|
background?: string;
|
|
49
54
|
files: FileLink[];
|
|
50
55
|
raisedBy?: string;
|
|
51
56
|
ticketId?: string;
|
|
52
57
|
askedAt: string;
|
|
53
|
-
// `changedCourse
|
|
58
|
+
// `changedCourse`: the agent had built another option and changed course.
|
|
54
59
|
answer?: { choice: string; words?: string; changedCourse?: boolean; at: string };
|
|
55
60
|
}
|
|
56
61
|
|
|
@@ -279,6 +284,8 @@ export function reduceProgress(parsed: { events: ParsedLine[]; issues: FileIssue
|
|
|
279
284
|
question: event.question,
|
|
280
285
|
options: event.options ?? [],
|
|
281
286
|
recommended: event.recommend ?? event.default ?? "",
|
|
287
|
+
state: event.mode ? QUESTION_MODE_RULES[event.mode].state(Boolean(event.big)) : event.default !== undefined && !event.waits ? "building" : "waiting",
|
|
288
|
+
big: event.big,
|
|
282
289
|
background: event.background,
|
|
283
290
|
files: event.files ?? [],
|
|
284
291
|
raisedBy: event.raisedBy,
|
package/shared/events.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { QUESTION_MODES } from "./question-modes.ts";
|
|
2
3
|
|
|
3
4
|
// One line of `progress.jsonl`. The command-line tool writes these; the
|
|
4
5
|
// dashboard reducer reads them. `v` lets later versions change a shape without
|
|
@@ -77,9 +78,11 @@ export const eventSchema = z.discriminatedUnion("type", [
|
|
|
77
78
|
// A blocker that is not a ticket status, like a failing external service.
|
|
78
79
|
z.object({ ...base, type: z.literal("stuck.set"), id: text, reason: text, ticket: text.optional() }),
|
|
79
80
|
z.object({ ...base, type: z.literal("stuck.clear"), id: text }),
|
|
80
|
-
// The agent asks
|
|
81
|
-
//
|
|
82
|
-
//
|
|
81
|
+
// The agent asks and recommends an option. `mode` is the question mode when
|
|
82
|
+
// asked; without it (0.3.0 to 1.3.x) the agent built nothing before the
|
|
83
|
+
// answer, like "work-around". `big` says why the question holds even in
|
|
84
|
+
// "keep-building" mode. Questions from before 0.3.0 carry `default`, the
|
|
85
|
+
// option the agent carried on with unless it `waits`.
|
|
83
86
|
z.object({
|
|
84
87
|
...base,
|
|
85
88
|
type: z.literal("question.ask"),
|
|
@@ -89,6 +92,8 @@ export const eventSchema = z.discriminatedUnion("type", [
|
|
|
89
92
|
options: z.array(questionOptionSchema).max(26).optional(),
|
|
90
93
|
// The agent's pick: an option letter, or a word such as "Looks right" when there are no options.
|
|
91
94
|
recommend: text.optional(),
|
|
95
|
+
mode: z.enum(QUESTION_MODES).optional(),
|
|
96
|
+
big: text.optional(),
|
|
92
97
|
default: text.optional(),
|
|
93
98
|
waits: z.boolean().optional(),
|
|
94
99
|
background: text.optional(),
|
|
@@ -104,7 +109,7 @@ export const eventSchema = z.discriminatedUnion("type", [
|
|
|
104
109
|
choice: text,
|
|
105
110
|
// The user's own words, quoted under the answer.
|
|
106
111
|
words: text.optional(),
|
|
107
|
-
//
|
|
112
|
+
// True when the agent had to undo work built on another option.
|
|
108
113
|
changedCourse: z.boolean().optional(),
|
|
109
114
|
}),
|
|
110
115
|
// Adds detail to an open question; its reference and default stay.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// What an agent does when it asks the user a question, picked in Paseo's
|
|
2
|
+
// Settings. The command, the settings page, and the dashboard all read this
|
|
3
|
+
// table. No Paseo imports: the command's bundle includes this file.
|
|
4
|
+
export const QUESTION_MODES = ["stop", "work-around", "keep-building"] as const;
|
|
5
|
+
export type QuestionMode = (typeof QUESTION_MODES)[number];
|
|
6
|
+
|
|
7
|
+
// Also what a missing or damaged mirror means.
|
|
8
|
+
export const DEFAULT_QUESTION_MODE: QuestionMode = "work-around";
|
|
9
|
+
|
|
10
|
+
// An open question on the dashboard: the agent stopped all work, holds what the
|
|
11
|
+
// question affects, or builds its recommendation.
|
|
12
|
+
export type QuestionState = "stopped" | "waiting" | "building";
|
|
13
|
+
|
|
14
|
+
interface QuestionModeRules {
|
|
15
|
+
label: string;
|
|
16
|
+
hint: string;
|
|
17
|
+
// What the agent does until the answer. `question ask` prints it after "Mode: <label>."
|
|
18
|
+
askRule(question: { recommend: string; big: boolean }): string;
|
|
19
|
+
state(big: boolean): QuestionState;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const HOLD = "Build no option before the answer, not even your recommendation. Work on everything the question doesn't affect. If nothing else is left, end your turn, tell the user in chat which questions you're waiting on, and leave the run open.";
|
|
23
|
+
|
|
24
|
+
export const QUESTION_MODE_RULES: Record<QuestionMode, QuestionModeRules> = {
|
|
25
|
+
stop: {
|
|
26
|
+
label: "Stop",
|
|
27
|
+
hint: "Agents stop all work when they ask, until you answer.",
|
|
28
|
+
askRule: () => "Ask any other questions you already have, then stop all work and resume only after an answer. End your turn now, tell the user in chat which questions you're waiting on, and leave the run open.",
|
|
29
|
+
state: () => "stopped",
|
|
30
|
+
},
|
|
31
|
+
"work-around": {
|
|
32
|
+
label: "Work around",
|
|
33
|
+
hint: "Agents hold what your answer affects and keep working on the rest.",
|
|
34
|
+
askRule: () => HOLD,
|
|
35
|
+
state: () => "waiting",
|
|
36
|
+
},
|
|
37
|
+
"keep-building": {
|
|
38
|
+
label: "Keep building",
|
|
39
|
+
hint: "Agents build their recommended option while you decide, and change course if you pick another. Big questions still wait.",
|
|
40
|
+
askRule: ({ recommend, big }) => big
|
|
41
|
+
? `This question is big, so hold it like Work around. ${HOLD}`
|
|
42
|
+
: `Build ${recommend} now and keep working. If the user picks another option, undo the work built on ${recommend} and build their choice first. If the rest of the work is done while it's still open, leave it open and tell the user in chat; don't withdraw it.`,
|
|
43
|
+
state: (big) => (big ? "waiting" : "building"),
|
|
44
|
+
},
|
|
45
|
+
};
|
package/shared/rpc.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineRpc } from "@getpaseo/plugin";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type { Dashboard } from "./dashboard.ts";
|
|
4
|
+
import type { WhatsNew } from "./whats-new.ts";
|
|
4
5
|
|
|
5
6
|
// `panelOpened`: the Progress panel has been opened for this worktree before.
|
|
6
7
|
// `root`: the worktree root the file was read from, which stored paths are
|
|
@@ -163,3 +164,20 @@ export const readDeliverablePiece = defineRpc({
|
|
|
163
164
|
}),
|
|
164
165
|
output: z.object({ data: z.string(), bytes: z.number() }),
|
|
165
166
|
});
|
|
167
|
+
|
|
168
|
+
// This version's changelog notes, once after an update; null otherwise.
|
|
169
|
+
// `headline` is the first sentence of the first note, without Markdown.
|
|
170
|
+
// A changelog bullet. `title` is its bold lead without Markdown, when it has
|
|
171
|
+
// one; `body` is the rest, Markdown kept.
|
|
172
|
+
export const getWhatsNew = defineRpc({
|
|
173
|
+
name: "progress.whats-new.get",
|
|
174
|
+
input: z.object({}),
|
|
175
|
+
output: z.custom<WhatsNew | null>(),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// The user closed it; it stays away for this version.
|
|
179
|
+
export const dismissWhatsNew = defineRpc({
|
|
180
|
+
name: "progress.whats-new.dismiss",
|
|
181
|
+
input: z.object({ version: z.string().min(1) }),
|
|
182
|
+
output: z.object({}),
|
|
183
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineSettings } from "@getpaseo/plugin";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { DEFAULT_QUESTION_MODE, QUESTION_MODES } from "./question-modes.ts";
|
|
4
|
+
|
|
5
|
+
export const questionSettings = defineSettings({
|
|
6
|
+
id: "questions",
|
|
7
|
+
scope: "host",
|
|
8
|
+
version: 1,
|
|
9
|
+
schema: z.object({ mode: z.enum(QUESTION_MODES).default(DEFAULT_QUESTION_MODE) }),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const LANGUAGE_PREFERENCES = ["system", "en", "zh-CN"] as const;
|
|
13
|
+
export type LanguagePreference = (typeof LANGUAGE_PREFERENCES)[number];
|
|
14
|
+
|
|
15
|
+
export const displaySettings = defineSettings({
|
|
16
|
+
id: "display",
|
|
17
|
+
scope: "host",
|
|
18
|
+
version: 1,
|
|
19
|
+
schema: z.object({ language: z.enum(LANGUAGE_PREFERENCES).default("system") }),
|
|
20
|
+
});
|
package/shared/show.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Dashboard } from "./dashboard.ts";
|
|
2
2
|
import { headlineText, shortTitle } from "./dashboard.ts";
|
|
3
3
|
import { formatAgo, formatClock, formatMinutes, formatWorkDone, minutesSince } from "./format.ts";
|
|
4
|
+
import type { QuestionState } from "./question-modes.ts";
|
|
4
5
|
|
|
5
6
|
const STATUS: Record<string, string> = { not_started: "Not started", working: "Working", blocked: "Blocked", done: "Done", skipped: "Skipped" };
|
|
7
|
+
const QUESTION_STATE: Record<QuestionState, string> = { stopped: "agent stopped until the answer", waiting: "waiting for the answer", building: "building with it" };
|
|
6
8
|
|
|
7
9
|
// The dashboard as plain text, so an agent can check what the user sees.
|
|
8
10
|
export function dashboardText(dashboard: Dashboard, nowMs: number): string {
|
|
@@ -44,7 +46,7 @@ export function dashboardText(dashboard: Dashboard, nowMs: number): string {
|
|
|
44
46
|
if (dashboard.questions.open.length) {
|
|
45
47
|
lines.push("", "Questions waiting");
|
|
46
48
|
for (const question of dashboard.questions.open) {
|
|
47
|
-
lines.push(` ${question.id} ${question.ticketId ? `[${question.ticketId}] ` : ""}(${question.title}): ${question.question} You recommended ${question.recommended}`);
|
|
49
|
+
lines.push(` ${question.id} ${question.ticketId ? `[${question.ticketId}] ` : ""}(${question.title}): ${question.question} You recommended ${question.recommended}, ${QUESTION_STATE[question.state]}${question.big ? `. Big: ${question.big}` : ""}`);
|
|
48
50
|
for (const option of question.options) lines.push(` ${option.letter}) ${option.label}${option.consequence ? `: ${option.consequence}` : ""}`);
|
|
49
51
|
}
|
|
50
52
|
}
|
package/shared/ticket-story.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import type { Activity, Deliverable, Question, Ticket } from "./dashboard.ts";
|
|
2
2
|
import type { TicketStatus } from "./events.ts";
|
|
3
3
|
|
|
4
|
+
export type TimelineLabel =
|
|
5
|
+
| { kind: "added" }
|
|
6
|
+
| { kind: "reset" }
|
|
7
|
+
| { kind: "stage"; stage: string }
|
|
8
|
+
| { kind: "status"; status: TicketStatus }
|
|
9
|
+
| { kind: "waiting"; id: string };
|
|
10
|
+
|
|
4
11
|
// One span in a ticket's life: from a status or stage change to the next one.
|
|
5
12
|
export interface TimelineStep {
|
|
6
13
|
at: string;
|
|
7
14
|
// When the next step began; null while this one is still going (or it's the end).
|
|
8
15
|
until: string | null;
|
|
9
|
-
label:
|
|
16
|
+
label: TimelineLabel;
|
|
10
17
|
status: TicketStatus;
|
|
11
18
|
notes: string[];
|
|
12
19
|
// Length of the step; null for a finished ticket's last step, which has no end.
|
|
@@ -27,15 +34,10 @@ export interface TicketStory {
|
|
|
27
34
|
|
|
28
35
|
const OPEN_ENDED: TicketStatus[] = ["working", "blocked", "not_started"];
|
|
29
36
|
|
|
30
|
-
function labelFor(status: TicketStatus, stage: string | undefined, first: boolean):
|
|
31
|
-
if (first && status === "not_started") return "
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
case "blocked": return "Blocked";
|
|
35
|
-
case "done": return "Done";
|
|
36
|
-
case "skipped": return "Skipped";
|
|
37
|
-
default: return "Back to not started";
|
|
38
|
-
}
|
|
37
|
+
function labelFor(status: TicketStatus, stage: string | undefined, first: boolean): TimelineLabel {
|
|
38
|
+
if (first && status === "not_started") return { kind: "added" };
|
|
39
|
+
if (status === "working" && stage) return { kind: "stage", stage };
|
|
40
|
+
return status === "not_started" ? { kind: "reset" } : { kind: "status", status };
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
export function ticketTimeline(ticket: Ticket, nowMs: number): TimelineStep[] {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// A changelog bullet can open with a tag, like `- [Fixed] **Title.** Detail.`,
|
|
2
|
+
// that says what kind of change it is. The parser and the dialog both read
|
|
3
|
+
// this table. `tone` is the hue the dialog colors the tag with.
|
|
4
|
+
export const NOTE_KINDS = {
|
|
5
|
+
new: { label: "New", tone: "green" },
|
|
6
|
+
improved: { label: "Improved", tone: "violet" },
|
|
7
|
+
fixed: { label: "Fixed", tone: "amber" },
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
export type NoteTone = (typeof NOTE_KINDS)[NoteKind]["tone"];
|
|
11
|
+
|
|
12
|
+
export type NoteKind = keyof typeof NOTE_KINDS;
|
|
13
|
+
|
|
14
|
+
export type WhatsNewNote = { kind?: NoteKind; title?: string; body: string };
|
|
15
|
+
|
|
16
|
+
export type WhatsNew = { version: string; headline: string; notes: WhatsNewNote[] };
|
package/shared/write-up.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// A ticket's write-up (its Markdown source file) as simple blocks the dialog
|
|
2
2
|
// can draw without a Markdown renderer: headings, paragraphs, list items and
|
|
3
|
-
// code. Inline marks are kept as spans: `code` stays code, **bold**
|
|
4
|
-
// _emphasis_
|
|
3
|
+
// code. Inline marks are kept as spans: `code` stays code, **bold** stays
|
|
4
|
+
// bold, _emphasis_ loses its markers, and [links](url) keep only their text.
|
|
5
5
|
|
|
6
|
-
export type Span = { text: string; code?: boolean };
|
|
6
|
+
export type Span = { text: string; code?: boolean; strong?: boolean };
|
|
7
7
|
|
|
8
8
|
export type Block =
|
|
9
9
|
| { kind: "heading"; level: number; spans: Span[] }
|
|
@@ -20,11 +20,12 @@ export function spans(text: string): Span[] {
|
|
|
20
20
|
result.push({ text: part.slice(1, -1), code: true });
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.replace(/(^|[\s(])[*_](\S(?:.*?\S)?)[*_](?=[\s).,;:!?]|$)/g, "$1$2");
|
|
27
|
-
|
|
23
|
+
part.replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").split(/((?:\*\*|__).+?(?:\*\*|__))/).forEach((piece) => {
|
|
24
|
+
if (!piece) return;
|
|
25
|
+
const strong = /^(\*\*|__).+\1$/.test(piece);
|
|
26
|
+
const text = (strong ? piece.slice(2, -2) : piece).replace(/(^|[\s(])[*_](\S(?:.*?\S)?)[*_](?=[\s).,;:!?]|$)/g, "$1$2");
|
|
27
|
+
result.push(strong ? { text, strong } : { text });
|
|
28
|
+
});
|
|
28
29
|
});
|
|
29
30
|
return result;
|
|
30
31
|
}
|
|
@@ -1,92 +1,50 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: paseo-progress
|
|
3
|
-
description:
|
|
3
|
+
description: Keep the Paseo Progress dashboard live with the paseo-progress command. Use when working through a spec or a set of tickets, on any job over 5 steps or 30 minutes, or when the user asks for a progress dashboard.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Paseo progress
|
|
7
7
|
|
|
8
|
-
The user watches a live dashboard in Paseo
|
|
8
|
+
The user watches a live dashboard in Paseo for this worktree. Keep it live: run `paseo-progress` from inside the worktree the moment each change happens, so the dashboard is a side effect of the work. Write every line for a product manager, in plain words. `paseo-progress --help` is the reference for every command and flag.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## 1. Set up
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Run `paseo-progress show` and continue an open run for this job. A Finished run is closed, so `start` a new one, then add every ticket in order:
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
2. Add every ticket in order with `paseo-progress ticket add "Ticket 01: <title>" --estimate <minutes>`, with `--waits-for T02` on any that can't start before another finishes. Keep the "Ticket NN:" prefix: the dashboard shortens titles to it. When a ticket is written up somewhere, a spec file or an issue, pass `--source <path or link>` so the user can open it from the ticket. For a job without tickets, add its steps instead and pass `--item-label Step` to `start`; give a step `--source` only when it has a write-up.
|
|
18
|
-
3. `paseo-progress activity add "<one line: what this run will do>"`.
|
|
14
|
+
- Title each `"Ticket 01: <title>"`. The dashboard shortens titles to that prefix.
|
|
15
|
+
- Give each an `--estimate`, plus `--waits-for` when it can't start before another and `--source` when a spec file or issue describes it.
|
|
16
|
+
- For a job without tickets, add its steps and pass `--item-label Step` to `start`.
|
|
19
17
|
|
|
20
18
|
Done when `show` lists every ticket with an estimate.
|
|
21
19
|
|
|
22
|
-
## 2. Work
|
|
23
|
-
|
|
24
|
-
Record each change as it happens, on the ticket's id from `show` (T01, T02, ...):
|
|
25
|
-
|
|
26
|
-
| When | Command |
|
|
27
|
-
| --- | --- |
|
|
28
|
-
| You start a ticket | `ticket update T03 --status working --stage Build` |
|
|
29
|
-
| You move to the next stage | `ticket update T03 --stage Review`, then `Fixes`, then `Verify` |
|
|
30
|
-
| Something reviewable lands (a screenshot folder, a screen recording, a report, a preview URL) | `deliverable add "<what it shows>" <path or URL> --ticket T03` |
|
|
31
|
-
| A milestone the user would want in the story | `activity add "<one line>" --ticket T03` |
|
|
32
|
-
| Between bigger events | `ticker set "<what you are doing right now>"` |
|
|
33
|
-
| The ticket is finished | `ticket update T03 --status done`, then start the next one |
|
|
34
|
-
| The plan drops it | `ticket update T03 --status skipped --note "<why>"` |
|
|
35
|
-
|
|
36
|
-
Pass `--ticket` on every activity, deliverable and question that belongs to a ticket. The user presses a ticket to see its story: each stage and how long it took, with the activity, deliverables and questions tagged to it. Leave it off only for run-wide notes. Stage changes and notes are timed from your `ticket update` calls, so make them when the change happens.
|
|
37
|
-
|
|
38
|
-
Other sessions may add tickets (or steps) to the same run. When a command's output says another session added one, it is part of the plan: work it in order after your current one, the same as the ones you added.
|
|
39
|
-
|
|
40
|
-
A working ticket turns stuck on its own once it goes longer than its estimate without an update: a status, stage or note change, or activity tagged to it. When a ticket will run long, keep updating its stage, or raise its estimate with `--estimate`.
|
|
41
|
-
|
|
42
|
-
A subagent records the work you hand it. Paseo shows you as idle while you wait on it, so its updates are the only sign the work is still going; after 15 minutes without one, the dashboard warns that the work may have stopped. Mark the ticket working, then put this in the subagent's prompt with this worktree's absolute path and the ticket id filled in:
|
|
43
|
-
|
|
44
|
-
```text
|
|
45
|
-
Record progress on the Paseo dashboard as you work:
|
|
46
|
-
- at each new stage: cd <worktree> && paseo-progress ticket update T03 --stage <stage>
|
|
47
|
-
- between stages: cd <worktree> && paseo-progress ticker set "<what you are doing now>"
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
The `cd` puts the updates on this worktree's dashboard even when the subagent works in another checkout. When the subagent reports back, mark the ticket done.
|
|
51
|
-
|
|
52
|
-
## 3. Ask questions, and work around them
|
|
20
|
+
## 2. Work
|
|
53
21
|
|
|
54
|
-
|
|
22
|
+
Move each ticket through `--status working --stage Build`, then the stages `Review`, `Fixes` and `Verify`, then `--status done`. Along the way, record what the user would want to see: `deliverable add` for anything reviewable, `activity add` for milestones, `ticker set` for what you are doing right now. Tag each with `--ticket` unless it covers the whole run.
|
|
55
23
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
--recommend <letter> --ticket T03 --raised-by "Ticket 03 design review" \
|
|
61
|
-
[--background "<what they need to decide without opening the worktree>"] \
|
|
62
|
-
[--file <path or http(s) URL>=<what it shows>]
|
|
63
|
-
```
|
|
24
|
+
- A ticket past its estimate with no update turns stuck. Keep its stage current, or raise its `--estimate`.
|
|
25
|
+
- A ticket another session adds is part of the plan. Work it in order.
|
|
26
|
+
- `--status blocked` and `stuck set` are for things outside the run, like a service that's down or missing access.
|
|
27
|
+
- Put this in a subagent's prompt, with the worktree path and ticket id filled in. While you wait on it, its updates are the dashboard's only sign the work is moving:
|
|
64
28
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
-
|
|
68
|
-
-
|
|
69
|
-
|
|
70
|
-
- When the user asks about a question, answer in chat and also put the explanation on the question with `question update Q7 --background "<text>"`, so the dashboard carries it too.
|
|
71
|
-
- `question remove Q7` withdraws a question that no longer applies.
|
|
29
|
+
```text
|
|
30
|
+
Record progress on the Paseo dashboard as you work:
|
|
31
|
+
- at each new stage: cd <worktree> && paseo-progress ticket update T03 --stage <stage>
|
|
32
|
+
- between stages: cd <worktree> && paseo-progress ticker set "<what you are doing now>"
|
|
33
|
+
```
|
|
72
34
|
|
|
73
|
-
|
|
35
|
+
Done when every ticket is done or skipped, or waits on a question.
|
|
74
36
|
|
|
75
|
-
|
|
76
|
-
- A ticket held up by something outside the run (a service, access, a decision nobody has made): `ticket update T03 --status blocked --note "<what it waits on>"`. That shows as stuck. Set it back to `working` when it moves again.
|
|
77
|
-
- A blocker outside any ticket (a failing service, a missing key): `stuck set "<reason>" [--ticket T03]`, then `stuck clear S1` once it is gone.
|
|
37
|
+
## 3. Ask
|
|
78
38
|
|
|
79
|
-
|
|
39
|
+
When a decision belongs to the user, ask it with `question ask`, lettered `--option`s and your `--recommend`. Read `paseo-progress question ask --help` before your first question. It defines a big question, which you mark with `--big "<the test it meets>"`.
|
|
80
40
|
|
|
81
|
-
|
|
41
|
+
The user picks the question mode in Paseo's Settings, and `question ask` prints what that mode means for this question. Do what it prints. Record the user's reply ("Q7 A", or their own words) with `question answer Q7 A --words "<their words>"`, and do what that prints too.
|
|
82
42
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
3. `paseo-progress finish "<one line: the outcome>"`. It refuses, and says what is still open, until the steps above are done.
|
|
86
|
-
4. Run `paseo-progress show`.
|
|
43
|
+
- When the question is about how something looks, attach the screenshot or link with `--file`.
|
|
44
|
+
- When the user asks about a question, answer in chat and add the explanation with `question update Q7 --background`.
|
|
87
45
|
|
|
88
|
-
|
|
46
|
+
## 4. Finish
|
|
89
47
|
|
|
90
|
-
|
|
48
|
+
The job is over when every ticket is done or skipped and every question is answered. While questions wait on the user, leave the run open and tell the user in chat which ones. Then run `paseo-progress finish "<one-line outcome>"`, and settle whatever it lists if it refuses.
|
|
91
49
|
|
|
92
50
|
Done when `show` says Finished with the right outcome.
|