pi-skill-stacks 0.4.2 → 0.5.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 +18 -79
- package/package.json +3 -4
- package/src/core.ts +2 -2
- package/src/store.ts +4 -3
- package/extensions/header.ts +0 -262
package/README.md
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
# pi-skill-stacks
|
|
2
2
|
|
|
3
|
-
A [pi](https://github.com/earendil-works/pi-mono) package that groups
|
|
4
|
-
|
|
5
|
-
## What it does
|
|
6
|
-
|
|
7
|
-
- `/stacks` opens a two-pane overlay. Left: stacks with on/off state, create (`n`), delete (`d`), and `a` to add skills that aren't in any stack yet. Right: the selected stack's members; `space` removes one, `enter` opens the skill's full markdown in a viewer pane. So you can build and edit stacks without touching JSON. Changes persist as you make them; pi reloads once when you close the overlay, and only if a toggle actually changed `settings.json`.
|
|
8
|
-
- `/stacks on <stack>` / `/stacks off <stack>` toggle a single stack (with argument completion); `/stacks list` prints state without changing anything. Bare `/stacks` outside the TUI (RPC/print mode) prints the list too.
|
|
9
|
-
- Toggling off writes `!skills/<dir>/SKILL.md` exclusion patterns into the global settings `skills` array — pi's own override mechanism — so disabled skills are excluded everywhere, not just from the prompt. The extension only ever removes patterns it wrote itself (tracked in `managedExclusions`); hand-written `pi config` entries are left alone.
|
|
10
|
-
- The bundled header extension replaces pi's startup `[Skills]` listing with a compact line like `matt-pocock (28), firecrawl (28) · 76/76 skills active` (with `off: <name> (n)` segments for disabled stacks) and hides the `[Themes]` section.
|
|
3
|
+
A [pi](https://github.com/earendil-works/pi-mono) package that groups skills into named stacks you can toggle on and off. Disabled skills leave the system prompt, `/skill:` commands, and discovery.
|
|
11
4
|
|
|
12
5
|
## Install
|
|
13
6
|
|
|
@@ -15,27 +8,23 @@ A [pi](https://github.com/earendil-works/pi-mono) package that groups your skill
|
|
|
15
8
|
pi install npm:pi-skill-stacks
|
|
16
9
|
```
|
|
17
10
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
pi install git:github.com/oscabriel/pi-skill-stacks
|
|
22
|
-
```
|
|
11
|
+
## Use
|
|
23
12
|
|
|
24
|
-
|
|
13
|
+
`/stacks` opens an overlay. Left pane: stacks with on/off state. Right pane: the selected stack's members. `enter` on a member shows its markdown.
|
|
25
14
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
15
|
+
| Pane | Keys |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| stacks | `↑`/`↓` select · `space` on/off · `→` members · `a` add skills · `n` new stack · `d` delete · `esc` close |
|
|
18
|
+
| members | `↑`/`↓` move · `space` remove · `→` view · `a` add skills · `←` back |
|
|
19
|
+
| viewer | `↑`/`↓` scroll (mouse wheel in fullscreen) · `←` back |
|
|
29
20
|
|
|
30
|
-
|
|
21
|
+
`a` lists skills not yet in any stack. To move a skill between stacks, `space` it out of one and `a` it into the other.
|
|
31
22
|
|
|
32
|
-
|
|
33
|
-
pi -e git:github.com/oscabriel/pi-skill-stacks
|
|
34
|
-
```
|
|
23
|
+
From the command line: `/stacks on <stack>`, `/stacks off <stack>`, `/stacks list`.
|
|
35
24
|
|
|
36
|
-
##
|
|
25
|
+
## How it works
|
|
37
26
|
|
|
38
|
-
|
|
27
|
+
Stacks live in `~/.pi/agent/skill-stacks.json` (`PI_CODING_AGENT_DIR` is honoured). You can edit it by hand:
|
|
39
28
|
|
|
40
29
|
```json
|
|
41
30
|
{
|
|
@@ -46,66 +35,16 @@ The package ships with no stacks. Open `/stacks` and press `n` to create one, or
|
|
|
46
35
|
}
|
|
47
36
|
```
|
|
48
37
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
A project can add or override stack definitions in `<project>/.pi/skill-stacks.json`. Project stacks contribute definitions only; on/off state is global. In the overlay they can be toggled but not edited or deleted — edit the project file instead.
|
|
52
|
-
|
|
53
|
-
The config file is validated on every load. A malformed entry (a stack that isn't an array, invalid JSON, and so on) aborts the command with an error and nothing is written, rather than defaulting to an empty config and saving that back.
|
|
54
|
-
|
|
55
|
-
## Overlay keys
|
|
56
|
-
|
|
57
|
-
Navigation is consistent across panes: `←`/`esc` always goes back a pane, `→`/`tab` (or `enter` on a member) always goes forward. `←` in the leftmost pane and `→` in the viewer do nothing.
|
|
58
|
-
|
|
59
|
-
| Pane | Keys |
|
|
60
|
-
| --- | --- |
|
|
61
|
-
| stacks | `↑`/`↓` (or `k`/`j`) select · `space` on/off · `→`/`l`/`tab`/`enter` into members · `a` add skills · `n` new stack · `d` delete · `esc`/`q` close |
|
|
62
|
-
| members | `↑`/`↓` (or `k`/`j`) move · `space` remove the skill · `enter`/`→`/`tab` view skill · `a` add skills · `←`/`h`/`esc` back to stacks |
|
|
63
|
-
| skill viewer | `↑`/`↓` (or `k`/`j`) scroll · `enter`/`←`/`h`/`esc` back to members |
|
|
64
|
-
| add skills dialog | `↑`/`↓` move · `space` mark · `enter` add the marked skills (or the highlighted one) · `esc` cancel |
|
|
65
|
-
| new stack / delete dialogs | `enter` confirm · `esc` cancel |
|
|
66
|
-
|
|
67
|
-
The `a`, `n` and `d` dialogs open as small overlays on top of `/stacks`. `a` lists only skills that no stack holds yet. To move a skill between stacks, `space` it out of one and `a` it into the other.
|
|
68
|
-
|
|
69
|
-
In fullscreen TUI mode the mouse wheel scrolls the skill viewer when the pointer is over the overlay. In regular mode the terminal owns the mouse and wheel input goes to its own scrollback.
|
|
70
|
-
|
|
71
|
-
`enter` in the members pane opens the skill's full markdown in a viewer pane inside the overlay: frontmatter dim, headings bold, bullets and indented code rendered, links shown as their text.
|
|
72
|
-
|
|
73
|
-
The title bar shows `reload pending` once a change has touched `settings.json`; the reload runs when you close the overlay.
|
|
74
|
-
|
|
75
|
-
## Rules
|
|
76
|
-
|
|
77
|
-
- **Overlap:** a skill is excluded only when it appears in at least one stack and no enabled stack contains it. Skills in no stack are never touched. So disabling a stack whose members all also belong to an enabled stack excludes nothing.
|
|
78
|
-
- **Stacks, not skills:** `on`/`off` take stack names only. To exclude a single skill regardless of stacks, use `pi config` (its `!` entries are respected and never claimed).
|
|
79
|
-
- **Persist-then-reload:** `on`/`off` commands write immediately and reload if `settings.json` changed. In the overlay every change is written as you make it and a single reload runs on close, again only if `settings.json` changed; re-stacking alone doesn't need one (the header line catches up on the next reload).
|
|
80
|
-
- **Other projects' stacks:** a project stack you disabled from inside its project stays disabled when you toggle things elsewhere. Its `disabledStacks` entry and the exclusions written for its skills are preserved, because from another directory those skills are "in no stack" and so left alone.
|
|
81
|
-
|
|
82
|
-
## Header
|
|
83
|
-
|
|
84
|
-
The header extension swaps pi's full skills listing for a compact summary and hides `[Themes]`. It also replaces the stock header banner with a minimal one-line directory label. If you run your own header extension (custom art, dashboard, and so on), disable this one and keep yours — add a filter to the package entry in `settings.json`:
|
|
85
|
-
|
|
86
|
-
```json
|
|
87
|
-
{
|
|
88
|
-
"packages": [
|
|
89
|
-
{
|
|
90
|
-
"source": "git:github.com/oscabriel/pi-skill-stacks",
|
|
91
|
-
"extensions": ["!extensions/header.ts"]
|
|
92
|
-
}
|
|
93
|
-
]
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
Your header extension can import the section machinery from this package's `extensions/header.ts` to render the same compact line itself: `buildSkillsSection(cwd, theme)` builds the node, `replaceSkillsSection(root, node)` and `hideThemesSection(root)` splice pi's sections, `isOurSection`, `firstLineOf`, and `renderedText` are the matchers, and `SectionTheme`/`RenderableNode` are the structural types they take.
|
|
38
|
+
Skills are discovered like pi does: every `SKILL.md` under `~/.agents/skills/` and `<agentDir>/skills/`, named by frontmatter `name` or the directory name. A project can add stack definitions in `<project>/.pi/skill-stacks.json`; those can be toggled but not edited from the overlay.
|
|
98
39
|
|
|
99
|
-
|
|
40
|
+
Turning a stack off writes `!skills/<dir>/SKILL.md` patterns into the `skills` array of `~/.pi/agent/settings.json`, pi's own exclusion mechanism. The package only removes patterns it wrote itself; hand-written entries stay. A skill is excluded only when no enabled stack contains it, and skills in no stack are never touched. pi reloads once when the overlay closes, only if `settings.json` changed.
|
|
100
41
|
|
|
101
|
-
|
|
102
|
-
- Counts in the overlay and header only include discovered skills, so a stale name doesn't inflate the numbers.
|
|
103
|
-
- `settings.json` is rewritten without a trailing newline to match pi's own formatting; `skill-stacks.json` ends with one.
|
|
42
|
+
Stack names that don't match a discovered skill are flagged `missing` and not counted.
|
|
104
43
|
|
|
105
44
|
## Develop
|
|
106
45
|
|
|
107
46
|
```bash
|
|
108
|
-
npm install
|
|
109
|
-
npm run check
|
|
110
|
-
npm test
|
|
47
|
+
npm install
|
|
48
|
+
npm run check
|
|
49
|
+
npm test
|
|
111
50
|
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-skill-stacks",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Pi package that groups skills into named stacks you can manage from a /stacks overlay — toggle stacks on/off and re-stack skills without editing JSON. Exclusions are written through pi's own settings override mechanism
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "Pi package that groups skills into named stacks you can manage from a /stacks overlay — toggle stacks on/off and re-stack skills without editing JSON. Exclusions are written through pi's own settings override mechanism.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
7
7
|
"pi",
|
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"pi": {
|
|
29
29
|
"extensions": [
|
|
30
|
-
"./extensions/index.ts"
|
|
31
|
-
"./extensions/header.ts"
|
|
30
|
+
"./extensions/index.ts"
|
|
32
31
|
]
|
|
33
32
|
},
|
|
34
33
|
"devDependencies": {
|
package/src/core.ts
CHANGED
|
@@ -36,7 +36,7 @@ export interface StacksSummary {
|
|
|
36
36
|
offStacks: string[];
|
|
37
37
|
totalCount: number;
|
|
38
38
|
activeCount: number;
|
|
39
|
-
/** Per-stack status in definition order
|
|
39
|
+
/** Per-stack status in definition order. */
|
|
40
40
|
stacks: StackStatus[];
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -161,7 +161,7 @@ export function nextDisabledStacks(
|
|
|
161
161
|
return sortNames(new Set([...unseen, ...visibleDisabled]));
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
/**
|
|
164
|
+
/** Stack and active-skill counts. Only discovered skills are counted. */
|
|
165
165
|
export function summarizeStacks(
|
|
166
166
|
stacks: StackMap,
|
|
167
167
|
disabledStacks: string[],
|
package/src/store.ts
CHANGED
|
@@ -246,9 +246,10 @@ export function updateSettingsSkills(path: string, skills: string[]) {
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
/**
|
|
249
|
-
* Fresh-from-disk summary for
|
|
250
|
-
*
|
|
251
|
-
*
|
|
249
|
+
* Fresh-from-disk stacks summary for external consumers (e.g. a user's own
|
|
250
|
+
* header extension). Not used by the package itself.
|
|
251
|
+
* Returns undefined when no stacks are configured. Throws ConfigError on
|
|
252
|
+
* malformed config; callers decide whether to fall back.
|
|
252
253
|
*/
|
|
253
254
|
export function loadStacksSummary(cwd: string) {
|
|
254
255
|
const global = loadStacksConfig();
|
package/extensions/header.ts
DELETED
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pi-skill-stacks header extension: replaces pi's startup `[Skills]` section
|
|
3
|
-
* with a compact one-line summary (e.g. `matt-pocock (28), firecrawl (28) ·
|
|
4
|
-
* 76/76 skills active`), hides the `[Themes]` section, and renders a minimal
|
|
5
|
-
* one-line banner in place of pi's stock header.
|
|
6
|
-
*
|
|
7
|
-
* Disable this file (settings.json package filter `!extensions/header.ts`) to
|
|
8
|
-
* keep your own header extension; the /stacks command extension is unaffected.
|
|
9
|
-
* The section-building helpers are exported so a custom header can reuse them.
|
|
10
|
-
*
|
|
11
|
-
* Mechanism notes:
|
|
12
|
-
* - pi's startup sections are childless leaves inside one container. The
|
|
13
|
-
* container's first rendered line can itself be "[Skills]" (when no
|
|
14
|
-
* [Context] section precedes it), so matchers require !hasChildren.
|
|
15
|
-
* - showLoadedResources runs after extension session_start handlers, so
|
|
16
|
-
* post-paint sweeps alone flash the full listing for a frame. addChild
|
|
17
|
-
* wrapping swaps our node in at insertion time instead. Timed sweeps stay
|
|
18
|
-
* as fallback for late installs; resources_discover re-runs the walk.
|
|
19
|
-
*/
|
|
20
|
-
import { homedir } from "node:os";
|
|
21
|
-
import { isAbsolute, relative } from "node:path";
|
|
22
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
23
|
-
import { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
24
|
-
import { loadStacksSummary } from "../src/store.ts";
|
|
25
|
-
|
|
26
|
-
export interface RenderableNode {
|
|
27
|
-
children?: RenderableNode[];
|
|
28
|
-
addChild?(component: RenderableNode): void;
|
|
29
|
-
invalidate(): void;
|
|
30
|
-
render(width: number): string[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Subset of pi's Theme we use; the real Theme is assignable to it. */
|
|
34
|
-
export interface SectionTheme {
|
|
35
|
-
fg(color: "mdHeading" | "dim", text: string): string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface DashboardTui extends RenderableNode {
|
|
39
|
-
requestRender(force?: boolean): void;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const RESET = "\x1b[0m";
|
|
43
|
-
const BOLD = "\x1b[1m";
|
|
44
|
-
const DIM = "\x1b[2m";
|
|
45
|
-
const ANSI_PATTERN =
|
|
46
|
-
/[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
|
|
47
|
-
|
|
48
|
-
const hasChildren = (
|
|
49
|
-
component: RenderableNode,
|
|
50
|
-
): component is RenderableNode & { children: RenderableNode[] } => Array.isArray(component.children);
|
|
51
|
-
|
|
52
|
-
export function renderedText(component: RenderableNode) {
|
|
53
|
-
try {
|
|
54
|
-
return component.render(200).join("\n").replace(ANSI_PATTERN, "");
|
|
55
|
-
} catch {
|
|
56
|
-
return "";
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function firstLineOf(component: RenderableNode) {
|
|
61
|
-
return renderedText(component)
|
|
62
|
-
.split("\n")
|
|
63
|
-
.find((line) => line.trim())
|
|
64
|
-
?.trim();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Marks the section nodes we build, so the addChild interceptor and the sweep
|
|
68
|
-
// never re-match them (their first rendered line is also exactly "[Skills]").
|
|
69
|
-
const ourSections = new WeakSet<object>();
|
|
70
|
-
|
|
71
|
-
export const isOurSection = (component: object) => ourSections.has(component);
|
|
72
|
-
|
|
73
|
-
/** A pi section leaf we should act on: exact header line, childless, not one of ours. */
|
|
74
|
-
const isPiSection = (child: RenderableNode, header: string) =>
|
|
75
|
-
firstLineOf(child) === header && !hasChildren(child) && !isOurSection(child);
|
|
76
|
-
|
|
77
|
-
export function hideThemesSection(component: RenderableNode): boolean {
|
|
78
|
-
if (!hasChildren(component)) return false;
|
|
79
|
-
|
|
80
|
-
for (let index = 0; index < component.children.length; index += 1) {
|
|
81
|
-
const child = component.children[index]!;
|
|
82
|
-
// Leaf check: pi's section components are childless ExpandableText nodes.
|
|
83
|
-
// The parent resources container can render the same first line when it's
|
|
84
|
-
// the first section, and matching it would splice out every section.
|
|
85
|
-
if (isPiSection(child, "[Themes]")) {
|
|
86
|
-
const next = component.children[index + 1];
|
|
87
|
-
const removeCount = next && renderedText(next).trim() === "" ? 2 : 1;
|
|
88
|
-
component.children.splice(index, removeCount);
|
|
89
|
-
component.invalidate();
|
|
90
|
-
return true;
|
|
91
|
-
}
|
|
92
|
-
if (hideThemesSection(child)) return true;
|
|
93
|
-
}
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Build a replacement [Skills] section styled like pi's own sections:
|
|
99
|
-
* mdHeading header line, dim indented body listing each stack with its size,
|
|
100
|
-
* disabled stacks separately, plus the active-skill count. Data is read fresh
|
|
101
|
-
* from disk so it stays correct across /reload. Returns undefined when no
|
|
102
|
-
* stacks are configured or the config is unreadable (pi's section is left alone).
|
|
103
|
-
*/
|
|
104
|
-
export function buildSkillsSection(cwd: string, theme: SectionTheme | undefined) {
|
|
105
|
-
let summary;
|
|
106
|
-
try {
|
|
107
|
-
summary = loadStacksSummary(cwd);
|
|
108
|
-
} catch {
|
|
109
|
-
return undefined;
|
|
110
|
-
}
|
|
111
|
-
if (!summary) return undefined;
|
|
112
|
-
const heading = (text: string) => (theme ? theme.fg("mdHeading", text) : `${BOLD}${text}${RESET}`);
|
|
113
|
-
const dim = (text: string) => (theme ? theme.fg("dim", text) : `${DIM}${text}${RESET}`);
|
|
114
|
-
const label = (stack: { name: string; size: number }) => `${stack.name} (${stack.size})`;
|
|
115
|
-
const on = summary.stacks.filter((stack) => stack.enabled).map(label).join(", ");
|
|
116
|
-
const off = summary.stacks.filter((stack) => !stack.enabled).map(label).join(", ");
|
|
117
|
-
const parts: string[] = [];
|
|
118
|
-
if (on) parts.push(on);
|
|
119
|
-
if (off) parts.push(`off: ${off}`);
|
|
120
|
-
parts.push(`${summary.activeCount}/${summary.totalCount} skills active`);
|
|
121
|
-
const section = new Text(`${heading("[Skills]")}\n${dim(` ${parts.join(" · ")}`)}`, 0, 0);
|
|
122
|
-
ourSections.add(section);
|
|
123
|
-
return section;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function replaceSkillsSection(component: RenderableNode, section: Text): boolean {
|
|
127
|
-
if (!hasChildren(component)) return false;
|
|
128
|
-
|
|
129
|
-
for (let index = 0; index < component.children.length; index += 1) {
|
|
130
|
-
const child = component.children[index]!;
|
|
131
|
-
if (isPiSection(child, "[Skills]")) {
|
|
132
|
-
component.children.splice(index, 1, section);
|
|
133
|
-
component.invalidate();
|
|
134
|
-
return true;
|
|
135
|
-
}
|
|
136
|
-
if (replaceSkillsSection(child, section)) return true;
|
|
137
|
-
}
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function formatDirectory(cwd: string) {
|
|
142
|
-
const home = homedir();
|
|
143
|
-
if (cwd === home) return "~";
|
|
144
|
-
const rel = relative(home, cwd);
|
|
145
|
-
return rel.startsWith("..") || isAbsolute(rel) ? cwd : `~/${rel}`;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export default function skillStacksHeader(pi: ExtensionAPI) {
|
|
149
|
-
let cwd = process.cwd();
|
|
150
|
-
let activeTui: DashboardTui | undefined;
|
|
151
|
-
let sectionTheme: SectionTheme | undefined;
|
|
152
|
-
let banner: string[] = [];
|
|
153
|
-
let fixupTimers: Array<ReturnType<typeof setTimeout>> = [];
|
|
154
|
-
const interceptedContainers = new WeakSet<object>();
|
|
155
|
-
|
|
156
|
-
// One disk read per fixup cycle: the interceptor and every sweep in the
|
|
157
|
-
// cycle share the same section instead of re-scanning the skill roots.
|
|
158
|
-
let cachedSection: { cwd: string; section: Text | undefined } | undefined;
|
|
159
|
-
function skillsSection() {
|
|
160
|
-
if (!cachedSection || cachedSection.cwd !== cwd) {
|
|
161
|
-
cachedSection = { cwd, section: buildSkillsSection(cwd, sectionTheme) };
|
|
162
|
-
}
|
|
163
|
-
return cachedSection.section;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Wrap addChild on every container reachable from the TUI root so pi's
|
|
167
|
-
// [Skills] section is swapped (and [Themes] dropped) at insertion time,
|
|
168
|
-
// before the first paint.
|
|
169
|
-
function interceptContainers(node: RenderableNode) {
|
|
170
|
-
if (hasChildren(node) && typeof node.addChild === "function" && !interceptedContainers.has(node)) {
|
|
171
|
-
interceptedContainers.add(node);
|
|
172
|
-
const original = node.addChild.bind(node);
|
|
173
|
-
let dropNextBlank = false;
|
|
174
|
-
node.addChild = (child: RenderableNode) => {
|
|
175
|
-
if (dropNextBlank) {
|
|
176
|
-
dropNextBlank = false;
|
|
177
|
-
// The spacer pi adds right after the section it belongs to.
|
|
178
|
-
if (!renderedText(child).trim()) return;
|
|
179
|
-
}
|
|
180
|
-
if (!isOurSection(child)) {
|
|
181
|
-
const firstLine = firstLineOf(child);
|
|
182
|
-
if (firstLine === "[Themes]") {
|
|
183
|
-
dropNextBlank = true;
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
if (firstLine === "[Skills]") {
|
|
187
|
-
const section = skillsSection();
|
|
188
|
-
if (section) {
|
|
189
|
-
original(section);
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
original(child);
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
if (hasChildren(node)) {
|
|
198
|
-
for (const child of node.children) interceptContainers(child);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Fallback for sections that were inserted before we wrapped their
|
|
203
|
-
// container (e.g. this extension loading into an already-painted session).
|
|
204
|
-
function sweepHeader(tui: DashboardTui) {
|
|
205
|
-
let changed = hideThemesSection(tui);
|
|
206
|
-
const section = skillsSection();
|
|
207
|
-
if (section && replaceSkillsSection(tui, section)) changed = true;
|
|
208
|
-
if (changed) tui.requestRender(true);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function clearFixupTimers() {
|
|
212
|
-
for (const timer of fixupTimers) clearTimeout(timer);
|
|
213
|
-
fixupTimers = [];
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function scheduleHeaderFixups(tui: DashboardTui) {
|
|
217
|
-
clearFixupTimers();
|
|
218
|
-
cachedSection = undefined;
|
|
219
|
-
interceptContainers(tui);
|
|
220
|
-
sweepHeader(tui);
|
|
221
|
-
for (const delay of [0, 50, 250, 1_000]) {
|
|
222
|
-
fixupTimers.push(setTimeout(() => sweepHeader(tui), delay));
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
pi.on("session_start", (_event, ctx) => {
|
|
227
|
-
cwd = ctx.cwd;
|
|
228
|
-
if (ctx.mode !== "tui") return;
|
|
229
|
-
|
|
230
|
-
ctx.ui.setHeader((tui, theme) => {
|
|
231
|
-
activeTui = tui;
|
|
232
|
-
sectionTheme = theme;
|
|
233
|
-
const label = theme.fg("muted", formatDirectory(cwd));
|
|
234
|
-
banner = [];
|
|
235
|
-
scheduleHeaderFixups(tui);
|
|
236
|
-
|
|
237
|
-
return {
|
|
238
|
-
render(width: number) {
|
|
239
|
-
// One dim centered line: the working directory.
|
|
240
|
-
if (banner.length === 0) {
|
|
241
|
-
const padding = Math.max(0, Math.floor((width - visibleWidth(label)) / 2));
|
|
242
|
-
banner = [truncateToWidth(`${" ".repeat(padding)}${label}`, width)];
|
|
243
|
-
}
|
|
244
|
-
return banner;
|
|
245
|
-
},
|
|
246
|
-
invalidate() {
|
|
247
|
-
banner = [];
|
|
248
|
-
},
|
|
249
|
-
};
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
pi.on("resources_discover", () => {
|
|
254
|
-
if (activeTui) scheduleHeaderFixups(activeTui);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
pi.on("session_shutdown", (_event, ctx) => {
|
|
258
|
-
clearFixupTimers();
|
|
259
|
-
activeTui = undefined;
|
|
260
|
-
if (ctx.mode === "tui") ctx.ui.setHeader(undefined);
|
|
261
|
-
});
|
|
262
|
-
}
|