@gavin-hu/my-pi-agent 0.16.0 → 0.17.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 +14 -8
- package/README.zh-CN.md +14 -9
- package/extensions/bytefly/README.md +174 -101
- package/extensions/bytefly/footer/collect/entries.ts +1 -6
- package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
- package/extensions/bytefly/footer/price/price.ts +12 -8
- package/extensions/bytefly/footer/register.ts +13 -1
- package/extensions/bytefly/footer/types.ts +11 -0
- package/extensions/bytefly/footer/unpriced.ts +36 -0
- package/extensions/bytefly/header/component.ts +15 -29
- package/extensions/bytefly/header/identity.ts +71 -0
- package/extensions/bytefly/header/register.ts +9 -3
- package/extensions/bytefly/index.ts +13 -5
- package/extensions/bytefly/message/format.ts +20 -13
- package/extensions/bytefly/message/register.ts +8 -7
- package/extensions/bytefly/panel/commands.ts +46 -25
- package/extensions/bytefly/panel/format.ts +49 -14
- package/extensions/bytefly/panel/join.ts +35 -9
- package/extensions/bytefly/panel/settings.ts +10 -27
- package/extensions/bytefly/panel/skills.ts +51 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
- package/extensions/bytefly/panel/tui/frame.ts +9 -1
- package/extensions/bytefly/panel/tui/list-view.ts +72 -0
- package/extensions/bytefly/panel/tui/nav.ts +71 -3
- package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
- package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
- package/extensions/bytefly/panel/tui.ts +223 -331
- package/extensions/bytefly/panel/types.ts +26 -8
- package/extensions/bytefly/picker/discovery.ts +32 -27
- package/extensions/bytefly/picker/resources.ts +16 -8
- package/extensions/bytefly/picker/runtime.ts +62 -34
- package/extensions/bytefly/picker/toggle.ts +16 -10
- package/extensions/bytefly/picker/types.ts +31 -13
- package/extensions/bytefly/update/format.ts +66 -8
- package/extensions/bytefly/update/patch.ts +66 -0
- package/extensions/bytefly/update/register.ts +4 -11
- package/extensions/bytefly/update/release.ts +65 -0
- package/extensions/bytefly/update/state.ts +14 -0
- package/extensions/bytefly/update/suppress.ts +47 -29
- package/extensions/bytefly/update/types.ts +15 -5
- package/extensions/check/tui.ts +2 -0
- package/extensions/context/README.md +21 -11
- package/extensions/context/format.ts +10 -8
- package/extensions/context/report.ts +21 -8
- package/extensions/context/tui.ts +26 -10
- package/extensions/context/types.ts +5 -3
- package/extensions/file-browse/README.md +29 -1
- package/extensions/file-browse/client.ts +16 -463
- package/extensions/file-browse/html.ts +7 -5
- package/extensions/file-browse/palette.ts +224 -0
- package/extensions/file-browse/router.ts +2 -2
- package/extensions/file-browse/styles.ts +8 -3
- package/extensions/file-browse/tree.ts +381 -0
- package/extensions/file-read/README.md +4 -1
- package/extensions/file-read/extract/odt.ts +7 -5
- package/extensions/goal/tools.ts +6 -11
- package/extensions/job/index.ts +20 -45
- package/extensions/job/tools.ts +51 -77
- package/extensions/memory/tools.ts +4 -6
- package/extensions/memory/tui.ts +5 -1
- package/extensions/plan/README.md +88 -33
- package/extensions/plan/commands.ts +19 -4
- package/extensions/plan/config.ts +27 -0
- package/extensions/plan/plan-index.ts +3 -1
- package/extensions/plan/plans.ts +31 -13
- package/extensions/plan/review.ts +34 -14
- package/extensions/plan/runtime.ts +3 -1
- package/extensions/plan/steps.ts +43 -14
- package/extensions/plan/tools/cancel.ts +79 -0
- package/extensions/plan/tools/enter.ts +10 -10
- package/extensions/plan/tools/exit.ts +54 -43
- package/extensions/plan/tools/steps.ts +7 -19
- package/extensions/plan/tools/write.ts +22 -29
- package/extensions/plan/tools.ts +5 -2
- package/extensions/plan/tui.ts +9 -7
- package/extensions/plan/types.ts +8 -0
- package/extensions/plan/web-chrome.ts +101 -0
- package/extensions/plan/web-client.ts +98 -311
- package/extensions/plan/web-markdown.ts +222 -0
- package/extensions/plan/web-page.ts +28 -2
- package/extensions/plan/web-router.ts +2 -2
- package/extensions/rewind/checklist.ts +4 -0
- package/extensions/rewind/review-tui.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +4 -0
- package/extensions/subagent/README.md +25 -12
- package/extensions/subagent/format.ts +72 -25
- package/extensions/subagent/index.ts +21 -47
- package/extensions/subagent/orchestrate.ts +18 -41
- package/extensions/subagent/render.ts +29 -42
- package/extensions/subagent/stream.ts +24 -18
- package/extensions/subagent/types.ts +7 -0
- package/extensions/todo/tools.ts +5 -6
- package/extensions/todo/tui.ts +5 -1
- package/extensions/watch/index.ts +10 -35
- package/extensions/watch/tools.ts +31 -59
- package/extensions/wechat/README.md +40 -18
- package/extensions/wechat/bridge.ts +38 -40
- package/extensions/wechat/client.ts +1 -4
- package/extensions/wechat/commands.ts +1 -1
- package/extensions/wechat/interaction.ts +25 -4
- package/extensions/wechat/login-screen.ts +2 -3
- package/extensions/wechat/notices.ts +7 -5
- package/extensions/wechat/prompt.ts +19 -4
- package/extensions/wechat/qrcode.ts +9 -9
- package/extensions/wechat/slash.ts +14 -0
- package/extensions/wechat/status-screen.ts +2 -2
- package/extensions/wechat/tools.ts +3 -11
- package/extensions/worktree/README.md +48 -12
- package/extensions/worktree/commands.ts +109 -65
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/exit.ts +11 -1
- package/extensions/worktree/lifecycle.ts +9 -1
- package/extensions/worktree/screen.ts +263 -0
- package/extensions/worktree/status.ts +84 -38
- package/lib/README.md +7 -6
- package/lib/config.ts +12 -0
- package/lib/format.ts +6 -2
- package/lib/interaction.ts +18 -1
- package/lib/report.ts +75 -2
- package/lib/tool-result.ts +54 -4
- package/lib/tui/README.md +16 -8
- package/lib/tui/chrome.ts +27 -6
- package/lib/tui/dock-screen.ts +6 -2
- package/lib/tui/gauge.ts +9 -5
- package/lib/usage.ts +26 -0
- package/lib/web/README.md +131 -0
- package/lib/web/base.ts +19 -2
- package/lib/web/components.ts +10 -6
- package/lib/web/layout.ts +2 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +103 -25
- package/lib/web/tokens.ts +23 -7
- package/package.json +3 -1
- package/skills/changelog/SKILL.md +57 -45
- package/skills/next/SKILL.md +184 -0
package/lib/report.ts
CHANGED
|
@@ -14,11 +14,14 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Delivery is one shared transcript notice per burst, and a burst is marked
|
|
16
16
|
* reported only after the send succeeded, so a throw leaves it for the next
|
|
17
|
-
* turn.
|
|
18
|
-
*
|
|
17
|
+
* turn. `registerPendingChannel` is the other half: the seven host hooks that
|
|
18
|
+
* drive the machine, registered once for `job` and `watch` rather than copied
|
|
19
|
+
* into each factory. Value-only: every flag lives in the returned closure, never
|
|
20
|
+
* in a module variable, because each extension gets its own module instance.
|
|
19
21
|
*/
|
|
20
22
|
|
|
21
23
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
24
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
22
25
|
import { pruneStaleNotices, type NoticeMessage } from "./notice.ts";
|
|
23
26
|
|
|
24
27
|
export interface PendingReporterOptions<T> {
|
|
@@ -97,3 +100,73 @@ export function createPendingReporter<T>(options: PendingReporterOptions<T>): Pe
|
|
|
97
100
|
},
|
|
98
101
|
};
|
|
99
102
|
}
|
|
103
|
+
|
|
104
|
+
/** The session-scoped state a channel drives, narrowed to what its hooks need. */
|
|
105
|
+
export interface PendingChannelRuntime {
|
|
106
|
+
/** Load this session's records; called on every session start. */
|
|
107
|
+
load(ctx: ExtensionContext): void;
|
|
108
|
+
/** Repaint the status chip. */
|
|
109
|
+
setStatus(ctx: ExtensionContext): void;
|
|
110
|
+
/** Release the session's resources; idempotent. */
|
|
111
|
+
shutdown(): Promise<void>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface PendingChannelOptions {
|
|
115
|
+
/** The reporter that turns pending outcomes into notices. */
|
|
116
|
+
reporter: PendingReporter;
|
|
117
|
+
/** The runtime whose pending outcomes the notices describe. */
|
|
118
|
+
runtime: PendingChannelRuntime;
|
|
119
|
+
/** Runs on every session start, after the runtime loads: arm the report trigger. */
|
|
120
|
+
onSessionStart?(ctx: ExtensionContext): void;
|
|
121
|
+
/** Runs before the runtime releases: clear what `onSessionStart` armed. */
|
|
122
|
+
onSessionShutdown?(): void;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Wire a reporter to the host's session lifecycle.
|
|
127
|
+
*
|
|
128
|
+
* The order inside `session_start` is the contract: the reporter forgets the
|
|
129
|
+
* previous session's busy window, the runtime loads the records, and only then
|
|
130
|
+
* is the report trigger armed — a reload or a replaced session reuses the same
|
|
131
|
+
* runtime. A shutdown clears the trigger before the runtime releases the table it
|
|
132
|
+
* was calling into.
|
|
133
|
+
*/
|
|
134
|
+
export function registerPendingChannel(
|
|
135
|
+
pi: Pick<ExtensionAPI, "on">,
|
|
136
|
+
{ reporter, runtime, onSessionStart, onSessionShutdown }: PendingChannelOptions,
|
|
137
|
+
): void {
|
|
138
|
+
pi.on("session_start", (_event, ctx) => {
|
|
139
|
+
reporter.sessionStart();
|
|
140
|
+
runtime.load(ctx);
|
|
141
|
+
onSessionStart?.(ctx);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Tree navigation does not change the pending records; only repaint.
|
|
145
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
146
|
+
runtime.setStatus(ctx);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
pi.on("agent_start", () => {
|
|
150
|
+
reporter.agentStart();
|
|
151
|
+
});
|
|
152
|
+
pi.on("agent_settled", () => {
|
|
153
|
+
reporter.agentSettled();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Report what is queued at the start of the next turn, without triggering one.
|
|
157
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
158
|
+
const reported = reporter.beforeAgentStart();
|
|
159
|
+
if (!reported) return undefined;
|
|
160
|
+
runtime.setStatus(ctx);
|
|
161
|
+
return reported;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Keep stale notices out of later turns. Only this source's notices are
|
|
165
|
+
// pruned; another source's are left for its own handler.
|
|
166
|
+
pi.on("context", (event) => reporter.filterContext(event.messages));
|
|
167
|
+
|
|
168
|
+
pi.on("session_shutdown", async () => {
|
|
169
|
+
onSessionShutdown?.();
|
|
170
|
+
await runtime.shutdown();
|
|
171
|
+
});
|
|
172
|
+
}
|
package/lib/tool-result.ts
CHANGED
|
@@ -1,17 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The shared shape of a
|
|
2
|
+
* The shared shape of a tool result.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Every extension tool returns the same envelope: one text block the model
|
|
5
|
+
* reads, the extension's own `details` for the renderer and the session branch,
|
|
6
|
+
* and — where the tool declares an `outputSchema` — the JSON mirror Pi calls
|
|
7
|
+
* `structuredContent`. A rejected call is that envelope marked `isError`, still
|
|
8
|
+
* carrying the previous state so the model can retry.
|
|
9
|
+
*
|
|
10
|
+
* `textResult` and `failedResult` build the two, so the flag and the absent
|
|
11
|
+
* `structuredContent` are decided once. What stays with the extension is the
|
|
12
|
+
* text and the mirror: `structuredContent` drops `undefined` field by field and
|
|
13
|
+
* projects nested objects, so it is written per `details` type rather than
|
|
14
|
+
* derived. `toolError` / `errorResult` keep the shorthand the
|
|
15
|
+
* action-and-message tools (`job` and `watch`) share.
|
|
7
16
|
*/
|
|
8
17
|
|
|
18
|
+
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
19
|
+
|
|
9
20
|
/** One text block in a tool result. */
|
|
10
21
|
export interface ToolErrorContent {
|
|
11
22
|
type: "text";
|
|
12
23
|
text: string;
|
|
13
24
|
}
|
|
14
25
|
|
|
26
|
+
/** A tool result: what the model reads, the extension's details, and its JSON mirror. */
|
|
27
|
+
export interface ToolResult<Details> {
|
|
28
|
+
content: ToolErrorContent[];
|
|
29
|
+
details: Details;
|
|
30
|
+
structuredContent?: Record<string, JsonValue>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A rejected call: the result, marked so the host reports it as an error. */
|
|
34
|
+
export interface ToolFailure<Details> extends ToolResult<Details> {
|
|
35
|
+
isError: true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The content and details both results share. */
|
|
39
|
+
function envelope<Details>(text: string, details: Details): ToolResult<Details> {
|
|
40
|
+
return { content: [{ type: "text", text }], details };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A successful call. */
|
|
44
|
+
export function textResult<Details>(
|
|
45
|
+
text: string,
|
|
46
|
+
details: Details,
|
|
47
|
+
structuredContent?: Record<string, JsonValue>,
|
|
48
|
+
): ToolResult<Details> {
|
|
49
|
+
const result = envelope(text, details);
|
|
50
|
+
return structuredContent === undefined ? result : { ...result, structuredContent };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A rejected call, keeping the state the caller had before it. */
|
|
54
|
+
export function failedResult<Details>(
|
|
55
|
+
text: string,
|
|
56
|
+
details: Details,
|
|
57
|
+
structuredContent?: Record<string, JsonValue>,
|
|
58
|
+
): ToolFailure<Details> {
|
|
59
|
+
const result = envelope(text, details);
|
|
60
|
+
return structuredContent === undefined
|
|
61
|
+
? { ...result, isError: true }
|
|
62
|
+
: { ...result, structuredContent, isError: true };
|
|
63
|
+
}
|
|
64
|
+
|
|
15
65
|
/** A rejected tool call: the message as content, the extension's details, and the error flag. */
|
|
16
66
|
export function toolError<Details extends { action: string; error: string }>(
|
|
17
67
|
message: string,
|
package/lib/tui/README.md
CHANGED
|
@@ -35,17 +35,24 @@ Reach for a `lib/tui/` primitive before composing your own: `plainRule` /
|
|
|
35
35
|
`fitCells` for two columns, `LinePane` for a scrollable log, `ListCursor` /
|
|
36
36
|
`IdCursor` for selection, `byRunningFirst` for a live list's order,
|
|
37
37
|
`DockListScreen` for a list that opens a pane, `TailResult` for a tool result
|
|
38
|
-
that lists rows or tails output,
|
|
38
|
+
that lists rows or tails output, `screenFooter` for a screen's own last three
|
|
39
|
+
rows, and `ConfirmState` before a destructive action.
|
|
39
40
|
|
|
40
41
|
## Docked list screens
|
|
41
42
|
|
|
42
43
|
Every list screen is `dockListFrame`: the header rule, an optional summary and
|
|
43
|
-
its blank line, the body, an optional range row, an optional detail block,
|
|
44
|
-
the
|
|
45
|
-
it — including the blank line above the detail block, which
|
|
46
|
-
from reading as one more row. A screen that sizes its body
|
|
47
|
-
counts the block with `detailBlockRows`, or the hint row is
|
|
48
|
-
bottom of a short terminal.
|
|
44
|
+
its blank line, the body, an optional range row, an optional detail block, the
|
|
45
|
+
key-hint row, and the closing rule. The frame owns that spacing, so a screen
|
|
46
|
+
never hand-rolls it — including the blank line above the detail block, which
|
|
47
|
+
keeps a footnote from reading as one more row. A screen that sizes its body
|
|
48
|
+
through `fitRows` counts the block with `detailBlockRows`, or the hint row is
|
|
49
|
+
pushed off the bottom of a short terminal.
|
|
50
|
+
|
|
51
|
+
The closing rule is not decoration: Pi's built-in selectors draw one under the
|
|
52
|
+
list and over the footer, and the editor a docked screen replaces draws its own,
|
|
53
|
+
so a screen that ends on a blank removes the dock's bottom edge. A screen that
|
|
54
|
+
lays out its own rows closes its block with `screenFooter(theme, width, hint)`;
|
|
55
|
+
a screen whose row above the hint varies places `plainRule` itself.
|
|
49
56
|
|
|
50
57
|
A list that **opens a pane over one row** — `/jobs`, `/watch`, `/agents`,
|
|
51
58
|
`/check` — subclasses [`dock-screen.ts`](./dock-screen.ts) instead of
|
|
@@ -92,7 +99,8 @@ Resolve colors through a [`tokens.ts`](./tokens.ts) role
|
|
|
92
99
|
(`paint(theme, role, text)`), never a literal ANSI sequence, hex value, or Pi
|
|
93
100
|
token named inline; a theme swap must re-skin every surface. Use the shared
|
|
94
101
|
`selectionMarker`, `GLYPHS`, `SEPARATORS`, and `STATUS_KEYS` instead of retyping
|
|
95
|
-
their literals,
|
|
102
|
+
their literals, `screenHint` / Pi's `keyHint` for the hint row, and
|
|
103
|
+
`screenFooter` for the hint row with its blank and closing rule.
|
|
96
104
|
|
|
97
105
|
A list accents the focused row's label (`labelRole: selected ? "accent" :
|
|
98
106
|
"label"`); the `❯` marker alone is not enough to tell which row is live. The
|
package/lib/tui/chrome.ts
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* the transcript visible.
|
|
9
9
|
*
|
|
10
10
|
* Screens are full-width, terminal-height-aware components. These helpers build
|
|
11
|
-
* the top rule, clamp a screen's body to the terminal, and
|
|
12
|
-
* footer, so each screen keeps its own layout but not its
|
|
13
|
-
* arithmetic or hint vocabulary.
|
|
11
|
+
* the top rule and the closing rule, clamp a screen's body to the terminal, and
|
|
12
|
+
* render the key-hint footer, so each screen keeps its own layout but not its
|
|
13
|
+
* own copy of the arithmetic or hint vocabulary.
|
|
14
14
|
*
|
|
15
15
|
* No runtime dependencies beyond `@earendil-works/pi-tui`: the theme is passed
|
|
16
16
|
* in, and the viewport options are explicit.
|
|
@@ -20,6 +20,7 @@ import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
|
20
20
|
import { Text, truncateToWidth, visibleWidth, type EditorTheme, type OverlayOptions } from "@earendil-works/pi-tui";
|
|
21
21
|
import { SEPARATORS } from "./glyphs.ts";
|
|
22
22
|
import { BODY_INDENT } from "./indent.ts";
|
|
23
|
+
import { plainRule } from "./rules.ts";
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* `ctx.ui.custom` options that give a screen the whole terminal.
|
|
@@ -88,12 +89,29 @@ export function screenHint(theme: Theme, width: number, hints: ScreenHint[], ind
|
|
|
88
89
|
return truncateToWidth(`${lead}${kept.map(render).join(separator)}`, target);
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
/**
|
|
93
|
+
* The bottom of a screen: a blank row, its key-hint row, and the closing rule.
|
|
94
|
+
*
|
|
95
|
+
* {@link dockListFrame} ends a docked list screen with this, and a screen that
|
|
96
|
+
* lays out its own rows — `/context`, the WeChat login and control screens —
|
|
97
|
+
* closes its block with it too, so no screen spells the three rows out alone. A
|
|
98
|
+
* screen whose row above the hint varies (a scroll gauge or a blank) places its
|
|
99
|
+
* own rule with `plainRule` instead.
|
|
100
|
+
*/
|
|
101
|
+
export function screenFooter(theme: Theme, width: number, hint: string): string[] {
|
|
102
|
+
return ["", hint, plainRule(theme, width)];
|
|
103
|
+
}
|
|
104
|
+
|
|
91
105
|
/**
|
|
92
106
|
* The sections of a docked list screen, assembled by {@link dockListFrame}.
|
|
93
107
|
* `body`, `range`, and `detail` lines are built (and truncated) by the caller;
|
|
94
108
|
* the frame only places them.
|
|
95
109
|
*/
|
|
96
110
|
export interface DockListFrame {
|
|
111
|
+
/** The active theme, used for the closing rule. */
|
|
112
|
+
theme: Theme;
|
|
113
|
+
/** The screen width in columns, used for the closing rule. */
|
|
114
|
+
width: number;
|
|
97
115
|
/** The pre-rendered header line, usually `titleRule(theme, width, title)`. */
|
|
98
116
|
header: string;
|
|
99
117
|
/** A summary line under the header; a blank line follows it. */
|
|
@@ -132,11 +150,14 @@ export function detailBlockRows(detail: readonly string[] | undefined, blank = t
|
|
|
132
150
|
/**
|
|
133
151
|
* Assemble a docked list screen: the caller's header rule, an optional summary
|
|
134
152
|
* and its blank line, the body, an optional range row, optional detail rows after
|
|
135
|
-
* their own blank line,
|
|
153
|
+
* their own blank line, the key-hint row, and the closing rule.
|
|
136
154
|
*
|
|
137
155
|
* This is the convention every docked list screen shares — the bytefly panel,
|
|
138
156
|
* `/jobs`, `/watch`, `/todos`, `/memory`, and `/rewind`. The header line is built
|
|
139
|
-
* by the caller (usually `titleRule`), so the frame never picks
|
|
157
|
+
* by the caller (usually `titleRule`), so the frame never picks the *opening*
|
|
158
|
+
* rule style; the closing rule is always the muted fill, matching the closing
|
|
159
|
+
* rule Pi's built-in selectors draw and the bottom border of the editor slot the
|
|
160
|
+
* screen replaces.
|
|
140
161
|
*/
|
|
141
162
|
export function dockListFrame(frame: DockListFrame): string[] {
|
|
142
163
|
const lines: string[] = [frame.header];
|
|
@@ -147,7 +168,7 @@ export function dockListFrame(frame: DockListFrame): string[] {
|
|
|
147
168
|
if (frame.detailBlank !== false) lines.push("");
|
|
148
169
|
lines.push(...frame.detail);
|
|
149
170
|
}
|
|
150
|
-
lines.push(
|
|
171
|
+
lines.push(...screenFooter(frame.theme, frame.width, frame.hint));
|
|
151
172
|
return lines;
|
|
152
173
|
}
|
|
153
174
|
|
package/lib/tui/dock-screen.ts
CHANGED
|
@@ -33,10 +33,10 @@ import { paint } from "./tokens.ts";
|
|
|
33
33
|
/** Rows the body shows when the terminal height is unknown. */
|
|
34
34
|
const SCREEN_DEFAULT_ROWS = 14;
|
|
35
35
|
|
|
36
|
-
/** Fixed rows around the list body: header, summary, blank, blank, hint,
|
|
36
|
+
/** Fixed rows around the list body: header, summary, blank, blank, hint, closing rule. */
|
|
37
37
|
const LIST_CHROME_ROWS = 6;
|
|
38
38
|
|
|
39
|
-
/** Fixed rows around a detail body: header, subtitle, blank, blank, hint,
|
|
39
|
+
/** Fixed rows around a detail body: header, subtitle, blank, blank, hint, closing rule. */
|
|
40
40
|
const DETAIL_CHROME_ROWS = 6;
|
|
41
41
|
|
|
42
42
|
/** What a detail pane shows: its title, lines, and the one-line subtitle. */
|
|
@@ -353,6 +353,8 @@ export abstract class DockListScreen<T> implements Component {
|
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
return dockListFrame({
|
|
356
|
+
theme: this.theme,
|
|
357
|
+
width: w,
|
|
356
358
|
header: titleRule(this.theme, w, this.options.title),
|
|
357
359
|
summary: this.summary(rows, w),
|
|
358
360
|
body,
|
|
@@ -389,6 +391,8 @@ export abstract class DockListScreen<T> implements Component {
|
|
|
389
391
|
}
|
|
390
392
|
|
|
391
393
|
return dockListFrame({
|
|
394
|
+
theme: this.theme,
|
|
395
|
+
width: w,
|
|
392
396
|
header: titleRule(this.theme, w, this.pane.title(this.options.detailTitle)),
|
|
393
397
|
summary: clip(` ${paint(this.theme, "faint", this.pane.subtitleLine())}`, w),
|
|
394
398
|
body,
|
package/lib/tui/gauge.ts
CHANGED
|
@@ -26,16 +26,20 @@ export const DEFAULT_GAUGE_THRESHOLDS: GaugeThresholds = { warn: 70, danger: 90
|
|
|
26
26
|
/**
|
|
27
27
|
* Filled and empty block counts for a percentage across `blocks` cells.
|
|
28
28
|
*
|
|
29
|
-
* The fill
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
29
|
+
* The fill rounds to the nearest cell, so a bar sits no more than half a cell
|
|
30
|
+
* away from the number printed beside it, and the last cell is kept for 100% so
|
|
31
|
+
* a bar reads full only at a full value. Any non-zero usage still forces one
|
|
32
|
+
* block, so a small percentage stays visible. A `null` percent (unknown) fills
|
|
33
|
+
* nothing, leaving the caller to decide whether to draw the empty track; a known
|
|
34
|
+
* `0` draws it.
|
|
33
35
|
*/
|
|
34
36
|
export function computeGauge(percent: number | null, blocks: number): { filled: number; empty: number } {
|
|
35
37
|
if (blocks <= 0) return { filled: 0, empty: 0 };
|
|
36
38
|
if (percent === null) return { filled: 0, empty: blocks };
|
|
37
39
|
const clamped = Math.max(0, Math.min(100, percent));
|
|
38
|
-
const
|
|
40
|
+
const nearest = Math.round((clamped / 100) * blocks);
|
|
41
|
+
const capped = clamped < 100 ? Math.min(blocks - 1, nearest) : nearest;
|
|
42
|
+
const filled = Math.max(clamped > 0 ? 1 : 0, capped);
|
|
39
43
|
return { filled, empty: blocks - filled };
|
|
40
44
|
}
|
|
41
45
|
|
package/lib/usage.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The usage counters this package accounts in.
|
|
3
|
+
*
|
|
4
|
+
* A session, a delegated child run, and a priced record are all measured in the
|
|
5
|
+
* same five numbers, so the list lives here rather than in each caller:
|
|
6
|
+
* bytefly's footer accumulates them for the session and for its `↳` roll-up, and
|
|
7
|
+
* `subagent` accumulates them from a child's JSON events.
|
|
8
|
+
*
|
|
9
|
+
* `turns` and the context size are deliberately not counters — one is counted
|
|
10
|
+
* per message, the other is a window occupancy rather than something to sum — so
|
|
11
|
+
* each owner tracks those beside these.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** The counters, in the order a usage line prints them. */
|
|
15
|
+
export const USAGE_COUNTERS = ["input", "output", "cacheRead", "cacheWrite", "cost"] as const;
|
|
16
|
+
|
|
17
|
+
/** One counter in a usage accumulator. */
|
|
18
|
+
export type UsageCounter = (typeof USAGE_COUNTERS)[number];
|
|
19
|
+
|
|
20
|
+
/** A usage accumulator: one running total per counter. */
|
|
21
|
+
export type UsageCounts = Record<UsageCounter, number>;
|
|
22
|
+
|
|
23
|
+
/** A zeroed accumulator, ready to sum into. */
|
|
24
|
+
export function zeroUsage(): UsageCounts {
|
|
25
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
26
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# lib/web — the shared browser design system
|
|
2
|
+
|
|
3
|
+
The tokens, base rules, app shell, `bf-*` components, HTML atoms, and client
|
|
4
|
+
runtime the two loopback pages draw with: the plan review
|
|
5
|
+
([`../../extensions/plan/web-page.ts`](../../extensions/plan/web-page.ts)) and
|
|
6
|
+
file browse ([`../../extensions/file-browse/styles.ts`](../../extensions/file-browse/styles.ts)).
|
|
7
|
+
The module-by-module inventory is the `web/` row of [`../README.md`](../README.md);
|
|
8
|
+
this file is the conventions those modules exist to make easy.
|
|
9
|
+
|
|
10
|
+
Both pages serve a same-origin document through the shared HTTP layer
|
|
11
|
+
([`../http/index.ts`](../http/index.ts)) under one CSP. The kit is the one policy
|
|
12
|
+
for how that document looks and moves — a second palette, focus ring, or dialog
|
|
13
|
+
is the bug this directory prevents.
|
|
14
|
+
|
|
15
|
+
## Compose the layers in order
|
|
16
|
+
|
|
17
|
+
A page concatenates `WEB_BASE_CSS`, `WEB_THEME_CSS`, `WEB_LAYOUT_CSS`,
|
|
18
|
+
`WEB_COMPONENTS_CSS`, then its own page rules. Each block appears **once**: a
|
|
19
|
+
repeated block still renders, so only a count catches it, and every page suite
|
|
20
|
+
counts its own stylesheet (`pages.test.ts`, `web.test.ts`). A page rule is
|
|
21
|
+
namespaced (`review-*`, `browse-*`) and overrides a kit class rather than
|
|
22
|
+
re-declaring it.
|
|
23
|
+
|
|
24
|
+
## Color is a role
|
|
25
|
+
|
|
26
|
+
`WEB_THEME_TOKENS` is derived from `themes/bytefly-dark.json` and
|
|
27
|
+
`themes/bytefly-light.json`, so the browser and the TUI cannot drift. Four values
|
|
28
|
+
are computed rather than mapped:
|
|
29
|
+
|
|
30
|
+
- `--accent-soft` — the accent at 15% (dark) or 10% (light), for a selection or
|
|
31
|
+
the active nav row.
|
|
32
|
+
- `--on-accent` — the appearance's canvas. A label on the accent fill is dark
|
|
33
|
+
text on the light-blue dark accent and white on the darker light one.
|
|
34
|
+
- `--scrim` and `--shadow` — per appearance, because a dimming layer and a lifted
|
|
35
|
+
panel need their own alpha rather than a palette entry.
|
|
36
|
+
|
|
37
|
+
Components read tokens only: no literal hex or `rgba()` in `components.ts`, and a
|
|
38
|
+
test fails one. Contrast is asserted, not assumed — text, muted, and accent at
|
|
39
|
+
4.5:1 on both surfaces, the primary label at 4.5:1 on the accent fill, `--dim` at
|
|
40
|
+
3:1 because it carries the line-number gutter, and the status colours at 3:1.
|
|
41
|
+
|
|
42
|
+
The theme layer emits the dark palette at `:root`, the explicit light override,
|
|
43
|
+
the system-light fallback, the `tok-*` syntax rules, and **last** a print block
|
|
44
|
+
that resets to the light palette: paper is white whatever the screen shows.
|
|
45
|
+
|
|
46
|
+
## The shell
|
|
47
|
+
|
|
48
|
+
`.bf-app` is a definite-height column — `100vh`, then `100dvh` — so the header,
|
|
49
|
+
sidebar, and main pane scroll inside it instead of the document. `.bf-app__body`
|
|
50
|
+
is a 260px sidebar plus main; at `SIDEBAR_COLLAPSE_MAX` (860) the sidebar becomes
|
|
51
|
+
a `<details>` disclosure and the page returns to normal scrolling. That is the
|
|
52
|
+
only breakpoint the shell owns; a page's own (`820`, `1180`) stays in its
|
|
53
|
+
stylesheet, since a media query cannot read a custom property.
|
|
54
|
+
|
|
55
|
+
A `<details>` lays its content out in a wrapper with no definite height, so a
|
|
56
|
+
sidebar that must scroll on its own box positions its panel absolutely rather
|
|
57
|
+
than making the panel a flex child. `file-browse` does this; the rule and its
|
|
58
|
+
reason are in `layout.ts`.
|
|
59
|
+
|
|
60
|
+
## The `bf-*` kit
|
|
61
|
+
|
|
62
|
+
`bf-` marks the kit: brand and mark, toolbar, title, buttons (`--primary`,
|
|
63
|
+
`--ghost`, `--icon`), chips and their tones, segmented control, field and input,
|
|
64
|
+
textarea, panel, progress, status, note, empty state, dialog (head, body, actions,
|
|
65
|
+
status, input, list, option), shortcut sheet and `bf-kbd`, tree, crumbs, table and
|
|
66
|
+
sort, card, code, media, Markdown body, comment, composer, and quote chip.
|
|
67
|
+
|
|
68
|
+
States are attributes, not classes, wherever a control has them:
|
|
69
|
+
`aria-pressed`, `aria-current`, `aria-sort`, `aria-expanded`, `hidden`. The base
|
|
70
|
+
layer's `[hidden]{display:none!important}` is what lets a `display:flex`
|
|
71
|
+
component disappear — do not add a page rule to hide it again.
|
|
72
|
+
|
|
73
|
+
## Element and script contracts
|
|
74
|
+
|
|
75
|
+
These names are one agreement between a page, its client, and tests:
|
|
76
|
+
|
|
77
|
+
| Contract | Owner |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `#theme-toggle` with a `.theme-glyph` | `themeToggle()` (`markup.ts`) |
|
|
80
|
+
| `THEME_JS_PATH` served by both routers | `runtime.ts` |
|
|
81
|
+
| `pi-web-theme` in `localStorage` | `WEB_THEME_STORAGE_KEY` |
|
|
82
|
+
| `#keys-dialog` (hidden, `aria-modal`) and `#keys-close` | `keysSheet` (`markup.ts`) |
|
|
83
|
+
| the page's own sheet trigger id | `keysDialogScript(triggerId)` |
|
|
84
|
+
| `window.bfKeys` — `open` / `toggle` / `close` / `isOpen` | `dialogScript` |
|
|
85
|
+
|
|
86
|
+
`dialogScript(options)` is the one modal implementation: it opens and closes,
|
|
87
|
+
closes on a backdrop click, traps `Tab` inside the panel, closes on `Esc` in the
|
|
88
|
+
capture phase (so a page's keymap cannot peel a different layer first), restores
|
|
89
|
+
focus to whatever opened it, and exposes `window[globalName]`. It renders nothing
|
|
90
|
+
when the element is absent, so a page can embed it unconditionally. A page adds
|
|
91
|
+
only its own confirm button's listener.
|
|
92
|
+
|
|
93
|
+
`THEME_JS` is the pre-paint bootstrap and lives in the head; `THEME_TOGGLE_JS`
|
|
94
|
+
paints the glyph, follows the system until the reader chooses, and then keeps the
|
|
95
|
+
choice. Neither page reloads for a theme.
|
|
96
|
+
|
|
97
|
+
## Markup atoms and escaping
|
|
98
|
+
|
|
99
|
+
`docHead`, `brandLockup`, `themeToggle`, `chip`, `button`, and `keysSheet` return
|
|
100
|
+
strings. Every dynamic **value** goes through `escapeHtml` from
|
|
101
|
+
[`../escape.ts`](../escape.ts); the fields documented as trusted (`css`, an
|
|
102
|
+
`icon` glyph, an attribute key, a client id) are code-authored constants, never
|
|
103
|
+
model or file text. A page that renders its own document follows the same rule.
|
|
104
|
+
|
|
105
|
+
The kit is HTML and CSS only; plan text stays out of the page markup and arrives
|
|
106
|
+
through the review's JSON island, so the client writes `textContent` and never
|
|
107
|
+
markup.
|
|
108
|
+
|
|
109
|
+
## Print and forced colours
|
|
110
|
+
|
|
111
|
+
`base.ts` owns both medias. Print un-constrains the shell and drops the toolbar,
|
|
112
|
+
sidebar, footer, progress bar, skip link, and dialogs; a page adds what its own
|
|
113
|
+
box clips (a card, a code pane, a sticky hunk header). Under
|
|
114
|
+
`forced-colors:active` the hand-drawn task checkbox names system colours and the
|
|
115
|
+
focus ring stays `CanvasText`, because Windows High Contrast replaces colour but
|
|
116
|
+
not a drawn control.
|
|
117
|
+
|
|
118
|
+
## Keyboard
|
|
119
|
+
|
|
120
|
+
Every interactive element gets the one focus ring from `base.ts`; a component
|
|
121
|
+
must not declare its own, or the ring disappears surface by surface. A dialog
|
|
122
|
+
traps `Tab`. Within a page, `j`/`k`, arrows, and `?` follow the TUI's vocabulary
|
|
123
|
+
where they mean the same thing; the browse tree is one tab stop with arrows, and
|
|
124
|
+
`Enter` follows the focused link.
|
|
125
|
+
|
|
126
|
+
## Tests
|
|
127
|
+
|
|
128
|
+
[`web.test.ts`](./web.test.ts) pins the token anchors, the contrast ratios, each
|
|
129
|
+
emitted block, the atoms' escaping, and every contract above. A page's suite adds
|
|
130
|
+
its own: the stylesheet is shipped once, and the ids its client greps for exist in
|
|
131
|
+
the markup.
|
package/lib/web/base.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Base rules shared by the Bytefly browser surfaces: box sizing, hidden,
|
|
3
|
-
* links, the focus ring,
|
|
3
|
+
* links, the focus ring, the skip-link / screen-reader helpers, and the two
|
|
4
|
+
* media the kit answers for every page — print and forced colours.
|
|
4
5
|
*
|
|
5
6
|
* Page layout (body display, shells) stays with each page; this layer is the
|
|
6
7
|
* smallest set both pages must agree on.
|
|
@@ -16,4 +17,20 @@ a:hover{text-decoration:underline}
|
|
|
16
17
|
a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:var(--focus-ring) solid var(--accent);outline-offset:2px}
|
|
17
18
|
.skip{position:absolute;left:-9999px;top:0;z-index:3;padding:var(--space-2) var(--space-4);background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:var(--radius-md)}
|
|
18
19
|
.skip:focus{left:var(--space-3);top:var(--space-3)}
|
|
19
|
-
.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
|
|
20
|
+
.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
|
|
21
|
+
/* A printed page is not a viewport: the shell un-constrains, its chrome goes,
|
|
22
|
+
and the panes stop scrolling so the document prints whole. */
|
|
23
|
+
@media print{
|
|
24
|
+
.bf-app,.bf-app__body,.bf-app__main,.bf-app__sidebar,.bf-panel{display:block!important;height:auto!important;min-height:0!important;max-height:none!important;overflow:visible!important}
|
|
25
|
+
.bf-toolbar,.bf-app__sidebar,.bf-app__footer,.bf-progress,.skip,.bf-dialog{display:none!important}
|
|
26
|
+
.bf-app__header{position:static!important;border:0!important}
|
|
27
|
+
.bf-app__main{padding:0!important}
|
|
28
|
+
}
|
|
29
|
+
/* Windows High Contrast replaces colour but not a drawn control, so the one
|
|
30
|
+
hand-drawn box names a system colour and the ring stays visible. */
|
|
31
|
+
@media (forced-colors:active){
|
|
32
|
+
a{color:LinkText}
|
|
33
|
+
:focus-visible{outline-color:CanvasText}
|
|
34
|
+
.bf-md .md-check{border-color:CanvasText}
|
|
35
|
+
.bf-md .md-check:checked{forced-color-adjust:none;background:Highlight;border-color:Highlight}
|
|
36
|
+
}`;
|
package/lib/web/components.ts
CHANGED
|
@@ -17,14 +17,13 @@ export const WEB_COMPONENTS_CSS = `.bf-brand{display:inline-flex;align-items:cen
|
|
|
17
17
|
.bf-btn{appearance:none;border:1px solid var(--border);border-radius:var(--radius-md);background:transparent;color:var(--text);font:var(--fs-sm) var(--font-ui);padding:5px var(--space-3);cursor:pointer}
|
|
18
18
|
.bf-btn:hover{border-color:var(--accent);color:var(--accent);text-decoration:none}
|
|
19
19
|
.bf-btn[aria-pressed="true"]{border-color:var(--accent);color:var(--accent);background:var(--accent-soft)}
|
|
20
|
-
.bf-btn--primary{background:var(--accent);border-color:var(--accent);color
|
|
21
|
-
.bf-btn--primary:hover{filter:brightness(1.06);color
|
|
20
|
+
.bf-btn--primary{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}
|
|
21
|
+
.bf-btn--primary:hover{filter:brightness(1.06);color:var(--on-accent)}
|
|
22
22
|
.bf-btn--ghost{border-color:transparent;background:none;padding:0;font:inherit;color:inherit}
|
|
23
23
|
.bf-btn--ghost:hover{color:var(--accent)}
|
|
24
24
|
.bf-btn--icon{min-width:36px;text-align:center;line-height:1}
|
|
25
|
+
.theme-glyph{font-size:var(--fs-md)}
|
|
25
26
|
.bf-btn:disabled{opacity:.5;cursor:default}
|
|
26
|
-
.bf-theme-toggle{min-width:36px;text-align:center;line-height:1}
|
|
27
|
-
.bf-theme-toggle .theme-glyph{font-size:var(--fs-md)}
|
|
28
27
|
.bf-chip{display:inline-flex;align-items:center;gap:5px;font-size:var(--fs-xs);color:var(--muted);white-space:nowrap}
|
|
29
28
|
.bf-chip--accent{color:var(--accent);border:1px solid currentColor;border-radius:var(--radius-pill);padding:2px 10px}
|
|
30
29
|
.bf-chip--success{color:var(--success)}
|
|
@@ -51,8 +50,8 @@ export const WEB_COMPONENTS_CSS = `.bf-brand{display:inline-flex;align-items:cen
|
|
|
51
50
|
.bf-empty__title{font-size:var(--fs-base);margin:0 0 6px}
|
|
52
51
|
.bf-empty__sub{color:var(--muted);font-size:var(--fs-sm);margin:0 0 var(--space-4)}
|
|
53
52
|
.bf-empty__code{font:var(--fw-bold) 64px/1 var(--font-mono);color:var(--danger)}
|
|
54
|
-
.bf-dialog{position:fixed;inset:0;z-index:5;display:flex;align-items:flex-start;justify-content:center;padding:10vh var(--space-4) var(--space-4);background:
|
|
55
|
-
.bf-dialog__panel{width:min(640px,100%);max-height:70vh;display:flex;flex-direction:column;overflow:hidden;background:var(--surface);border:1px solid var(--border-strong);border-radius:var(--radius-lg);box-shadow:
|
|
53
|
+
.bf-dialog{position:fixed;inset:0;z-index:5;display:flex;align-items:flex-start;justify-content:center;padding:10vh var(--space-4) var(--space-4);background:var(--scrim)}
|
|
54
|
+
.bf-dialog__panel{width:min(640px,100%);max-height:70vh;display:flex;flex-direction:column;overflow:hidden;background:var(--surface);border:1px solid var(--border-strong);border-radius:var(--radius-lg);box-shadow:var(--shadow)}
|
|
56
55
|
.bf-dialog__status{margin:0;padding:var(--space-2) var(--space-4);border-top:1px solid var(--border);color:var(--muted);font-size:var(--fs-xs)}
|
|
57
56
|
.bf-dialog__input{appearance:none;border:0;border-bottom:1px solid var(--border);background:transparent;color:var(--text);font:var(--fs-md) var(--font-mono);padding:var(--space-3) var(--space-4);outline:none}
|
|
58
57
|
.bf-dialog__list{list-style:none;margin:0;padding:var(--space-1) 0;overflow:auto;min-height:0}
|
|
@@ -61,6 +60,11 @@ export const WEB_COMPONENTS_CSS = `.bf-brand{display:inline-flex;align-items:cen
|
|
|
61
60
|
.bf-dialog mark{background:transparent;color:var(--warn);font-weight:var(--fw-semibold)}
|
|
62
61
|
.bf-dialog__head{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--border)}
|
|
63
62
|
.bf-dialog__head h2{margin:0;font-size:var(--fs-lg)}
|
|
63
|
+
.bf-dialog__body{padding:var(--space-3) var(--space-4);font-size:var(--fs-sm);line-height:var(--lh-relaxed)}
|
|
64
|
+
.bf-dialog__body p{margin:0 0 var(--space-2)}
|
|
65
|
+
.bf-dialog__body p:last-child{margin:0}
|
|
66
|
+
.bf-dialog__hint{color:var(--muted)}
|
|
67
|
+
.bf-dialog__actions{display:flex;justify-content:flex-end;gap:var(--space-2);padding:var(--space-3) var(--space-4);border-top:1px solid var(--border)}
|
|
64
68
|
.bf-keys{display:grid;grid-template-columns:auto 1fr;gap:8px var(--space-4);margin:0;padding:var(--space-4)}
|
|
65
69
|
.bf-keys dt{white-space:nowrap}
|
|
66
70
|
.bf-keys dd{margin:0;color:var(--muted);font-size:var(--fs-sm)}
|
package/lib/web/layout.ts
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
export const SIDEBAR_COLLAPSE_MAX = 860;
|
|
28
28
|
|
|
29
29
|
export const WEB_LAYOUT_CSS = `.bf-app{display:flex;flex-direction:column;height:100vh;min-height:0;overflow:hidden;background:var(--canvas);color:var(--text);font:var(--fs-base)/var(--lh-normal) var(--font-ui)}
|
|
30
|
+
.bf-app{height:100dvh}
|
|
30
31
|
.bf-app__header{position:sticky;top:0;z-index:2;flex:0 0 auto;background:var(--surface);border-bottom:1px solid var(--border)}
|
|
31
32
|
.bf-app__body{flex:1 1 auto;min-height:0;display:grid;grid-template-columns:260px minmax(0,1fr)}
|
|
32
33
|
.bf-app__sidebar{display:flex;flex-direction:column;min-width:0;min-height:0;overflow-x:hidden;overflow-y:auto;background:var(--surface);border-right:1px solid var(--border)}
|
|
@@ -34,6 +35,7 @@ export const WEB_LAYOUT_CSS = `.bf-app{display:flex;flex-direction:column;height
|
|
|
34
35
|
.bf-app__footer{flex:0 0 auto;border-top:1px solid var(--border);color:var(--muted);font-size:var(--fs-sm);padding:var(--space-3) var(--space-6)}
|
|
35
36
|
@media (max-width:${SIDEBAR_COLLAPSE_MAX}px){
|
|
36
37
|
.bf-app{height:auto;min-height:100vh;overflow:visible}
|
|
38
|
+
.bf-app{min-height:100dvh}
|
|
37
39
|
.bf-app__body{flex:none;grid-template-columns:1fr}
|
|
38
40
|
.bf-app__sidebar{display:block;position:static;max-height:none;overflow:visible;border-right:0;border-bottom:1px solid var(--border)}
|
|
39
41
|
.bf-app__main{overflow:visible}
|
package/lib/web/markup.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { escapeHtml } from "../escape.ts";
|
|
10
|
+
import { THEME_JS_PATH } from "./runtime.ts";
|
|
10
11
|
|
|
11
12
|
/** Options for {@link docHead}. */
|
|
12
13
|
export interface DocHeadOptions {
|
|
@@ -27,7 +28,7 @@ export function docHead(options: DocHeadOptions): string {
|
|
|
27
28
|
<meta name="color-scheme" content="dark light">
|
|
28
29
|
<title>${escapeHtml(options.title)}</title>
|
|
29
30
|
<style>${options.css}</style>
|
|
30
|
-
<script src="
|
|
31
|
+
<script src="${THEME_JS_PATH}"></script>`;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/**
|