@andrioid/paseo-attention-span 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/OVERVIEW.md +63 -0
- package/README.md +33 -2
- package/assets/favicon.svg +7 -0
- package/assets/icon.png +0 -0
- package/assets/logo.svg +7 -0
- package/client/attention-screen.tsx +948 -0
- package/client/coverage-settings.tsx +145 -0
- package/client/data.ts +22 -0
- package/client/icons.ts +45 -0
- package/client/next-up.tsx +161 -0
- package/client/sessions.ts +202 -0
- package/client/setup.tsx +134 -0
- package/client/sheet.tsx +87 -0
- package/client/ui.tsx +173 -0
- package/docs/screenshots/desktop-dark-doing.png +0 -0
- package/docs/screenshots/desktop-dark-next-up.png +0 -0
- package/docs/screenshots/phone-dark-doing.png +0 -0
- package/index.client.tsx +36 -0
- package/index.server.ts +49 -0
- package/package.json +45 -4
- package/paseo-plugin.json +14 -0
- package/server/actions.ts +90 -0
- package/server/classify.ts +448 -0
- package/server/context.ts +87 -0
- package/server/details.ts +76 -0
- package/server/fixture.ts +220 -0
- package/server/gh.ts +36 -0
- package/server/github-types.ts +114 -0
- package/server/markdown.ts +73 -0
- package/server/queries.ts +152 -0
- package/server/service.ts +248 -0
- package/server/setup.ts +47 -0
- package/server/store.ts +99 -0
- package/server/view.ts +37 -0
- package/shared/attention.ts +208 -0
- package/shared/config.ts +62 -0
- package/shared/format.ts +16 -0
- package/shared/prompts.ts +89 -0
- package/shared/rpc.ts +56 -0
- package/shared/setup.ts +27 -0
package/shared/config.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// Session types, each with its own prompt template and profile lookup.
|
|
4
|
+
export const SessionKind = z.enum(["review", "fix", "address", "rebase", "refresh", "start"]);
|
|
5
|
+
export type SessionKind = z.infer<typeof SessionKind>;
|
|
6
|
+
|
|
7
|
+
export const MergeMethod = z.enum(["squash", "merge", "rebase"]);
|
|
8
|
+
export type MergeMethod = z.infer<typeof MergeMethod>;
|
|
9
|
+
|
|
10
|
+
// An org issue field and how its values rank; values match by name, so "Small" matches "🐇 Small (0.5-2d)".
|
|
11
|
+
const RankedField = z.object({
|
|
12
|
+
field: z.string(),
|
|
13
|
+
// Best first. Without it, the field's own option order applies.
|
|
14
|
+
order: z.array(z.string()).optional(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// ~/.config/paseo-attention-span/config.json, edited by hand [SRC-01].
|
|
18
|
+
export const AttentionConfig = z.object({
|
|
19
|
+
// GitHub orgs and "owner/name" repos to cover. Both empty means everything involving me.
|
|
20
|
+
orgs: z.array(z.string()).default([]),
|
|
21
|
+
repos: z.array(z.string()).default([]),
|
|
22
|
+
// Agent profile name per session type. A missing profile falls back to "default",
|
|
23
|
+
// then to Paseo's defaults [SES-06].
|
|
24
|
+
profiles: z
|
|
25
|
+
.object({ default: z.string().default("default") })
|
|
26
|
+
.catchall(z.string())
|
|
27
|
+
.default({ default: "default", review: "review" }),
|
|
28
|
+
// Replaces a built-in prompt template [SES-04].
|
|
29
|
+
prompts: z.partialRecord(SessionKind, z.string()).default({}),
|
|
30
|
+
// Where an issue repo's code lives, "owner/name" -> "owner/name", for issues kept apart from the code [SES-07].
|
|
31
|
+
codeRepos: z.record(z.string(), z.string()).default({}),
|
|
32
|
+
// Merge method per "owner/name" [PRS-04].
|
|
33
|
+
mergeMethods: z.record(z.string(), MergeMethod).default({}),
|
|
34
|
+
waitingTooLongDays: z.number().positive().default(3),
|
|
35
|
+
// A digest, not a live view: GitHub is asked at most this often unless I press ⟳ [GH-01].
|
|
36
|
+
refreshMinutes: z.number().min(5).default(15),
|
|
37
|
+
staleDraftDays: z.number().positive().default(30),
|
|
38
|
+
mentionDays: z.number().positive().default(30),
|
|
39
|
+
// How many weeks History shows [HIS-01].
|
|
40
|
+
historyWeeks: z.number().int().positive().default(4),
|
|
41
|
+
// Unclaimed issues to pick from [NXT-04]. Repos default to `orgs` and `repos`.
|
|
42
|
+
nextUp: z
|
|
43
|
+
.object({
|
|
44
|
+
repos: z.array(z.string()).optional(),
|
|
45
|
+
priority: RankedField.optional(),
|
|
46
|
+
effort: RankedField.optional(),
|
|
47
|
+
scope: z.object({ field: z.string() }).optional(),
|
|
48
|
+
})
|
|
49
|
+
.default({}),
|
|
50
|
+
});
|
|
51
|
+
export type AttentionConfig = z.infer<typeof AttentionConfig>;
|
|
52
|
+
|
|
53
|
+
// "owner/name" of a Paseo project with a GitHub remote, from its project key.
|
|
54
|
+
export function githubRepoOf(projectKey: string | null | undefined): string | null {
|
|
55
|
+
return /^remote:github\.com\/(.+)$/i.exec(projectKey ?? "")?.[1] ?? null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The repository whose checkout a session on an issue from `repo` works in [SES-07].
|
|
59
|
+
export function codeRepoFor(config: Pick<AttentionConfig, "codeRepos">, repo: string): string {
|
|
60
|
+
const mapped = Object.entries(config.codeRepos).find(([issues]) => issues.toLowerCase() === repo.toLowerCase());
|
|
61
|
+
return mapped?.[1] ?? repo;
|
|
62
|
+
}
|
package/shared/format.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// "5m", "3h", "4d", "2 months": how long ago an ISO timestamp was.
|
|
2
|
+
export function ago(iso: string, now: Date = new Date()): string {
|
|
3
|
+
const minutes = Math.max(0, Math.floor((now.getTime() - new Date(iso).getTime()) / 60_000));
|
|
4
|
+
if (minutes < 60) return `${minutes}m`;
|
|
5
|
+
const hours = Math.floor(minutes / 60);
|
|
6
|
+
if (hours < 24) return `${hours}h`;
|
|
7
|
+
const days = Math.floor(hours / 24);
|
|
8
|
+
if (days < 60) return `${days}d`;
|
|
9
|
+
const months = Math.floor(days / 30);
|
|
10
|
+
if (months < 24) return `${months} months`;
|
|
11
|
+
return `${Math.floor(days / 365)} years`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function daysSince(iso: string, now: Date): number {
|
|
15
|
+
return (now.getTime() - new Date(iso).getTime()) / 86_400_000;
|
|
16
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ActionId, Target } from "./attention";
|
|
2
|
+
import type { AttentionConfig, SessionKind } from "./config";
|
|
3
|
+
|
|
4
|
+
// Built-in prompt templates [SES-04]. A template in config.json replaces one.
|
|
5
|
+
// Placeholders: {{repo}} {{number}} {{title}} {{url}} {{head}} {{base}} {{stack}} {{context}}
|
|
6
|
+
export const PROMPT_TEMPLATES: Record<SessionKind, string> = {
|
|
7
|
+
review: `Review pull request {{repo}}#{{number}}: "{{title}}" ({{url}}). You are in a checkout of the PR branch.
|
|
8
|
+
|
|
9
|
+
1. Review: read the description and the diff against {{base}}.
|
|
10
|
+
2. Investigate: dig into anything risky or unclear in the code it touches.
|
|
11
|
+
3. Browser sweep, only if you're equipped to (you can run this repo's app and have browser tools): work out which pages and screens the change affects and check each one. If you can't, skip this and say so. Don't spend the session getting the app to run.
|
|
12
|
+
4. Report back:
|
|
13
|
+
- Findings, most important first.
|
|
14
|
+
- Verdict: approve, request changes or comment, with a one-line reason.
|
|
15
|
+
- Draft review comments, ready to paste, each with file:line.
|
|
16
|
+
|
|
17
|
+
Do not post anything to GitHub.
|
|
18
|
+
|
|
19
|
+
{{context}}`,
|
|
20
|
+
|
|
21
|
+
fix: `The checks on pull request {{repo}}#{{number}} "{{title}}" ({{url}}) are failing.
|
|
22
|
+
|
|
23
|
+
Find the cause and fix it on branch {{head}}. Run the relevant checks locally. Commit, but do not push: tell me what you changed and wait for me.
|
|
24
|
+
|
|
25
|
+
{{context}}`,
|
|
26
|
+
|
|
27
|
+
address: `Pull request {{repo}}#{{number}} "{{title}}" ({{url}}) has review comments. You are on branch {{head}}.
|
|
28
|
+
|
|
29
|
+
Evaluate each comment: is it right, and what would fixing it take?
|
|
30
|
+
- If the fix is trivial, apply it. Trivial means no behavior change: typos, naming, comments, formatting, or a small refactor the reviewer spelled out. Commit, but don't push.
|
|
31
|
+
- Anything that changes behavior or needs a judgment call: don't apply it.
|
|
32
|
+
|
|
33
|
+
Then report back on every comment: what you changed, and for the rest, your evaluation and what you propose. Do not reply on GitHub.
|
|
34
|
+
|
|
35
|
+
{{context}}`,
|
|
36
|
+
|
|
37
|
+
rebase: `Bring pull request {{repo}}#{{number}} "{{title}}" ({{url}}) up to date with its base, {{base}}.
|
|
38
|
+
|
|
39
|
+
Stack, bottom first: {{stack}}
|
|
40
|
+
|
|
41
|
+
1. Fetch, then rebase. For a stack, check out the top branch and run \`git rebase --update-refs origin/{{base}}\` so every branch in the stack moves together.
|
|
42
|
+
2. If the rebase had no conflicts, push every rebased branch with \`git push --force-with-lease\` and tell me it's done.
|
|
43
|
+
3. If you resolved conflicts, do NOT push. Explain each resolution and wait. Push with \`--force-with-lease\` only after I reply "push".
|
|
44
|
+
|
|
45
|
+
{{context}}`,
|
|
46
|
+
|
|
47
|
+
refresh: `Draft pull request {{repo}}#{{number}} "{{title}}" ({{url}}) has gone stale.
|
|
48
|
+
|
|
49
|
+
Stack, bottom first: {{stack}}
|
|
50
|
+
|
|
51
|
+
Rebase it onto the latest {{base}} the same way as any update: \`git rebase --update-refs\` for a stack. If there were no conflicts, push with \`git push --force-with-lease\`. If you resolved conflicts, do NOT push: explain each resolution and wait until I reply "push". Then tell me briefly what the draft still needs.
|
|
52
|
+
|
|
53
|
+
{{context}}`,
|
|
54
|
+
|
|
55
|
+
start: `Start work on issue {{repo}}#{{number}} "{{title}}" ({{url}}).
|
|
56
|
+
|
|
57
|
+
Read the issue and the code it touches, then propose a plan before changing anything.
|
|
58
|
+
|
|
59
|
+
{{context}}`,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const ACTION_SESSION: Partial<Record<ActionId, SessionKind>> = {
|
|
63
|
+
review: "review",
|
|
64
|
+
fix: "fix",
|
|
65
|
+
address: "address",
|
|
66
|
+
rebase: "rebase",
|
|
67
|
+
refresh: "refresh",
|
|
68
|
+
start: "start",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export function renderPrompt(
|
|
72
|
+
kind: SessionKind,
|
|
73
|
+
target: Target,
|
|
74
|
+
context: string,
|
|
75
|
+
config: Pick<AttentionConfig, "prompts">,
|
|
76
|
+
): string {
|
|
77
|
+
const template = config.prompts[kind] ?? PROMPT_TEMPLATES[kind];
|
|
78
|
+
const values: Record<string, string> = {
|
|
79
|
+
repo: target.repo,
|
|
80
|
+
number: String(target.number),
|
|
81
|
+
title: target.title,
|
|
82
|
+
url: target.url,
|
|
83
|
+
head: target.headRefName ?? "",
|
|
84
|
+
base: target.baseRefName ?? target.defaultBranch ?? "",
|
|
85
|
+
stack: (target.stack ?? [{ number: target.number }]).map((pr) => `#${pr.number}`).join(" → "),
|
|
86
|
+
context: context.trim(),
|
|
87
|
+
};
|
|
88
|
+
return template.replace(/\{\{(\w+)\}\}/g, (match, name: string) => values[name] ?? match).trim();
|
|
89
|
+
}
|
package/shared/rpc.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { defineRpc } from "@getpaseo/plugin";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { AttentionView, ItemDetails, Target } from "./attention";
|
|
4
|
+
import { SessionKind } from "./config";
|
|
5
|
+
import type { ConfigStatus, CoverageCheck, SetupStatus } from "./setup";
|
|
6
|
+
|
|
7
|
+
// Both sides are this plugin, so the types are checked by TypeScript, not repeated as schemas.
|
|
8
|
+
const view = z.custom<AttentionView>();
|
|
9
|
+
const target = z.custom<Target>();
|
|
10
|
+
const done = z.object({ ok: z.literal(true) });
|
|
11
|
+
|
|
12
|
+
// The cached view. Feeds older than their schedule are refreshed from GitHub first; `refresh` fetches that feed now (⟳).
|
|
13
|
+
export const getAttention = defineRpc({ name: "attention.get", input: z.object({ refresh: z.enum(["live", "history"]).optional() }), output: view });
|
|
14
|
+
export const dismissItem = defineRpc({ name: "attention.dismiss", input: z.object({ key: z.string() }), output: view });
|
|
15
|
+
// Undo one dismissal [ATT-08].
|
|
16
|
+
export const undismissItem = defineRpc({ name: "attention.undismiss", input: z.object({ key: z.string() }), output: view });
|
|
17
|
+
export const restoreDismissed = defineRpc({ name: "attention.restore-dismissed", input: z.object({}), output: view });
|
|
18
|
+
// Adds one entry to codeRepos in config.json [SES-07].
|
|
19
|
+
export const setCodeRepo = defineRpc({ name: "config.set-code-repo", input: z.object({ issueRepo: z.string(), codeRepo: z.string() }), output: view });
|
|
20
|
+
// What the setup checklist shows when nothing loads [SET-01].
|
|
21
|
+
export const setupStatus = defineRpc({ name: "setup.status", input: z.object({}), output: z.custom<SetupStatus>() });
|
|
22
|
+
// The settings screen: the orgs and repos in config.json, each checked on GitHub as it's added [SRC-02].
|
|
23
|
+
export const getCoverage = defineRpc({ name: "config.coverage", input: z.object({}), output: z.custom<ConfigStatus>() });
|
|
24
|
+
export const checkCoverage = defineRpc({ name: "config.check-coverage", input: z.object({ kind: z.enum(["org", "repo"]), name: z.string() }), output: z.custom<CoverageCheck>() });
|
|
25
|
+
export const setCoverage = defineRpc({ name: "config.set-coverage", input: z.object({ orgs: z.array(z.string()), repos: z.array(z.string()) }), output: done });
|
|
26
|
+
// Remember the scope chips picked on Next up [NXT-03].
|
|
27
|
+
export const setScopes = defineRpc({ name: "next-up.scopes", input: z.object({ scopes: z.array(z.string()) }), output: done });
|
|
28
|
+
|
|
29
|
+
// GitHub changes, each from an explicit tap [SAFE-01].
|
|
30
|
+
export const updateBranch = defineRpc({ name: "pr.update-branch", input: z.object({ target }), output: done });
|
|
31
|
+
export const mergePullRequest = defineRpc({ name: "pr.merge", input: z.object({ target }), output: done });
|
|
32
|
+
export const closePullRequest = defineRpc({ name: "pr.close", input: z.object({ target, comment: z.string().optional() }), output: done });
|
|
33
|
+
export const claimIssue = defineRpc({ name: "issue.claim", input: z.object({ target }), output: done });
|
|
34
|
+
export const unclaimIssue = defineRpc({ name: "issue.unclaim", input: z.object({ target }), output: done });
|
|
35
|
+
// `ready: false` and `on: false` are the Undo of each.
|
|
36
|
+
export const setReady = defineRpc({ name: "pr.ready", input: z.object({ target, ready: z.boolean() }), output: done });
|
|
37
|
+
export const setAutoMerge = defineRpc({ name: "pr.auto-merge", input: z.object({ target, on: z.boolean() }), output: done });
|
|
38
|
+
export const react = defineRpc({ name: "comment.react", input: z.object({ subjectIds: z.array(z.string()), on: z.boolean() }), output: done });
|
|
39
|
+
export const rerunFailed = defineRpc({ name: "checks.rerun", input: z.object({ repo: z.string(), runIds: z.array(z.number()) }), output: done });
|
|
40
|
+
|
|
41
|
+
// What an item's sheet shows beyond the card [ATT-06]. `checks` asks for failing checks too, for Rerun.
|
|
42
|
+
export const itemDetails = defineRpc({
|
|
43
|
+
name: "item.details",
|
|
44
|
+
input: z.object({ subject: z.object({ kind: z.enum(["pr", "issue"]), repo: z.string(), number: z.number() }), target: target.optional(), checks: z.boolean() }),
|
|
45
|
+
output: z.custom<ItemDetails>(),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// The branch a session on an issue starts from, when its code lives in another repo [SES-07].
|
|
49
|
+
export const defaultBranch = defineRpc({ name: "repo.default-branch", input: z.object({ repo: z.string() }), output: z.object({ name: z.string().nullable() }) });
|
|
50
|
+
|
|
51
|
+
// Context for a session prompt: failing logs, unresolved threads, descriptions [SES-04].
|
|
52
|
+
export const sessionContext = defineRpc({
|
|
53
|
+
name: "session.context",
|
|
54
|
+
input: z.object({ kind: SessionKind, target }),
|
|
55
|
+
output: z.object({ context: z.string() }),
|
|
56
|
+
});
|
package/shared/setup.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// What getting started needs, for the setup checklist [SET-01] and the settings screen [SRC-02].
|
|
2
|
+
|
|
3
|
+
export interface ConfigStatus {
|
|
4
|
+
// Why config.json can't be read, if it can't. The settings screen won't overwrite it then.
|
|
5
|
+
error: string | null;
|
|
6
|
+
orgs: string[];
|
|
7
|
+
repos: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SetupStatus {
|
|
11
|
+
gh: {
|
|
12
|
+
found: boolean;
|
|
13
|
+
// The account gh is signed in as; null when it isn't, with gh's reason in `error`.
|
|
14
|
+
login: string | null;
|
|
15
|
+
error: string | null;
|
|
16
|
+
};
|
|
17
|
+
config: ConfigStatus;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Nothing loads until these are fixed, so the checklist takes the tabs' place.
|
|
21
|
+
export function blocks(status: SetupStatus): boolean {
|
|
22
|
+
return !status.gh.found || !status.gh.login || !!status.config.error;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// An org or repo checked on GitHub before it's saved: its exact name, or why it can't be added.
|
|
26
|
+
export type CoverageCheck = { name: string; error: null } | { name: null; error: string };
|
|
27
|
+
export type CoverageKind = "org" | "repo";
|