@gavin-hu/my-pi-agent 0.19.0 → 0.20.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/README.md +17 -11
- package/README.zh-CN.md +13 -8
- package/docs/extensions/README.md +1 -1
- package/docs/extensions/board.md +286 -123
- package/docs/extensions/bytefly.md +381 -223
- package/docs/extensions/file-browse.md +14 -7
- package/docs/extensions/job.md +1 -2
- package/docs/extensions/plan.md +56 -11
- package/docs/extensions/session-bus.md +23 -10
- package/docs/extensions/team.md +45 -17
- package/docs/extensions/wechat.md +2 -1
- package/docs/extensions/worktree.md +30 -15
- package/extensions/board/commands.ts +23 -5
- package/extensions/board/format.ts +107 -9
- package/extensions/board/join.ts +1 -1
- package/extensions/board/page.ts +170 -94
- package/extensions/board/result.ts +9 -0
- package/extensions/board/schema.ts +22 -7
- package/extensions/board/seat.ts +9 -10
- package/extensions/board/snapshot.ts +1 -0
- package/extensions/board/tools.ts +186 -21
- package/extensions/board/transport/router.ts +27 -108
- package/extensions/board/transport/server.ts +18 -29
- package/extensions/board/types.ts +67 -0
- package/extensions/board/view/client.ts +23 -13
- package/extensions/board/view/dialog.ts +43 -0
- package/extensions/board/view/keys.ts +28 -24
- package/extensions/board/view/live.ts +5 -171
- package/extensions/board/view/names.ts +91 -0
- package/extensions/board/view/page.ts +48 -88
- package/extensions/board/view/render.ts +213 -190
- package/extensions/board/view/styles.ts +65 -48
- package/extensions/board/view/team.ts +63 -41
- package/extensions/bytefly/footer/budget.ts +6 -5
- package/extensions/bytefly/footer/collect/entries.ts +12 -4
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
- package/extensions/bytefly/footer/config.ts +19 -8
- package/extensions/bytefly/footer/price/price.ts +30 -8
- package/extensions/bytefly/footer/register.ts +5 -5
- package/extensions/bytefly/footer/render/format.ts +18 -0
- package/extensions/bytefly/footer/render/lines.ts +23 -7
- package/extensions/bytefly/footer/render/segments.ts +117 -31
- package/extensions/bytefly/footer/types.ts +29 -5
- package/extensions/bytefly/footer/unpriced.ts +42 -17
- package/extensions/bytefly/panel/commands.ts +28 -1
- package/extensions/bytefly/panel/filter.ts +68 -0
- package/extensions/bytefly/panel/format.ts +16 -15
- package/extensions/bytefly/panel/join.ts +56 -13
- package/extensions/bytefly/panel/prices.ts +43 -0
- package/extensions/bytefly/panel/settings.ts +72 -23
- package/extensions/bytefly/panel/tui/entries.ts +26 -8
- package/extensions/bytefly/panel/tui/frame.ts +130 -93
- package/extensions/bytefly/panel/tui/keys.ts +352 -0
- package/extensions/bytefly/panel/tui/list-view.ts +25 -46
- package/extensions/bytefly/panel/tui/panes.ts +76 -0
- package/extensions/bytefly/panel/tui/section-view.ts +126 -0
- package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
- package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
- package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
- package/extensions/bytefly/panel/tui.ts +306 -259
- package/extensions/check/commands.ts +2 -1
- package/extensions/file-browse/files/classify.ts +136 -0
- package/extensions/file-browse/files/files.ts +8 -547
- package/extensions/file-browse/files/index.ts +90 -0
- package/extensions/file-browse/files/list.ts +261 -0
- package/extensions/file-browse/files/tree.ts +101 -0
- package/extensions/file-browse/transport/router.ts +0 -2
- package/extensions/file-browse/view/emit.ts +51 -0
- package/extensions/file-browse/view/html.ts +6 -7
- package/extensions/file-browse/view/markdown.ts +11 -13
- package/extensions/file-browse/view/raw-html.ts +18 -13
- package/extensions/job/runtime.ts +4 -4
- package/extensions/plan/feedback.ts +40 -16
- package/extensions/plan/format.ts +10 -3
- package/extensions/plan/index.ts +17 -1
- package/extensions/plan/plan-files.ts +88 -0
- package/extensions/plan/plans.ts +19 -23
- package/extensions/plan/policy.ts +10 -5
- package/extensions/plan/runtime.ts +31 -2
- package/extensions/plan/todos.ts +51 -3
- package/extensions/plan/tools/exit.ts +10 -1
- package/extensions/plan/tools/todo-seed.ts +4 -2
- package/extensions/plan/tools/write.ts +12 -1
- package/extensions/plan/types.ts +15 -0
- package/extensions/plan/web-blocks.ts +89 -0
- package/extensions/plan/web-chrome.ts +3 -5
- package/extensions/plan/web-client.ts +179 -330
- package/extensions/plan/web-collab.ts +3 -3
- package/extensions/plan/web-comments.ts +18 -8
- package/extensions/plan/web-composer.ts +80 -0
- package/extensions/plan/web-draft.ts +40 -0
- package/extensions/plan/web-keys.ts +134 -0
- package/extensions/plan/web-outline.ts +192 -0
- package/extensions/plan/web-page.ts +4 -1
- package/extensions/plan/web-plan.ts +150 -0
- package/extensions/plan/web-router.ts +0 -1
- package/extensions/plan/web-settle.ts +194 -0
- package/extensions/rewind/commands/review.ts +2 -1
- package/extensions/session-bus/rows.ts +7 -3
- package/extensions/session-bus/runtime.ts +100 -38
- package/extensions/session-bus/store.ts +14 -2
- package/extensions/session-bus/tui.ts +1 -1
- package/extensions/session-bus/types.ts +10 -1
- package/extensions/subagent/commands.ts +2 -1
- package/extensions/team/commands.ts +41 -8
- package/extensions/team/format.ts +14 -0
- package/extensions/team/index.ts +2 -1
- package/extensions/team/tools.ts +30 -9
- package/extensions/team/types.ts +17 -0
- package/extensions/todo/index.ts +4 -1
- package/extensions/todo/nudge.ts +8 -2
- package/extensions/wechat/bridge.ts +43 -172
- package/extensions/wechat/intake.ts +245 -0
- package/extensions/worktree/commands.ts +24 -17
- package/extensions/worktree/exit.ts +12 -6
- package/extensions/worktree/git.ts +22 -14
- package/extensions/worktree/guard.ts +15 -285
- package/extensions/worktree/handoff.ts +10 -2
- package/extensions/worktree/index.ts +2 -1
- package/extensions/worktree/lifecycle.ts +2 -0
- package/extensions/worktree/registry.ts +20 -11
- package/extensions/worktree/root-tools.ts +13 -3
- package/extensions/worktree/runtime.ts +8 -2
- package/extensions/worktree/screen.ts +15 -8
- package/extensions/worktree/shell.ts +303 -0
- package/extensions/worktree/state.ts +1 -1
- package/extensions/worktree/status.ts +42 -9
- package/extensions/worktree/tools.ts +19 -13
- package/lib/README.md +6 -5
- package/lib/board/card.ts +192 -3
- package/lib/board/links.ts +26 -2
- package/lib/board/page-pointer.ts +86 -32
- package/lib/board/round-text.ts +11 -2
- package/lib/board/round.ts +25 -20
- package/lib/board/rules.ts +144 -11
- package/lib/board/store.ts +239 -52
- package/lib/board/view.ts +94 -10
- package/lib/format.ts +22 -0
- package/lib/http/serve.ts +3 -0
- package/lib/http/sink.ts +34 -9
- package/lib/plan-entry.ts +11 -0
- package/lib/policy.ts +14 -1
- package/lib/session-roster.ts +74 -3
- package/lib/spawn.ts +37 -3
- package/lib/stray-fields.ts +45 -0
- package/lib/tool-names.ts +13 -0
- package/lib/tui/glyphs.ts +2 -3
- package/lib/web/README.md +52 -2
- package/lib/web/client-runtime.ts +123 -0
- package/lib/web/index.ts +4 -2
- package/lib/web/markup.ts +88 -1
- package/lib/web/names.ts +42 -0
- package/package.json +1 -1
- package/extensions/board/view/actions.ts +0 -306
- package/extensions/board/writes.ts +0 -99
- package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
- package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
- package/extensions/job/process.ts +0 -35
package/extensions/plan/todos.ts
CHANGED
|
@@ -22,6 +22,16 @@ export interface TodoMerge {
|
|
|
22
22
|
skipped: number;
|
|
23
23
|
/** Existing items kept as they were. */
|
|
24
24
|
kept: number;
|
|
25
|
+
/**
|
|
26
|
+
* Untouched items dropped because they were seeded by an earlier approval of this
|
|
27
|
+
* plan and are no longer one of its steps: the plan was renamed or rewritten.
|
|
28
|
+
*/
|
|
29
|
+
replaced: number;
|
|
30
|
+
/**
|
|
31
|
+
* Older seeds kept because someone had already started or finished them. Deleting one
|
|
32
|
+
* would erase work a reader did, so they are reported instead of replaced.
|
|
33
|
+
*/
|
|
34
|
+
stale: number;
|
|
25
35
|
/** Plan steps left out because the list reached its cap. */
|
|
26
36
|
deferred: number;
|
|
27
37
|
/**
|
|
@@ -89,9 +99,38 @@ function contentKey(content: string): string {
|
|
|
89
99
|
* (still subject to the same duplicate and cap rules) but is not counted as a
|
|
90
100
|
* step, because it is not one.
|
|
91
101
|
*/
|
|
92
|
-
export function mergeTodos(
|
|
102
|
+
export function mergeTodos(
|
|
103
|
+
existing: ExistingTodo[],
|
|
104
|
+
steps: PlanStep[],
|
|
105
|
+
max: number,
|
|
106
|
+
trailing?: PlanStep,
|
|
107
|
+
/** What an earlier approval of this plan seeded, when there was one. */
|
|
108
|
+
previous?: readonly string[],
|
|
109
|
+
): TodoMerge {
|
|
93
110
|
const limit = Number.isInteger(max) && max > 0 ? max : DEFAULT_TODO_MAX;
|
|
94
|
-
|
|
111
|
+
// A plan refined after approval is approved again over the list its first approval
|
|
112
|
+
// made. An item this plan seeded that is no longer one of its steps is a leftover, and
|
|
113
|
+
// only an item *this plan seeded* is a candidate: a user's own item is never touched.
|
|
114
|
+
const ours = new Set((previous ?? []).map(contentKey));
|
|
115
|
+
const current = new Set(steps.map((step) => contentKey(step.content)));
|
|
116
|
+
if (trailing) current.add(contentKey(trailing.content));
|
|
117
|
+
let replaced = 0;
|
|
118
|
+
let stale = 0;
|
|
119
|
+
const kept: ExistingTodo[] = [];
|
|
120
|
+
for (const todo of existing) {
|
|
121
|
+
const key = contentKey(todo.content);
|
|
122
|
+
if (ours.has(key) && !current.has(key)) {
|
|
123
|
+
// An untouched leftover is bookkeeping and goes; one someone acted on is
|
|
124
|
+
// evidence of work, and is named rather than deleted.
|
|
125
|
+
if (todo.status === "pending") {
|
|
126
|
+
replaced += 1;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
stale += 1;
|
|
130
|
+
}
|
|
131
|
+
kept.push(todo);
|
|
132
|
+
}
|
|
133
|
+
const todos: ExistingTodo[] = kept.map((todo) =>
|
|
95
134
|
todo.status === "in_progress" && !todo.activeForm ? { ...todo, activeForm: todo.content } : todo,
|
|
96
135
|
);
|
|
97
136
|
const seen = new Set(todos.map((todo) => contentKey(todo.content)));
|
|
@@ -126,5 +165,14 @@ export function mergeTodos(existing: ExistingTodo[], steps: PlanStep[], max: num
|
|
|
126
165
|
}
|
|
127
166
|
}
|
|
128
167
|
|
|
129
|
-
return {
|
|
168
|
+
return {
|
|
169
|
+
todos,
|
|
170
|
+
added,
|
|
171
|
+
skipped,
|
|
172
|
+
kept: existing.length - replaced,
|
|
173
|
+
replaced,
|
|
174
|
+
stale,
|
|
175
|
+
deferred,
|
|
176
|
+
trailing: trailingOutcome,
|
|
177
|
+
};
|
|
130
178
|
}
|
|
@@ -213,7 +213,16 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
|
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
runtime.disable(ctx);
|
|
216
|
-
|
|
216
|
+
// A plan refined and approved again seeds over the list its first approval made:
|
|
217
|
+
// the steps that approval appended are the ones this one may replace.
|
|
218
|
+
const previous = runtime.seeded(decision.plan.path);
|
|
219
|
+
const { steps, merged, error } = await seedTodos(pi, ctx, decision.plan.content, previous);
|
|
220
|
+
if (error === undefined && steps.length > 0) {
|
|
221
|
+
runtime.markSeeded(
|
|
222
|
+
decision.plan.path,
|
|
223
|
+
steps.map((step) => step.content),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
217
226
|
const goal = await finalizeGoal(pi, ctx, decision.plan);
|
|
218
227
|
const listing =
|
|
219
228
|
steps.length > 0
|
|
@@ -36,14 +36,16 @@ export async function seedTodos(
|
|
|
36
36
|
pi: ExtensionAPI,
|
|
37
37
|
ctx: ExtensionToolContext,
|
|
38
38
|
plan: string,
|
|
39
|
+
/** What an earlier approval of the same plan appended, when it was approved before. */
|
|
40
|
+
previous?: readonly string[],
|
|
39
41
|
): Promise<{ steps: PlanStep[]; merged?: TodoMerge; error?: string }> {
|
|
40
42
|
const steps = extractPlanSteps(plan);
|
|
41
43
|
const verification = verificationItem(plan);
|
|
42
44
|
if (steps.length === 0 && !verification) return { steps };
|
|
43
|
-
const merged = mergeTodos(lastTodos(ctx.sessionManager.getBranch()), steps, todoMaxItems(pi), verification);
|
|
45
|
+
const merged = mergeTodos(lastTodos(ctx.sessionManager.getBranch()), steps, todoMaxItems(pi), verification, previous);
|
|
44
46
|
// Nothing new to write: every step (and the verification item) is already in
|
|
45
47
|
// the list, or the list is full, so a rewrite would only churn the widget.
|
|
46
|
-
if (merged.added === 0 && merged.trailing !== "added") return { steps, merged };
|
|
48
|
+
if (merged.added === 0 && merged.trailing !== "added" && merged.replaced === 0) return { steps, merged };
|
|
47
49
|
const outcome = await ctx.executeTool(TODO_TOOL, { todos: merged.todos });
|
|
48
50
|
if (!outcome.isError) return { steps, merged };
|
|
49
51
|
// The nested call fails both when the tool is unavailable and when it rejects
|
|
@@ -93,8 +93,19 @@ export function registerWriteTool(pi: ExtensionAPI, runtime: PlanRuntime): void
|
|
|
93
93
|
// write; say so once, where the user is looking.
|
|
94
94
|
const moved = runtime.plans.takeNotice();
|
|
95
95
|
if (moved) ctx.ui.notify(moved, "info");
|
|
96
|
+
// A refinement says where the text it replaced went: a reviewer's comment that was
|
|
97
|
+
// carried to a block that moved can be checked against what she actually read.
|
|
98
|
+
const kept =
|
|
99
|
+
file.replaced === undefined ? "" : `, keeping the revision it replaced at ${file.replaced.relativePath}`;
|
|
100
|
+
// A plan that was approved before is now out of step with the todo list, and
|
|
101
|
+
// the list is only re-seeded by approving it again. Say so here, where the
|
|
102
|
+
// refinement is made, instead of letting the two disagree in silence.
|
|
103
|
+
const reseed =
|
|
104
|
+
runtime.seeded(file.path) === undefined
|
|
105
|
+
? ""
|
|
106
|
+
: " This plan was approved before: its todo items are the ones that approval seeded, and approving it again replaces the steps that changed (an item you started or finished is left alone and named).";
|
|
96
107
|
return textResult(
|
|
97
|
-
`Wrote the plan to ${file.relativePath}. Call exit_plan_mode with plan_path: ${file.path}${stepElisionNote(summary.elided)}`,
|
|
108
|
+
`Wrote the plan to ${file.relativePath}${kept}. Call exit_plan_mode with plan_path: ${file.path}${stepElisionNote(summary.elided)}${reseed}`,
|
|
98
109
|
{ path: file.path, relativePath: file.relativePath, bytes: file.bytes } satisfies WritePlanDetails,
|
|
99
110
|
);
|
|
100
111
|
} catch (error) {
|
package/extensions/plan/types.ts
CHANGED
|
@@ -50,6 +50,21 @@ export interface ReviewBaselineEntry {
|
|
|
50
50
|
sections: SectionDigest[];
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* What one approval put into the todo list, so the next approval can replace it.
|
|
55
|
+
*
|
|
56
|
+
* A plan refined after it was approved is approved again, and the list still holds the
|
|
57
|
+
* items seeded the first time. Their texts are the only record of which items came from
|
|
58
|
+
* this plan, so a re-approval keeps them to recognise — and replace — its own leftovers
|
|
59
|
+
* without touching an item the user was already tracking.
|
|
60
|
+
*/
|
|
61
|
+
export interface SeededStepsEntry {
|
|
62
|
+
/** Resolved path of the approved plan file. */
|
|
63
|
+
path: string;
|
|
64
|
+
/** The step texts the approval appended, in order. */
|
|
65
|
+
steps: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
53
68
|
/** Structured result carried in `enter_plan_mode`'s `details`. */
|
|
54
69
|
export interface EnterPlanModeDetails {
|
|
55
70
|
/** Whether the user agreed to enter plan mode. */
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The review page's block navigation: which block is active, and how a key walks them.
|
|
3
|
+
*
|
|
4
|
+
* The fourth capability out of the review client's template literal. Each function takes
|
|
5
|
+
* the document it reads and returns the index it computed, so the client keeps the one
|
|
6
|
+
* variable (`activeIndex`) and these own the rules: clamp to the blocks that exist, start
|
|
7
|
+
* at the first or last when nothing is active, and land beside the reader rather than
|
|
8
|
+
* jumping to the middle of the page.
|
|
9
|
+
*
|
|
10
|
+
* `el` comes from the runtime, declared here because this source is spliced into the same
|
|
11
|
+
* bundle; the declaration emits nothing.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ClientNode } from "../../lib/web/client-runtime.ts";
|
|
15
|
+
|
|
16
|
+
/** The island's view of one block, as far as navigation needs it. */
|
|
17
|
+
export interface IslandBlock {
|
|
18
|
+
start: number;
|
|
19
|
+
kind?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The rendered blocks.
|
|
24
|
+
*
|
|
25
|
+
* A rebuild replaces them, so this is read fresh rather than remembered: the array a
|
|
26
|
+
* caller holds goes stale the moment `renderPlan` throws the DOM away.
|
|
27
|
+
*/
|
|
28
|
+
export function blockElements(planEl: ClientNode | null): ClientNode[] {
|
|
29
|
+
if (!planEl) return [];
|
|
30
|
+
const nodes = planEl.querySelectorAll(".bf-block");
|
|
31
|
+
const blocks: ClientNode[] = [];
|
|
32
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
33
|
+
const block = nodes[i];
|
|
34
|
+
if (block) blocks.push(block);
|
|
35
|
+
}
|
|
36
|
+
return blocks;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Make one block active and bring it into view, or say nothing when there are no blocks.
|
|
41
|
+
*
|
|
42
|
+
* `null` means "nothing changed": a page with no blocks has no active block, and the
|
|
43
|
+
* caller keeps the index it had rather than being reset to zero.
|
|
44
|
+
*/
|
|
45
|
+
export function setActive(planEl: ClientNode | null, index: number): number | null {
|
|
46
|
+
const blocks = blockElements(planEl);
|
|
47
|
+
if (blocks.length === 0) return null;
|
|
48
|
+
const next = Math.max(0, Math.min(blocks.length - 1, index));
|
|
49
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
50
|
+
const block = blocks[i];
|
|
51
|
+
if (block) block.classList.toggle("bf-block--active", i === next);
|
|
52
|
+
}
|
|
53
|
+
const current = blocks[next];
|
|
54
|
+
if (current && current.scrollIntoView) current.scrollIntoView({ block: "nearest" });
|
|
55
|
+
return next;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The index one step from `activeIndex`, or `null` when there is nothing to walk.
|
|
60
|
+
*
|
|
61
|
+
* With nothing active yet, `j` starts at the top and `k` at the bottom, which is what a
|
|
62
|
+
* reader pressing either key on a fresh page expects.
|
|
63
|
+
*/
|
|
64
|
+
export function moveActive(planEl: ClientNode | null, activeIndex: number, delta: number): number | null {
|
|
65
|
+
const blocks = blockElements(planEl);
|
|
66
|
+
if (blocks.length === 0) return null;
|
|
67
|
+
const next = activeIndex < 0 ? (delta < 0 ? blocks.length - 1 : 0) : activeIndex + delta;
|
|
68
|
+
return setActive(planEl, next);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The rendered block and its island block for a source line, when both exist.
|
|
73
|
+
*
|
|
74
|
+
* Both halves are required: a rendered block with no island entry has nothing to comment
|
|
75
|
+
* on, and an island entry with no rendered block is a comment the page cannot place.
|
|
76
|
+
*/
|
|
77
|
+
export function blockAtLine(
|
|
78
|
+
planEl: ClientNode | null,
|
|
79
|
+
blocks: readonly IslandBlock[],
|
|
80
|
+
line: number,
|
|
81
|
+
): { wrap: ClientNode; block: IslandBlock } | null {
|
|
82
|
+
const wrap = planEl ? planEl.querySelector('.bf-block[data-line="' + line + '"]') : null;
|
|
83
|
+
let block: IslandBlock | undefined;
|
|
84
|
+
for (const candidate of blocks) if (candidate.start === line) block = candidate;
|
|
85
|
+
return wrap && block ? { wrap, block } : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The block rules as source, for the client bundle. */
|
|
89
|
+
export const WEB_BLOCKS_JS = [blockElements, setActive, moveActive, blockAtLine].map(String).join("\n\n");
|
|
@@ -8,10 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
export const WEB_CHROME_JS = String.raw` function setStatus(text, kind) {
|
|
11
|
-
if (
|
|
12
|
-
statusEl.textContent = text || "";
|
|
13
|
-
statusEl.title = text || "";
|
|
14
|
-
statusEl.className = kind ? "bf-status bf-status--" + kind : "bf-status";
|
|
11
|
+
if (statusEl) paintStatus(statusEl, text, kind);
|
|
15
12
|
}
|
|
16
13
|
|
|
17
14
|
// The always-visible header chip: connection, comment count, and whether
|
|
@@ -56,7 +53,8 @@ export const WEB_CHROME_JS = String.raw` function setStatus(text, kind) {
|
|
|
56
53
|
titleEl.textContent = data.title;
|
|
57
54
|
titleEl.title = data.title;
|
|
58
55
|
}
|
|
59
|
-
|
|
56
|
+
// The document's own <title> already names the plan, rendered with the page: a second
|
|
57
|
+
// copy of the same sentence in script is a tab title that can disagree with the head.
|
|
60
58
|
}
|
|
61
59
|
|
|
62
60
|
function updateProgress() {
|