@gavin-hu/my-pi-agent 0.12.0 → 0.13.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 +28 -21
- package/README.zh-CN.md +30 -22
- package/extensions/bytefly/README.md +162 -70
- package/extensions/bytefly/chrome.ts +7 -2
- package/extensions/bytefly/doctor/register.ts +6 -0
- package/extensions/bytefly/doctor/runtime.ts +38 -6
- package/extensions/bytefly/footer/budget.ts +64 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +33 -5
- package/extensions/bytefly/footer/config.ts +7 -1
- package/extensions/bytefly/footer/delegation.ts +100 -0
- package/extensions/bytefly/footer/price/config.ts +16 -0
- package/extensions/bytefly/footer/price/types.ts +5 -0
- package/extensions/bytefly/footer/register.ts +46 -4
- package/extensions/bytefly/footer/render/lines.ts +2 -0
- package/extensions/bytefly/footer/render/segments.ts +27 -0
- package/extensions/bytefly/footer/types.ts +11 -0
- package/extensions/bytefly/index.ts +8 -5
- package/extensions/bytefly/notify/config.ts +2 -0
- package/extensions/bytefly/notify/format.ts +19 -1
- package/extensions/bytefly/notify/register.ts +52 -14
- package/extensions/bytefly/notify/types.ts +2 -0
- package/extensions/bytefly/panel/commands.ts +34 -13
- package/extensions/bytefly/panel/settings.ts +129 -16
- package/extensions/bytefly/update/cache.ts +73 -0
- package/extensions/bytefly/update/format.ts +65 -10
- package/extensions/bytefly/update/register.ts +99 -13
- package/extensions/bytefly/update/state.ts +13 -1
- package/extensions/document/README.md +22 -12
- package/extensions/document/config.ts +4 -0
- package/extensions/document/deadline.ts +61 -0
- package/extensions/document/tool.ts +13 -2
- package/extensions/file-browse/README.md +12 -8
- package/extensions/file-browse/client.ts +6 -7
- package/extensions/file-browse/files.ts +0 -4
- package/extensions/file-browse/git.ts +9 -10
- package/extensions/file-browse/html.ts +17 -7
- package/extensions/file-browse/router.ts +24 -15
- package/extensions/goal/README.md +7 -1
- package/extensions/goal/schema.ts +6 -2
- package/extensions/goal/types.ts +6 -2
- package/extensions/job/README.md +16 -12
- package/extensions/job/format.ts +4 -7
- package/extensions/job/runtime.ts +23 -6
- package/extensions/job/status.ts +4 -2
- package/extensions/memory/README.md +55 -13
- package/extensions/memory/commands.ts +20 -2
- package/extensions/memory/doctor.ts +137 -0
- package/extensions/memory/format.ts +24 -7
- package/extensions/memory/runtime.ts +49 -0
- package/extensions/memory/store.ts +31 -0
- package/extensions/memory/tui.ts +136 -12
- package/extensions/plan/README.md +60 -29
- package/extensions/plan/commands.ts +67 -8
- package/extensions/plan/index.ts +35 -11
- package/extensions/plan/plan-index.ts +234 -0
- package/extensions/plan/plan-text.ts +95 -0
- package/extensions/plan/plans.ts +90 -1
- package/extensions/plan/review.ts +12 -9
- package/extensions/plan/runtime.ts +72 -27
- package/extensions/plan/steps.ts +35 -25
- package/extensions/plan/todos.ts +24 -2
- package/extensions/plan/tools.ts +124 -24
- package/extensions/plan/tui.ts +7 -28
- package/extensions/plan/types.ts +9 -0
- package/extensions/plan/web-page.ts +2 -2
- package/extensions/plan/web.ts +12 -11
- package/extensions/rewind/README.md +28 -8
- package/extensions/rewind/commands.ts +1 -1
- package/extensions/rewind/format.ts +4 -4
- package/extensions/rewind/git.ts +56 -18
- package/extensions/rewind/index.ts +3 -5
- package/extensions/rewind/path-picker.ts +198 -0
- package/extensions/rewind/paths.ts +128 -0
- package/extensions/rewind/restore.ts +69 -28
- package/extensions/rewind/rewind.ts +80 -3
- package/extensions/rewind/runtime.ts +13 -5
- package/extensions/rewind/snapshot-run.ts +5 -2
- package/extensions/rewind/types.ts +2 -0
- package/extensions/subagent/README.md +7 -1
- package/extensions/subagent/accounting.ts +34 -0
- package/extensions/subagent/index.ts +23 -1
- package/extensions/subagent/stream.ts +2 -0
- package/extensions/subagent/types.ts +2 -0
- package/extensions/todo/nudge.ts +5 -1
- package/extensions/watch/README.md +19 -8
- package/extensions/watch/commands.ts +4 -3
- package/extensions/watch/config.ts +1 -1
- package/extensions/watch/format.ts +4 -7
- package/extensions/watch/index.ts +19 -0
- package/extensions/watch/runtime.ts +83 -12
- package/extensions/web-access/README.md +43 -13
- package/extensions/web-access/charset.ts +58 -0
- package/extensions/web-access/fetch/find.ts +11 -7
- package/extensions/web-access/fetch/format.ts +64 -18
- package/extensions/web-access/fetch/page.ts +5 -1
- package/extensions/web-access/fetch/tool.ts +19 -18
- package/extensions/web-access/http.ts +49 -8
- package/extensions/web-access/search/facts.ts +38 -0
- package/extensions/web-access/search/tool.ts +13 -0
- package/extensions/web-access/text.ts +50 -11
- package/extensions/worktree/README.md +81 -16
- package/extensions/worktree/config.ts +11 -0
- package/extensions/worktree/enter.ts +10 -3
- package/extensions/worktree/exit.ts +55 -3
- package/extensions/worktree/gaps.ts +74 -0
- package/extensions/worktree/git.ts +28 -0
- package/extensions/worktree/guard.ts +50 -4
- package/extensions/worktree/handoff.ts +55 -0
- package/extensions/worktree/index.ts +14 -2
- package/extensions/worktree/root-tools.ts +1 -1
- package/extensions/worktree/runtime.ts +25 -0
- package/extensions/worktree/status.ts +15 -2
- package/extensions/worktree/tools.ts +3 -2
- package/lib/README.md +14 -10
- package/lib/events.ts +86 -1
- package/lib/goal-state.ts +95 -0
- package/lib/requirements.ts +361 -62
- package/lib/tui/README.md +91 -0
- package/lib/tui/columns.ts +17 -0
- package/lib/tui/glyphs.ts +1 -0
- package/lib/tui/tool.ts +6 -2
- package/lib/web/layout.ts +12 -2
- package/package.json +5 -3
- package/themes/bytefly-dark.json +1 -1
- package/assets/chrome.png +0 -0
- package/assets/file-browse.png +0 -0
- package/assets/job.png +0 -0
- package/assets/plan-review.png +0 -0
- package/assets/question.png +0 -0
- package/assets/rewind.png +0 -0
- package/assets/todo.png +0 -0
- package/assets/tool-renderings.png +0 -0
- package/assets/tools/ask_user_question-cancelled.png +0 -0
- package/assets/tools/ask_user_question.png +0 -0
- package/assets/tools/bash.png +0 -0
- package/assets/tools/edit.png +0 -0
- package/assets/tools/enter_plan_mode.png +0 -0
- package/assets/tools/enter_worktree.png +0 -0
- package/assets/tools/exit_plan_mode.png +0 -0
- package/assets/tools/exit_worktree.png +0 -0
- package/assets/tools/find.png +0 -0
- package/assets/tools/goal.png +0 -0
- package/assets/tools/grep.png +0 -0
- package/assets/tools/job.png +0 -0
- package/assets/tools/list_worktrees.png +0 -0
- package/assets/tools/ls.png +0 -0
- package/assets/tools/memory-long-subject.png +0 -0
- package/assets/tools/memory.png +0 -0
- package/assets/tools/notice.png +0 -0
- package/assets/tools/open_path.png +0 -0
- package/assets/tools/prune_worktrees.png +0 -0
- package/assets/tools/read.png +0 -0
- package/assets/tools/read_doc-running.png +0 -0
- package/assets/tools/read_doc.png +0 -0
- package/assets/tools/run_doctor.png +0 -0
- package/assets/tools/send_wechat.png +0 -0
- package/assets/tools/set_plan_steps.png +0 -0
- package/assets/tools/subagent.png +0 -0
- package/assets/tools/todo.png +0 -0
- package/assets/tools/turn-rule.png +0 -0
- package/assets/tools/user-message.png +0 -0
- package/assets/tools/watch.png +0 -0
- package/assets/tools/web_fetch-rejected.png +0 -0
- package/assets/tools/web_fetch.png +0 -0
- package/assets/tools/web_search.png +0 -0
- package/assets/tools/write.png +0 -0
- package/assets/tools/write_plan.png +0 -0
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* (present the plan for approval) and `write_plan` (save the plan artifact under
|
|
7
7
|
* `.pi/plans`). Disabling restores exactly what it hid. The enabled flag is
|
|
8
8
|
* persisted as a custom session entry so `/resume` and `/tree` follow the
|
|
9
|
-
* branch, and it is also mirrored into the footer status.
|
|
9
|
+
* branch, and it is also mirrored into the footer status. The review baseline
|
|
10
|
+
* travels the same way, as one digest per heading section, so the changed-section
|
|
11
|
+
* markers survive a resume too.
|
|
10
12
|
*
|
|
11
13
|
* `write_plan` is the one sanctioned write while planning. The read-only guard
|
|
12
14
|
* in `index.ts` permits a control tool only when the registered tool is this
|
|
@@ -23,7 +25,8 @@ import type { ReadOnlyPolicy } from "../../lib/policy.ts";
|
|
|
23
25
|
import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
24
26
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
25
27
|
import { createPlanStore, type PlanStore } from "./plans.ts";
|
|
26
|
-
import type
|
|
28
|
+
import { sameSections, sectionDigests, type SectionDigest } from "./plan-text.ts";
|
|
29
|
+
import type { PlanModeEntry, ReviewBaselineEntry } from "./types.ts";
|
|
27
30
|
|
|
28
31
|
/** Tool the model calls to leave plan mode after approval. */
|
|
29
32
|
export const EXIT_TOOL = "exit_plan_mode";
|
|
@@ -33,8 +36,10 @@ export const ENTER_TOOL = "enter_plan_mode";
|
|
|
33
36
|
export const WRITE_PLAN_TOOL = "write_plan";
|
|
34
37
|
/** Tool the model calls to keep an approved plan's steps current. */
|
|
35
38
|
export const SET_PLAN_STEPS_TOOL = "set_plan_steps";
|
|
36
|
-
/** Custom-entry type used for
|
|
39
|
+
/** Custom-entry type used for plan-mode state. */
|
|
37
40
|
export const STATE_TYPE = "plan-mode";
|
|
41
|
+
/** Custom-entry type used for a plan's review baseline. */
|
|
42
|
+
export const BASELINE_TYPE = "plan-baseline";
|
|
38
43
|
|
|
39
44
|
/** Longest plan-file basename shown in the footer chip. */
|
|
40
45
|
const STATUS_NAME_LENGTH = 28;
|
|
@@ -58,27 +63,60 @@ export interface PlanRuntime {
|
|
|
58
63
|
lastPlanPath(): string | undefined;
|
|
59
64
|
/** Remember a written plan and repaint the footer. */
|
|
60
65
|
setLastPlan(ctx: ExtensionContext, path: string): void;
|
|
61
|
-
/** The
|
|
62
|
-
reviewed(path: string):
|
|
63
|
-
/** Remember what a review just
|
|
66
|
+
/** The sections the last review of `path` showed, restored with the branch. */
|
|
67
|
+
reviewed(path: string): SectionDigest[] | undefined;
|
|
68
|
+
/** Remember what a review just showed, as a session entry on this branch. */
|
|
64
69
|
markReviewed(path: string, content: string): void;
|
|
65
70
|
}
|
|
66
71
|
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
/** What the branch carries: the last plan-mode state, and a baseline per plan. */
|
|
73
|
+
interface BranchState {
|
|
74
|
+
mode: PlanModeEntry | undefined;
|
|
75
|
+
baselines: Map<string, SectionDigest[]>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Well-formed `{ title, digest }` pairs from untrusted branch data, or undefined. */
|
|
79
|
+
function readSections(value: unknown): SectionDigest[] | undefined {
|
|
80
|
+
if (!Array.isArray(value)) return undefined;
|
|
81
|
+
const sections: SectionDigest[] = [];
|
|
82
|
+
for (const raw of value) {
|
|
83
|
+
const section = raw as { title?: unknown; digest?: unknown } | null | undefined;
|
|
84
|
+
if (typeof section?.title !== "string" || typeof section.digest !== "string") return undefined;
|
|
85
|
+
sections.push({ title: section.title, digest: section.digest });
|
|
86
|
+
}
|
|
87
|
+
return sections;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The last plan-mode state and the last review baseline per plan on the branch.
|
|
92
|
+
* One pass, because reading the branch walks the whole session.
|
|
93
|
+
*/
|
|
94
|
+
function readBranchState(ctx: ExtensionContext): BranchState {
|
|
95
|
+
let mode: PlanModeEntry | undefined;
|
|
96
|
+
const baselines = new Map<string, SectionDigest[]>();
|
|
70
97
|
for (const raw of ctx.sessionManager.getBranch()) {
|
|
71
98
|
const entry = raw as { type?: string; customType?: string; data?: unknown };
|
|
72
|
-
if (entry.type !== "custom"
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
99
|
+
if (entry.type !== "custom") continue;
|
|
100
|
+
if (entry.customType === STATE_TYPE) {
|
|
101
|
+
const data = entry.data as { enabled?: unknown; removed?: unknown } | null | undefined;
|
|
102
|
+
if (typeof data?.enabled !== "boolean") continue;
|
|
103
|
+
// Branch data is untrusted; keep only string tool names.
|
|
104
|
+
const removed = Array.isArray(data.removed)
|
|
105
|
+
? data.removed.filter((name): name is string => typeof name === "string")
|
|
106
|
+
: undefined;
|
|
107
|
+
mode = removed ? { enabled: data.enabled, removed } : { enabled: data.enabled };
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (entry.customType !== BASELINE_TYPE) continue;
|
|
111
|
+
const data = entry.data as { path?: unknown; sections?: unknown } | null | undefined;
|
|
112
|
+
if (typeof data?.path !== "string") continue;
|
|
113
|
+
const sections = readSections(data.sections);
|
|
114
|
+
// Last entry per path wins, the same rule the mode entry follows. The key is
|
|
115
|
+
// canonical, so a path recorded on another platform or relative to another cwd
|
|
116
|
+
// still answers a lookup for the plan it names.
|
|
117
|
+
if (sections) baselines.set(resolve(data.path), sections);
|
|
80
118
|
}
|
|
81
|
-
return
|
|
119
|
+
return { mode, baselines };
|
|
82
120
|
}
|
|
83
121
|
|
|
84
122
|
/** Compare two paths, resolving symlinks when possible so `/tmp` and `/private/tmp` match. */
|
|
@@ -107,10 +145,10 @@ export function createPlanRuntime(
|
|
|
107
145
|
): PlanRuntime {
|
|
108
146
|
let enabled = false;
|
|
109
147
|
let lastPlanPath: string | undefined;
|
|
110
|
-
// The
|
|
111
|
-
// mark what changed.
|
|
112
|
-
//
|
|
113
|
-
const reviewedPlans = new Map<string,
|
|
148
|
+
// The section digests each plan path last showed in a review, so the next review
|
|
149
|
+
// can mark what changed. Persisted with the branch, so `/resume` and `/tree`
|
|
150
|
+
// compare against the review this branch actually showed.
|
|
151
|
+
const reviewedPlans = new Map<string, SectionDigest[]>();
|
|
114
152
|
// Tools plan mode deactivated, so disabling restores exactly those and does
|
|
115
153
|
// not re-activate a tool another extension intentionally hid.
|
|
116
154
|
let removedForPlan: string[] = [];
|
|
@@ -177,11 +215,12 @@ export function createPlanRuntime(
|
|
|
177
215
|
toggle: (ctx) => setEnabled(!enabled, ctx, true),
|
|
178
216
|
restore: (ctx) => {
|
|
179
217
|
lastPlanPath = undefined;
|
|
180
|
-
|
|
181
|
-
//
|
|
182
|
-
//
|
|
218
|
+
const branch = readBranchState(ctx);
|
|
219
|
+
// The baseline belongs to the branch that was reviewed: a plan this branch
|
|
220
|
+
// never showed has no digest here, so nothing looks changed until it does.
|
|
183
221
|
reviewedPlans.clear();
|
|
184
|
-
const
|
|
222
|
+
for (const [path, sections] of branch.baselines) reviewedPlans.set(path, sections);
|
|
223
|
+
const persisted = branch.mode;
|
|
185
224
|
// `--plan` starts a fresh session in plan mode; an explicit persisted
|
|
186
225
|
// choice (including a later disable) wins over the launch flag.
|
|
187
226
|
const fromFlag = persisted === undefined && pi.getFlag("plan") === true;
|
|
@@ -206,7 +245,13 @@ export function createPlanRuntime(
|
|
|
206
245
|
},
|
|
207
246
|
reviewed: (path) => reviewedPlans.get(resolve(path)),
|
|
208
247
|
markReviewed: (path, content) => {
|
|
209
|
-
|
|
248
|
+
const key = resolve(path);
|
|
249
|
+
const sections = sectionDigests(content);
|
|
250
|
+
const previous = reviewedPlans.get(key);
|
|
251
|
+
// A screen shown again over the same content must not grow the branch.
|
|
252
|
+
if (previous && sameSections(previous, sections)) return;
|
|
253
|
+
reviewedPlans.set(key, sections);
|
|
254
|
+
pi.appendEntry(BASELINE_TYPE, { path: key, sections } satisfies ReviewBaselineEntry);
|
|
210
255
|
},
|
|
211
256
|
};
|
|
212
257
|
}
|
package/extensions/plan/steps.ts
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* guided to write, the top-level steps that seed `todo` and drive the review
|
|
6
6
|
* gauge, the heading sections the review screen navigates, and the in-place
|
|
7
7
|
* checkbox edit that keeps the file current after approval. All of it is pure so
|
|
8
|
-
* the tools, the store, and the renderer agree on one parser.
|
|
8
|
+
* the tools, the store, and the renderer agree on one parser. The review's
|
|
9
|
+
* section comparison lives in `plan-text.ts`, which owns the text normalization
|
|
10
|
+
* the digest is computed over.
|
|
9
11
|
*
|
|
10
12
|
* Only top-level list items become steps: nested sub-bullets are notes, and the
|
|
11
13
|
* shallowest list level is treated as the plan. When the plan has a `Steps`
|
|
@@ -224,6 +226,38 @@ function elide(content: string): string {
|
|
|
224
226
|
return content.length > MAX_STEP_LENGTH ? `${content.slice(0, MAX_STEP_LENGTH - 1).trimEnd()}…` : content;
|
|
225
227
|
}
|
|
226
228
|
|
|
229
|
+
/** A list marker with an optional checkbox, at any indent. */
|
|
230
|
+
const LIST_PREFIX = /^\s*(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s*)?/;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The plan's `Verification` section as one trailing checklist item, or
|
|
234
|
+
* `undefined` when the plan has none.
|
|
235
|
+
*
|
|
236
|
+
* The section is prose ("commands and checks"), and a todo item is one line, so
|
|
237
|
+
* the lines are stripped of list markers and inline emphasis and joined with
|
|
238
|
+
* `; `, prefixed with `Verify:` and elided like a long step. The item is
|
|
239
|
+
* deliberately *not* a plan step: it is not in the `Steps` section, so it must
|
|
240
|
+
* not count against the step cap or appear in `set_plan_steps` numbering.
|
|
241
|
+
*
|
|
242
|
+
* A plan with no `Steps` heading feeds its shallowest list as steps (the older
|
|
243
|
+
* rule), so that list already covers the verification bullets; appending a
|
|
244
|
+
* summary of them would duplicate them, and there is nothing to carry.
|
|
245
|
+
*/
|
|
246
|
+
export function verificationItem(plan: string): PlanStep | undefined {
|
|
247
|
+
if (stepsSection(plan) === undefined) return undefined;
|
|
248
|
+
const section = extractPlanSections(plan).find(
|
|
249
|
+
(candidate) => candidate.level > 0 && candidate.title.toLowerCase() === "verification",
|
|
250
|
+
);
|
|
251
|
+
if (!section) return undefined;
|
|
252
|
+
const items = section.text
|
|
253
|
+
.split("\n")
|
|
254
|
+
.slice(1)
|
|
255
|
+
.map((line) => plainText(line.replace(LIST_PREFIX, "")))
|
|
256
|
+
.filter(Boolean);
|
|
257
|
+
if (items.length === 0) return undefined;
|
|
258
|
+
return { content: elide(`Verify: ${items.join("; ")}`), status: "pending" };
|
|
259
|
+
}
|
|
260
|
+
|
|
227
261
|
/** Top-level steps from a plan, in order. */
|
|
228
262
|
export function extractPlanSteps(plan: string): PlanStep[] {
|
|
229
263
|
return topLevelItems(plan).map((item) => ({ content: elide(item.step.content), status: item.step.status }));
|
|
@@ -294,30 +328,6 @@ export function setStepStatuses(plan: string, updates: StepUpdate[]): string | u
|
|
|
294
328
|
return lines.join("\n");
|
|
295
329
|
}
|
|
296
330
|
|
|
297
|
-
/**
|
|
298
|
-
* Per-section change flags for a second review: `boolean[]` aligned with
|
|
299
|
-
* `extractPlanSections(current)`, so duplicate headings are flagged by
|
|
300
|
-
* occurrence rather than by title. Revision snapshots are the caller's to
|
|
301
|
-
* provide; strip stored feedback first so a review comment does not look like
|
|
302
|
-
* an edit.
|
|
303
|
-
*/
|
|
304
|
-
export function changedSections(previous: string, current: string): boolean[] {
|
|
305
|
-
const before = new Map<string, string[]>();
|
|
306
|
-
for (const section of extractPlanSections(previous)) {
|
|
307
|
-
const texts = before.get(section.title);
|
|
308
|
-
if (texts) texts.push(section.text.trim());
|
|
309
|
-
else before.set(section.title, [section.text.trim()]);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const seen = new Map<string, number>();
|
|
313
|
-
return extractPlanSections(current).map((section) => {
|
|
314
|
-
const index = seen.get(section.title) ?? 0;
|
|
315
|
-
seen.set(section.title, index + 1);
|
|
316
|
-
const prior = before.get(section.title)?.[index];
|
|
317
|
-
return prior === undefined || prior !== section.text.trim();
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
|
|
321
331
|
/**
|
|
322
332
|
* Sections of a plan, split at heading lines outside code fences. The content
|
|
323
333
|
* before the first heading is a section titled `""`. Pi's `Markdown` renderer
|
package/extensions/plan/todos.ts
CHANGED
|
@@ -23,6 +23,11 @@ export interface TodoMerge {
|
|
|
23
23
|
kept: number;
|
|
24
24
|
/** Plan steps left out because the list reached its cap. */
|
|
25
25
|
deferred: number;
|
|
26
|
+
/**
|
|
27
|
+
* What happened to the trailing checklist item (the plan's verification
|
|
28
|
+
* summary): appended after the steps, already present, or left out by the cap.
|
|
29
|
+
*/
|
|
30
|
+
trailing?: "added" | "skipped" | "deferred";
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
/** Fallback list cap when the registered todo tool does not advertise one. */
|
|
@@ -38,8 +43,12 @@ function contentKey(content: string): string {
|
|
|
38
43
|
* keep their status, a legacy in-progress item without an `activeForm` gets its
|
|
39
44
|
* content as one so the todo tool accepts it, and the appended steps stop at
|
|
40
45
|
* `max` items — the rest are counted as deferred, never dropped.
|
|
46
|
+
*
|
|
47
|
+
* `trailing` is the plan's verification summary: it is appended after every step
|
|
48
|
+
* (still subject to the same duplicate and cap rules) but is not counted as a
|
|
49
|
+
* step, because it is not one.
|
|
41
50
|
*/
|
|
42
|
-
export function mergeTodos(existing: ExistingTodo[], steps: PlanStep[], max: number): TodoMerge {
|
|
51
|
+
export function mergeTodos(existing: ExistingTodo[], steps: PlanStep[], max: number, trailing?: PlanStep): TodoMerge {
|
|
43
52
|
const limit = Number.isInteger(max) && max > 0 ? max : DEFAULT_TODO_MAX;
|
|
44
53
|
const todos: ExistingTodo[] = existing.map((todo) =>
|
|
45
54
|
todo.status === "in_progress" && !todo.activeForm ? { ...todo, activeForm: todo.content } : todo,
|
|
@@ -63,5 +72,18 @@ export function mergeTodos(existing: ExistingTodo[], steps: PlanStep[], max: num
|
|
|
63
72
|
todos.push({ content: step.content, status: step.status });
|
|
64
73
|
added += 1;
|
|
65
74
|
}
|
|
66
|
-
|
|
75
|
+
|
|
76
|
+
let trailingOutcome: TodoMerge["trailing"];
|
|
77
|
+
if (trailing) {
|
|
78
|
+
const key = contentKey(trailing.content);
|
|
79
|
+
if (!key) trailingOutcome = undefined;
|
|
80
|
+
else if (seen.has(key)) trailingOutcome = "skipped";
|
|
81
|
+
else if (todos.length >= limit) trailingOutcome = "deferred";
|
|
82
|
+
else {
|
|
83
|
+
todos.push({ content: trailing.content, status: trailing.status });
|
|
84
|
+
trailingOutcome = "added";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { todos, added, skipped, kept: existing.length, deferred, trailing: trailingOutcome };
|
|
67
89
|
}
|
package/extensions/plan/tools.ts
CHANGED
|
@@ -19,7 +19,8 @@ import { StringEnum } from "@earendil-works/pi-ai";
|
|
|
19
19
|
import type { ExtensionAPI, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { Type, type Static } from "typebox";
|
|
21
21
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
22
|
-
import {
|
|
22
|
+
import { lastGoal } from "../../lib/goal-state.ts";
|
|
23
|
+
import { GOAL_TOOL, TODO_TOOL } from "../../lib/tool-names.ts";
|
|
23
24
|
import { lastTodos } from "../../lib/todo-state.ts";
|
|
24
25
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
25
26
|
import { rejectionLine, reuseCallText, toolCallTitle } from "../../lib/tui/tool.ts";
|
|
@@ -34,13 +35,15 @@ import {
|
|
|
34
35
|
PLAN_OUTLINE,
|
|
35
36
|
extractPlanSteps,
|
|
36
37
|
planStepSummary,
|
|
38
|
+
verificationItem,
|
|
37
39
|
type PlanStep,
|
|
38
|
-
type PlanStepSummary,
|
|
39
40
|
type StepUpdate,
|
|
40
41
|
} from "./steps.ts";
|
|
42
|
+
import { basename } from "node:path";
|
|
43
|
+
import { planTitle, type StoredPlan } from "./plans.ts";
|
|
41
44
|
import { DEFAULT_TODO_MAX, mergeTodos, type TodoMerge } from "./todos.ts";
|
|
42
45
|
import { reviewPlan } from "./review.ts";
|
|
43
|
-
import { sanitizePlanText } from "./
|
|
46
|
+
import { sanitizePlanText } from "./plan-text.ts";
|
|
44
47
|
import type { EnterPlanModeDetails, ExitPlanModeDetails, SetPlanStepsDetails, WritePlanDetails } from "./types.ts";
|
|
45
48
|
import { createPlanWebReview, type PlanWebReview } from "./web.ts";
|
|
46
49
|
|
|
@@ -116,20 +119,21 @@ function preview(plan: string): { body: string; more: number } {
|
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
/**
|
|
119
|
-
* One sentence
|
|
120
|
-
*
|
|
122
|
+
* One sentence when a step is longer than the display cap. The text stays in the
|
|
123
|
+
* plan file either way; only the todo list and the approval listing shorten it.
|
|
121
124
|
*/
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
125
|
+
function stepElisionNote(elided: number): string {
|
|
126
|
+
if (elided === 0) return "";
|
|
127
|
+
const plural = elided === 1 ? " is" : "s are";
|
|
128
|
+
return ` Note: ${elided} step${plural} longer than ${MAX_STEP_LENGTH} characters and will be shortened in the todo list.`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The diagnosis both cap refusals share: a step list longer than approval can
|
|
133
|
+
* track. Each caller appends the fix that is available from where it stands.
|
|
134
|
+
*/
|
|
135
|
+
function overCap(total: number): string {
|
|
136
|
+
return `The plan has ${total} top-level steps but approval tracks at most ${MAX_STEPS}.`;
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
/** "Step 2" or "Steps 1, 2" for a set of step numbers. */
|
|
@@ -171,11 +175,12 @@ async function seedTodos(
|
|
|
171
175
|
plan: string,
|
|
172
176
|
): Promise<{ steps: PlanStep[]; merged?: TodoMerge; error?: string }> {
|
|
173
177
|
const steps = extractPlanSteps(plan);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
//
|
|
178
|
-
|
|
178
|
+
const verification = verificationItem(plan);
|
|
179
|
+
if (steps.length === 0 && !verification) return { steps };
|
|
180
|
+
const merged = mergeTodos(lastTodos(ctx.sessionManager.getBranch()), steps, todoMaxItems(pi), verification);
|
|
181
|
+
// Nothing new to write: every step (and the verification item) is already in
|
|
182
|
+
// the list, or the list is full, so a rewrite would only churn the widget.
|
|
183
|
+
if (merged.added === 0 && merged.trailing !== "added") return { steps, merged };
|
|
179
184
|
const outcome = await ctx.executeTool(TODO_TOOL, { todos: merged.todos });
|
|
180
185
|
if (!outcome.isError) return { steps, merged };
|
|
181
186
|
// The nested call fails both when the tool is unavailable and when it rejects
|
|
@@ -185,6 +190,56 @@ async function seedTodos(
|
|
|
185
190
|
return { steps, merged, error: firstLine ? sanitizeLine(firstLine) : undefined };
|
|
186
191
|
}
|
|
187
192
|
|
|
193
|
+
/** What the goal offer did: `recorded` undefined when there was nothing to offer. */
|
|
194
|
+
interface GoalOffer {
|
|
195
|
+
objective: string;
|
|
196
|
+
recorded?: boolean;
|
|
197
|
+
error?: string;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Offer to record the approved plan as the session goal — only when the session
|
|
202
|
+
* has no goal at all (an active or achieved one is the user's, never replaced) and
|
|
203
|
+
* the session can ask. The objective is the plan's own `# title`, or the file-name
|
|
204
|
+
* slug; recording runs through the goal tool, so validation and the widget stay
|
|
205
|
+
* goal's, and no model turn is spent.
|
|
206
|
+
*/
|
|
207
|
+
async function offerSessionGoal(ctx: ExtensionToolContext, plan: StoredPlan): Promise<GoalOffer> {
|
|
208
|
+
const objective = planObjective(plan);
|
|
209
|
+
if (!ctx.hasUI) return { objective };
|
|
210
|
+
// The branch already carries a goal: leave it alone, and say nothing.
|
|
211
|
+
if (lastGoal(ctx.sessionManager.getBranch()) !== null) return { objective };
|
|
212
|
+
const approved = await ctx.ui.confirm("Record this plan as the session goal?", objective);
|
|
213
|
+
if (!approved) return { objective };
|
|
214
|
+
const outcome = await ctx.executeTool(GOAL_TOOL, { objective });
|
|
215
|
+
if (!outcome.isError) return { objective, recorded: true };
|
|
216
|
+
const part = outcome.result?.content?.find((entry) => entry.type === "text");
|
|
217
|
+
const firstLine = part?.type === "text" ? part.text.split("\n", 1)[0] : undefined;
|
|
218
|
+
return { objective, recorded: false, error: firstLine ? sanitizeLine(firstLine) : undefined };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The plan's own title: its first `# heading`, else the file-name slug. */
|
|
222
|
+
function planObjective(plan: StoredPlan): string {
|
|
223
|
+
const heading = /^ {0,3}#\s+(.+)$/m.exec(plan.content)?.[1];
|
|
224
|
+
const title = heading === undefined ? "" : sanitizeLine(heading).trim();
|
|
225
|
+
return title || planTitle(basename(plan.path));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* One sentence about the plan's verification item, or "" when the plan has none
|
|
230
|
+
* (or it was already in the list). The item is a todo, not a plan step, so it is
|
|
231
|
+
* ticked in the list rather than in the file.
|
|
232
|
+
*/
|
|
233
|
+
function verificationNote(merged: TodoMerge | undefined): string {
|
|
234
|
+
if (merged?.trailing === "added") {
|
|
235
|
+
return "\nThe plan's verification section is the last todo item; tick it in the todo list, not in the file.";
|
|
236
|
+
}
|
|
237
|
+
if (merged?.trailing === "deferred") {
|
|
238
|
+
return "\nThe plan's verification section did not fit in the full todo list; keep it in mind.";
|
|
239
|
+
}
|
|
240
|
+
return "";
|
|
241
|
+
}
|
|
242
|
+
|
|
188
243
|
/** What the merge did, for the approval message; `steps` is the plan's step count. */
|
|
189
244
|
function mergeSummary(merged: TodoMerge, steps: PlanStep[]): string {
|
|
190
245
|
const total = steps.length;
|
|
@@ -287,7 +342,8 @@ export function registerTools(
|
|
|
287
342
|
"Save the plan you designed as a markdown file under .pi/plans (project root, or the agent directory outside a " +
|
|
288
343
|
"repository). Write the complete plan, not a summary, in this outline so it is consistent and scannable:\n\n" +
|
|
289
344
|
`${PLAN_OUTLINE}\n\n` +
|
|
290
|
-
"Returns the path to pass to exit_plan_mode; plan_path overwrites an existing file when refining."
|
|
345
|
+
"Returns the path to pass to exit_plan_mode; plan_path overwrites an existing file when refining. " +
|
|
346
|
+
`A plan with more than ${MAX_STEPS} top-level steps in its Steps section is refused, so approval never drops steps it cannot track.`,
|
|
291
347
|
promptSnippet: "Save the designed plan as a markdown file under .pi/plans.",
|
|
292
348
|
promptGuidelines: [
|
|
293
349
|
"While plan mode is active, write the full plan with write_plan, then call exit_plan_mode with the returned path.",
|
|
@@ -308,6 +364,21 @@ export function registerTools(
|
|
|
308
364
|
};
|
|
309
365
|
}
|
|
310
366
|
const args = params as WritePlanArgs;
|
|
367
|
+
const summary = planStepSummary(args.content);
|
|
368
|
+
if (summary.total > MAX_STEPS) {
|
|
369
|
+
// Refuse before writing: a note after the fact arrives once the file exists and
|
|
370
|
+
// the review has listed every step, so the overflow is already lost.
|
|
371
|
+
return {
|
|
372
|
+
content: [
|
|
373
|
+
{
|
|
374
|
+
type: "text",
|
|
375
|
+
text: `${overCap(summary.total)} Nothing was written; group the steps (one outcome each, sub-bullets for detail) and call write_plan again.`,
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
details: undefined,
|
|
379
|
+
isError: true,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
311
382
|
try {
|
|
312
383
|
const file = await runtime.plans.write(ctx.cwd, {
|
|
313
384
|
title: args.title,
|
|
@@ -319,7 +390,7 @@ export function registerTools(
|
|
|
319
390
|
content: [
|
|
320
391
|
{
|
|
321
392
|
type: "text",
|
|
322
|
-
text: `Wrote the plan to ${file.relativePath}. Call exit_plan_mode with plan_path: ${file.path}${
|
|
393
|
+
text: `Wrote the plan to ${file.relativePath}. Call exit_plan_mode with plan_path: ${file.path}${stepElisionNote(summary.elided)}`,
|
|
323
394
|
},
|
|
324
395
|
],
|
|
325
396
|
details: { path: file.path, relativePath: file.relativePath, bytes: file.bytes } satisfies WritePlanDetails,
|
|
@@ -394,6 +465,27 @@ export function registerTools(
|
|
|
394
465
|
};
|
|
395
466
|
}
|
|
396
467
|
|
|
468
|
+
const total = planStepSummary(file.content).total;
|
|
469
|
+
if (total > MAX_STEPS) {
|
|
470
|
+
// A hand-edited or older plan file can still be over the cap; refuse it here
|
|
471
|
+
// too, so approval never seeds a list it cannot hold in full.
|
|
472
|
+
return {
|
|
473
|
+
content: [
|
|
474
|
+
{
|
|
475
|
+
type: "text",
|
|
476
|
+
text: `${overCap(total)} It was not presented for approval; call write_plan with plan_path to group the steps, then exit_plan_mode again.`,
|
|
477
|
+
},
|
|
478
|
+
],
|
|
479
|
+
details: {
|
|
480
|
+
approved: false,
|
|
481
|
+
plan: file.content,
|
|
482
|
+
planPath: file.path,
|
|
483
|
+
relativePath: file.relativePath,
|
|
484
|
+
} satisfies ExitPlanModeDetails,
|
|
485
|
+
isError: true,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
397
489
|
if (!ctx.hasUI) {
|
|
398
490
|
return {
|
|
399
491
|
content: [
|
|
@@ -464,6 +556,7 @@ export function registerTools(
|
|
|
464
556
|
|
|
465
557
|
runtime.disable(ctx);
|
|
466
558
|
const { steps, merged, error } = await seedTodos(pi, ctx, decision.plan.content);
|
|
559
|
+
const goal = await offerSessionGoal(ctx, decision.plan);
|
|
467
560
|
const listing =
|
|
468
561
|
steps.length > 0
|
|
469
562
|
? `\n\n${steps
|
|
@@ -478,6 +571,13 @@ export function registerTools(
|
|
|
478
571
|
: merged
|
|
479
572
|
? mergeSummary(merged, steps)
|
|
480
573
|
: ` Could not read the todo list. Record these steps with the todo tool before you start, and tick them off with ${SET_PLAN_STEPS_TOOL}.`;
|
|
574
|
+
const verification = verificationNote(merged);
|
|
575
|
+
const goalNote =
|
|
576
|
+
goal.recorded === undefined
|
|
577
|
+
? ""
|
|
578
|
+
: goal.recorded
|
|
579
|
+
? ` Recorded the session goal: ${goal.objective}.`
|
|
580
|
+
: ` Could not record the session goal: ${goal.error ?? "unknown error"}. Use the goal tool to set it.`;
|
|
481
581
|
const browserNote = decision.comments.length
|
|
482
582
|
? `\n\nThe user approved from the browser review with these comments:\n${feedbackSummary(decision.comments)}`
|
|
483
583
|
: "";
|
|
@@ -485,7 +585,7 @@ export function registerTools(
|
|
|
485
585
|
content: [
|
|
486
586
|
{
|
|
487
587
|
type: "text",
|
|
488
|
-
text: `Plan approved. Plan mode is off and write access is restored. The plan file ${decision.plan.relativePath} is the source of truth; follow its steps.${browserNote}${tail}${listing}`,
|
|
588
|
+
text: `Plan approved. Plan mode is off and write access is restored. The plan file ${decision.plan.relativePath} is the source of truth; follow its steps.${browserNote}${tail}${verification}${goalNote}${listing}`,
|
|
489
589
|
},
|
|
490
590
|
],
|
|
491
591
|
details: {
|
package/extensions/plan/tui.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Terminal rendering for the plan review screen.
|
|
3
3
|
*
|
|
4
|
-
* `sanitizePlanText` is exported so the transcript renderer (`tools.ts`) can
|
|
5
|
-
* strip control characters from plan text with the same rules the review screen
|
|
6
|
-
* uses.
|
|
7
|
-
*
|
|
8
4
|
* `PlanViewComponent` is the scrollable review screen `exit_plan_mode` opens
|
|
9
5
|
* for approval. The plan body is rendered by Pi's own `Markdown` component with
|
|
10
6
|
* the active Markdown theme, so headings, lists, code, links, and quotes match
|
|
@@ -30,7 +26,7 @@ import {
|
|
|
30
26
|
type TuiMouseEvent,
|
|
31
27
|
type TuiMouseEventResult,
|
|
32
28
|
} from "@earendil-works/pi-tui";
|
|
33
|
-
import { sanitizeLine
|
|
29
|
+
import { sanitizeLine } from "../../lib/format.ts";
|
|
34
30
|
import {
|
|
35
31
|
editorTheme,
|
|
36
32
|
screenHint,
|
|
@@ -45,8 +41,8 @@ import { titleRule } from "../../lib/tui/rules.ts";
|
|
|
45
41
|
import { emptyState, checkbox } from "../../lib/tui/rows.ts";
|
|
46
42
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
47
43
|
import { planTitle, type StoredPlan } from "./plans.ts";
|
|
48
|
-
import { changedSections,
|
|
49
|
-
import {
|
|
44
|
+
import { changedSections, sanitizePlanText, type SectionDigest } from "./plan-text.ts";
|
|
45
|
+
import { extractPlanSections } from "./steps.ts";
|
|
50
46
|
|
|
51
47
|
/** What the user chose from the read/review screen. */
|
|
52
48
|
export type PlanViewAction = "approve" | "refine" | "keep" | "browser";
|
|
@@ -71,10 +67,10 @@ interface BodyCache {
|
|
|
71
67
|
export interface PlanViewOptions {
|
|
72
68
|
plan: StoredPlan;
|
|
73
69
|
/**
|
|
74
|
-
* The
|
|
75
|
-
* Used only to mark the sections that changed since then.
|
|
70
|
+
* The section digests the previous review of this plan showed, when there was
|
|
71
|
+
* one. Used only to mark the sections that changed since then.
|
|
76
72
|
*/
|
|
77
|
-
|
|
73
|
+
baseline?: SectionDigest[];
|
|
78
74
|
theme: Theme;
|
|
79
75
|
/**
|
|
80
76
|
* Called once when the screen closes. `refinement` carries the typed text
|
|
@@ -110,17 +106,6 @@ const PRIMARY_HINTS: ScreenHint[] = [
|
|
|
110
106
|
const SCROLL_HINTS: ScreenHint[] = ["↑/↓ or k/j scroll", "space/b page", "g/G ends"];
|
|
111
107
|
/** Section-jump hint, shown when the plan has more than one heading section. */
|
|
112
108
|
const SECTION_HINTS: ScreenHint[] = ["n/p section"];
|
|
113
|
-
/** Spaces a tab expands to; the terminal's own tab stops are not width-modelled. */
|
|
114
|
-
const TAB_WIDTH = 4;
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Replace control characters (including ESC and the C1 block) and expand tabs
|
|
118
|
-
* so model-authored plan text cannot drive the terminal. Newlines are kept, so
|
|
119
|
-
* callers can still render or split the text line by line.
|
|
120
|
-
*/
|
|
121
|
-
export function sanitizePlanText(text: string): string {
|
|
122
|
-
return stripControlChars(text.replace(/\t/g, " ".repeat(TAB_WIDTH)));
|
|
123
|
-
}
|
|
124
109
|
|
|
125
110
|
/** A task checkbox in a list marker: `- [x] `, `1. [ ] `. */
|
|
126
111
|
const TASK_MARKER = /^([-*+]|\d+[.)])(\s+)(\[([ xX])\])(\s*)$/;
|
|
@@ -210,13 +195,7 @@ export class PlanViewComponent implements Component {
|
|
|
210
195
|
const base = this.options.markdownTheme ?? getMarkdownTheme();
|
|
211
196
|
const theme = planMarkdownTheme(this.theme, base);
|
|
212
197
|
const current = sanitizePlanText(this.options.plan.content);
|
|
213
|
-
|
|
214
|
-
// raw, so sanitizing only `current` would report a tab as a change on every
|
|
215
|
-
// review.
|
|
216
|
-
const changed =
|
|
217
|
-
this.options.previous === undefined
|
|
218
|
-
? []
|
|
219
|
-
: changedSections(stripFeedback(sanitizePlanText(this.options.previous)), stripFeedback(current));
|
|
198
|
+
const changed = changedSections(this.options.baseline, current);
|
|
220
199
|
const chunks = extractPlanSections(current);
|
|
221
200
|
const lines: string[] = [];
|
|
222
201
|
const sections: SectionOffset[] = [];
|
package/extensions/plan/types.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { PlanAnnotation } from "./feedback.ts";
|
|
6
|
+
import type { SectionDigest } from "./plan-text.ts";
|
|
6
7
|
import type { StepUpdate } from "./steps.ts";
|
|
7
8
|
|
|
8
9
|
/** Persisted (custom entry) plan-mode state. */
|
|
@@ -15,6 +16,14 @@ export interface PlanModeEntry {
|
|
|
15
16
|
removed?: string[];
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
/** Persisted (custom entry) review baseline for one plan. */
|
|
20
|
+
export interface ReviewBaselineEntry {
|
|
21
|
+
/** Resolved path of the plan file the review showed. */
|
|
22
|
+
path: string;
|
|
23
|
+
/** One digest per heading section, in document order. */
|
|
24
|
+
sections: SectionDigest[];
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
/** Structured result carried in `enter_plan_mode`'s `details`. */
|
|
19
28
|
export interface EnterPlanModeDetails {
|
|
20
29
|
/** Whether the user agreed to enter plan mode. */
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import {
|
|
17
17
|
brandLockup,
|
|
18
18
|
docHead,
|
|
19
|
+
SIDEBAR_COLLAPSE_MAX,
|
|
19
20
|
themeToggle,
|
|
20
21
|
WEB_BASE_CSS,
|
|
21
22
|
WEB_COMPONENTS_CSS,
|
|
@@ -90,7 +91,6 @@ ${WEB_COMPONENTS_CSS}
|
|
|
90
91
|
.review-progress__fill{display:block;height:100%;width:0;background:var(--accent);transition:width var(--motion-fast) linear}
|
|
91
92
|
/* Review panel: the plan's review facts, the section outline, then the keys. */
|
|
92
93
|
.bf-app__body--single{grid-template-columns:minmax(0,1fr)}
|
|
93
|
-
.bf-app__sidebar .bf-panel{flex:1 1 auto;min-height:0;min-width:0}
|
|
94
94
|
.review-panel{display:flex;flex-direction:column;gap:var(--space-3);padding:var(--space-4) var(--space-4) var(--space-2)}
|
|
95
95
|
.review-facts{display:flex;flex-direction:column;gap:var(--space-2);padding-bottom:var(--space-3);border-bottom:1px solid var(--border)}
|
|
96
96
|
.review-fact{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);font-size:var(--fs-sm);color:var(--muted)}
|
|
@@ -172,7 +172,7 @@ ${WEB_COMPONENTS_CSS}
|
|
|
172
172
|
.review-nav .lvl-2 a,.review-nav .lvl-3 a,.review-nav .lvl-4 a,.review-nav .lvl-5 a,.review-nav .lvl-6 a{padding-left:8px}
|
|
173
173
|
.review-hints{display:none}
|
|
174
174
|
}
|
|
175
|
-
@media (max-width
|
|
175
|
+
@media (max-width:${SIDEBAR_COLLAPSE_MAX}px){
|
|
176
176
|
.bf-review{padding:var(--space-4) 18px 80px}
|
|
177
177
|
.review-nav__title{display:none}
|
|
178
178
|
}
|