pi-minimal 0.1.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/.github/workflows/ci.yml +25 -0
- package/.github/workflows/publish.yml +29 -0
- package/LICENSE +21 -0
- package/README.md +39 -0
- package/dist/activity.js +12 -0
- package/dist/chrome.js +1 -0
- package/dist/config.js +45 -0
- package/dist/cwd.js +103 -0
- package/dist/footer.js +46 -0
- package/dist/header.js +14 -0
- package/dist/herdr.js +44 -0
- package/dist/index.js +155 -0
- package/dist/live-watch.js +52 -0
- package/dist/reclaim.js +19 -0
- package/dist/tool-rows.js +34 -0
- package/dist/tools.js +103 -0
- package/dist/tracker.js +63 -0
- package/dist/working-status.js +191 -0
- package/doc/plans/2026-09-22-pi-minimal-package.md +354 -0
- package/doc/specs/2026-09-22-pi-minimal-package.md +94 -0
- package/eslint.config.js +8 -0
- package/package.json +37 -0
- package/src/activity.ts +16 -0
- package/src/chrome.ts +1 -0
- package/src/config.ts +59 -0
- package/src/cwd.ts +108 -0
- package/src/footer.ts +54 -0
- package/src/header.ts +16 -0
- package/src/herdr.ts +66 -0
- package/src/index.ts +152 -0
- package/src/live-watch.ts +59 -0
- package/src/reclaim.ts +34 -0
- package/src/tool-rows.ts +58 -0
- package/src/tools.ts +161 -0
- package/src/tracker.ts +91 -0
- package/src/working-status.ts +214 -0
- package/test/activity.test.ts +27 -0
- package/test/config.test.ts +114 -0
- package/test/index.test.ts +262 -0
- package/test/package.test.ts +18 -0
- package/test/tools.test.ts +48 -0
- package/test/tracker.test.ts +104 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tests.json +8 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-node@v4
|
|
17
|
+
with:
|
|
18
|
+
node-version: 22.19.0
|
|
19
|
+
cache: npm
|
|
20
|
+
- run: npm ci
|
|
21
|
+
- run: npm run lint
|
|
22
|
+
- run: npm run typecheck
|
|
23
|
+
- run: npm test
|
|
24
|
+
- run: npm run build
|
|
25
|
+
- run: git diff --exit-code -- dist
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
id-token: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
publish:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
environment: npm
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: 22.19.0
|
|
20
|
+
registry-url: https://registry.npmjs.org
|
|
21
|
+
cache: npm
|
|
22
|
+
- run: npm ci
|
|
23
|
+
- run: npm run lint
|
|
24
|
+
- run: npm test
|
|
25
|
+
- run: npm run build
|
|
26
|
+
- run: git diff --exit-code -- dist
|
|
27
|
+
- run: npm publish --access public
|
|
28
|
+
env:
|
|
29
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 raviy06
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# pi-minimal
|
|
2
|
+
|
|
3
|
+
A Pi coding-agent package.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node.js `>=22.19.0`
|
|
8
|
+
- Pi `>=0.84.4`
|
|
9
|
+
|
|
10
|
+
No other Pi extension is required. Pi supplies the peer packages used by this extension.
|
|
11
|
+
|
|
12
|
+
Optional integrations:
|
|
13
|
+
|
|
14
|
+
- `pi-gauntlet`: supplies the phase and task data shown in the footer. Its widgets are hidden by default.
|
|
15
|
+
- Herdr: sibling status appears only when Pi is running inside Herdr.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pi install git:github.com/raviy06/pi-minimal
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Do not load this package beside the live `grok-chrome` extension. Disable or remove `grok-chrome` before enabling `pi-minimal`.
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
The package loads JSON in this order:
|
|
28
|
+
|
|
29
|
+
1. Built-in defaults
|
|
30
|
+
2. Global: `~/.pi/agent/pi-minimal.json` (or `$PI_CODING_AGENT_DIR/pi-minimal.json` when the agent directory is overridden)
|
|
31
|
+
3. Project: `.pi/pi-minimal.json` in the current working directory, only when the project is trusted
|
|
32
|
+
|
|
33
|
+
Project values override global values. A missing file, omitted key, or invalid value does not override the lower-precedence value.
|
|
34
|
+
|
|
35
|
+
The only supported key is `hideGauntletWidgets`. It defaults to `true`.
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{ "hideGauntletWidgets": true }
|
|
39
|
+
```
|
package/dist/activity.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function rowIsRunning(context) {
|
|
2
|
+
if (context.isError)
|
|
3
|
+
return false;
|
|
4
|
+
if (context.state?.done)
|
|
5
|
+
return false;
|
|
6
|
+
if (context.isPartial === false)
|
|
7
|
+
return false;
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
export function activityBullet(running, frame) {
|
|
11
|
+
return running && frame === 1 ? "◇" : "◆";
|
|
12
|
+
}
|
package/dist/chrome.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CHROME_MARGIN = 1;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
const DEFAULTS = {
|
|
5
|
+
hideGauntletWidgets: true,
|
|
6
|
+
};
|
|
7
|
+
export function loadPiMinimalConfig(options) {
|
|
8
|
+
const config = { ...DEFAULTS };
|
|
9
|
+
applyLayer(config, join(options.agentDir, "pi-minimal.json"));
|
|
10
|
+
if (options.projectTrusted) {
|
|
11
|
+
applyLayer(config, join(options.cwd, CONFIG_DIR_NAME, "pi-minimal.json"));
|
|
12
|
+
}
|
|
13
|
+
return config;
|
|
14
|
+
}
|
|
15
|
+
function applyLayer(config, path) {
|
|
16
|
+
let text;
|
|
17
|
+
try {
|
|
18
|
+
text = readFileSync(path, "utf8");
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(text);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
console.error(`pi-minimal: invalid JSON in ${path}`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
32
|
+
console.error(`pi-minimal: expected an object in ${path}`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const record = parsed;
|
|
36
|
+
if (!Object.hasOwn(record, "hideGauntletWidgets")) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const value = record.hideGauntletWidgets;
|
|
40
|
+
if (typeof value !== "boolean") {
|
|
41
|
+
console.error(`pi-minimal: hideGauntletWidgets must be a boolean in ${path}`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
config.hideGauntletWidgets = value;
|
|
45
|
+
}
|
package/dist/cwd.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { CHROME_MARGIN } from "./chrome.js";
|
|
4
|
+
function formatCwd(cwd) {
|
|
5
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
6
|
+
if (!home)
|
|
7
|
+
return cwd;
|
|
8
|
+
const resolved = path.resolve(cwd);
|
|
9
|
+
const rel = path.relative(path.resolve(home), resolved);
|
|
10
|
+
if (!rel.startsWith("..") && !path.isAbsolute(rel)) {
|
|
11
|
+
return rel ? `~/${rel}` : "~";
|
|
12
|
+
}
|
|
13
|
+
return cwd;
|
|
14
|
+
}
|
|
15
|
+
function formatTokens(n) {
|
|
16
|
+
if (n < 1000)
|
|
17
|
+
return String(n);
|
|
18
|
+
if (n < 10000)
|
|
19
|
+
return `${(n / 1000).toFixed(1)}K`;
|
|
20
|
+
if (n < 1e6)
|
|
21
|
+
return `${Math.round(n / 1000)}K`;
|
|
22
|
+
if (n < 1e7)
|
|
23
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
24
|
+
return `${Math.round(n / 1e6)}M`;
|
|
25
|
+
}
|
|
26
|
+
function modelIcon(id) {
|
|
27
|
+
const s = id.toLowerCase();
|
|
28
|
+
if (s.includes("grok"))
|
|
29
|
+
return "✦";
|
|
30
|
+
if (s.includes("claude") ||
|
|
31
|
+
s.includes("fable") ||
|
|
32
|
+
s.includes("sonnet") ||
|
|
33
|
+
s.includes("opus") ||
|
|
34
|
+
s.includes("haiku")) {
|
|
35
|
+
return "✳";
|
|
36
|
+
}
|
|
37
|
+
if (s.includes("gpt") || s.includes("codex"))
|
|
38
|
+
return "◈";
|
|
39
|
+
return "◆";
|
|
40
|
+
}
|
|
41
|
+
function shortModel(id) {
|
|
42
|
+
return (id.split("/").pop() || id).replace(/^claude-/, "");
|
|
43
|
+
}
|
|
44
|
+
function thinkingColor(level) {
|
|
45
|
+
if (level === "high" || level === "xhigh" || level === "max")
|
|
46
|
+
return "warning";
|
|
47
|
+
if (level === "medium")
|
|
48
|
+
return "accent";
|
|
49
|
+
return "dim";
|
|
50
|
+
}
|
|
51
|
+
function meter(percent, fill, empty) {
|
|
52
|
+
const n = 8;
|
|
53
|
+
const filled = Math.max(0, Math.min(n, Math.round((percent / 100) * n)));
|
|
54
|
+
return fill.repeat(filled) + empty.repeat(n - filled);
|
|
55
|
+
}
|
|
56
|
+
function splitRow(left, right, width) {
|
|
57
|
+
let L = left;
|
|
58
|
+
let R = right;
|
|
59
|
+
while (visibleWidth(L) + 1 + visibleWidth(R) > width && visibleWidth(R) > 0) {
|
|
60
|
+
R = truncateToWidth(R, Math.max(0, visibleWidth(R) - 1), "");
|
|
61
|
+
}
|
|
62
|
+
while (visibleWidth(L) + 1 + visibleWidth(R) > width && visibleWidth(L) > 0) {
|
|
63
|
+
L = truncateToWidth(L, Math.max(0, visibleWidth(L) - 1), "");
|
|
64
|
+
}
|
|
65
|
+
const gap = Math.max(1, width - visibleWidth(L) - visibleWidth(R));
|
|
66
|
+
return L + " ".repeat(gap) + R;
|
|
67
|
+
}
|
|
68
|
+
export function installCwd(ctx) {
|
|
69
|
+
ctx.ui.setWidget("tw-cwd", (_tui, theme) => ({
|
|
70
|
+
invalidate() { },
|
|
71
|
+
render(width) {
|
|
72
|
+
const inner = Math.max(8, width - CHROME_MARGIN * 2);
|
|
73
|
+
const id = ctx.model?.id || "no-model";
|
|
74
|
+
const icon = theme.fg("accent", modelIcon(id));
|
|
75
|
+
const name = theme.fg("text", shortModel(id));
|
|
76
|
+
const usage = ctx.getContextUsage?.();
|
|
77
|
+
const window = usage?.contextWindow || ctx.model?.contextWindow;
|
|
78
|
+
const used = usage?.tokens;
|
|
79
|
+
let tokenStr = theme.fg("dim", "?/?");
|
|
80
|
+
let bar = theme.fg("dim", "░░░░░░░░");
|
|
81
|
+
let pctStr = theme.fg("dim", "?%");
|
|
82
|
+
if (typeof used === "number" && window) {
|
|
83
|
+
const pct = Math.max(0, Math.min(100, Math.round((used / window) * 100)));
|
|
84
|
+
tokenStr = theme.fg("dim", `${formatTokens(used)}/${formatTokens(window)}`);
|
|
85
|
+
const hot = pct >= 85;
|
|
86
|
+
bar = meter(pct, theme.fg(hot ? "warning" : "accent", "█"), theme.fg("dim", "░"));
|
|
87
|
+
pctStr = theme.fg(hot ? "warning" : "muted", `${pct}%`);
|
|
88
|
+
}
|
|
89
|
+
else if (used === null && window) {
|
|
90
|
+
tokenStr = theme.fg("dim", `?/${formatTokens(window)}`);
|
|
91
|
+
}
|
|
92
|
+
const thinking = ctx.thinkingLevel || "";
|
|
93
|
+
const think = thinking ? theme.fg(thinkingColor(thinking), thinking) : "";
|
|
94
|
+
const midot = theme.fg("dim", " · ");
|
|
95
|
+
const left = [`${icon} ${name}`, tokenStr, `${bar} ${pctStr}`, think]
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.join(midot);
|
|
98
|
+
const right = ctx.cwd ? theme.fg("accent", formatCwd(ctx.cwd)) : "";
|
|
99
|
+
const row = splitRow(left, right, inner);
|
|
100
|
+
return [`${" ".repeat(CHROME_MARGIN)}${row}`];
|
|
101
|
+
},
|
|
102
|
+
}), { placement: "aboveEditor" });
|
|
103
|
+
}
|
package/dist/footer.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { CHROME_MARGIN } from "./chrome.js";
|
|
4
|
+
import { formatTrackerLines } from "./tracker.js";
|
|
5
|
+
function statusColor(status) {
|
|
6
|
+
if (status === "idle" || status === "done")
|
|
7
|
+
return "success";
|
|
8
|
+
if (status === "working")
|
|
9
|
+
return "accent";
|
|
10
|
+
if (status === "blocked")
|
|
11
|
+
return "error";
|
|
12
|
+
return "muted";
|
|
13
|
+
}
|
|
14
|
+
export function installFooter(ctx, getSiblings) {
|
|
15
|
+
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
16
|
+
const unsub = footerData.onBranchChange(() => tui.requestRender());
|
|
17
|
+
return {
|
|
18
|
+
dispose: unsub,
|
|
19
|
+
invalidate() { },
|
|
20
|
+
render(width) {
|
|
21
|
+
const send = keyHint("tui.input.submit", "send");
|
|
22
|
+
const think = keyHint("app.thinking.cycle", "thinking");
|
|
23
|
+
const left = [send, think].filter(Boolean).join(" · ");
|
|
24
|
+
const siblings = getSiblings();
|
|
25
|
+
let right = "";
|
|
26
|
+
if (siblings && siblings.length > 0) {
|
|
27
|
+
right = siblings
|
|
28
|
+
.map((s) => {
|
|
29
|
+
const name = theme.fg("text", s.name);
|
|
30
|
+
const st = theme.fg(statusColor(s.status), s.status);
|
|
31
|
+
return `${name} ${st}`;
|
|
32
|
+
})
|
|
33
|
+
.join(theme.fg("dim", " · "));
|
|
34
|
+
}
|
|
35
|
+
const inner = Math.max(8, width - CHROME_MARGIN * 2);
|
|
36
|
+
const pad = Math.max(1, inner - visibleWidth(left) - visibleWidth(right));
|
|
37
|
+
const line = truncateToWidth(left + " ".repeat(pad) + right, inner, "");
|
|
38
|
+
const out = [`${" ".repeat(CHROME_MARGIN)}${line}`];
|
|
39
|
+
for (const extra of formatTrackerLines(ctx, theme)) {
|
|
40
|
+
out.push(`${" ".repeat(CHROME_MARGIN)}${truncateToWidth(extra, inner, "")}`);
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
package/dist/header.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
2
|
+
export function installHeader(ctx) {
|
|
3
|
+
ctx.ui.setHeader(() => {
|
|
4
|
+
const text = new Text("", 0, 0);
|
|
5
|
+
return {
|
|
6
|
+
render() {
|
|
7
|
+
return text.render(1);
|
|
8
|
+
},
|
|
9
|
+
invalidate() {
|
|
10
|
+
text.invalidate();
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
}
|
package/dist/herdr.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function parseJson(stdout) {
|
|
2
|
+
return JSON.parse(stdout);
|
|
3
|
+
}
|
|
4
|
+
export function parseAgentList(stdout) {
|
|
5
|
+
const data = parseJson(stdout);
|
|
6
|
+
const rows = data.result?.agents;
|
|
7
|
+
if (!Array.isArray(rows))
|
|
8
|
+
throw new Error("invalid herdr agent list");
|
|
9
|
+
return rows.map((row) => ({
|
|
10
|
+
agent: String(row.agent ?? ""),
|
|
11
|
+
agent_status: String(row.agent_status ?? ""),
|
|
12
|
+
pane_id: String(row.pane_id ?? ""),
|
|
13
|
+
tab_id: String(row.tab_id ?? ""),
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
export function parseTabList(stdout) {
|
|
17
|
+
const data = parseJson(stdout);
|
|
18
|
+
const rows = data.result?.tabs;
|
|
19
|
+
if (!Array.isArray(rows))
|
|
20
|
+
throw new Error("invalid herdr tab list");
|
|
21
|
+
return rows.map((row) => ({
|
|
22
|
+
label: String(row.label ?? ""),
|
|
23
|
+
tab_id: String(row.tab_id ?? ""),
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
export function listSiblings(agents, tabs, selfPane, selfTab) {
|
|
27
|
+
if (!selfPane && !selfTab)
|
|
28
|
+
return undefined;
|
|
29
|
+
const labels = new Map(tabs.map((t) => [t.tab_id, t.label]));
|
|
30
|
+
const parts = [];
|
|
31
|
+
for (const agent of agents) {
|
|
32
|
+
if (selfPane && agent.pane_id === selfPane)
|
|
33
|
+
continue;
|
|
34
|
+
if (!selfPane && selfTab && agent.tab_id === selfTab)
|
|
35
|
+
continue;
|
|
36
|
+
const name = labels.get(agent.tab_id) || agent.agent;
|
|
37
|
+
if (!name)
|
|
38
|
+
continue;
|
|
39
|
+
parts.push({ name, status: agent.agent_status });
|
|
40
|
+
}
|
|
41
|
+
if (parts.length === 0)
|
|
42
|
+
return undefined;
|
|
43
|
+
return parts;
|
|
44
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { loadPiMinimalConfig } from "./config.js";
|
|
3
|
+
import { installHeader } from "./header.js";
|
|
4
|
+
import { installFooter } from "./footer.js";
|
|
5
|
+
import { installCwd } from "./cwd.js";
|
|
6
|
+
import { listSiblings, parseAgentList, parseTabList } from "./herdr.js";
|
|
7
|
+
import { hideGauntletWidgets } from "./tracker.js";
|
|
8
|
+
import { stopLiveWatch, wrapBuiltinTools } from "./tools.js";
|
|
9
|
+
import { applyReclaim, reclaimPolicy } from "./reclaim.js";
|
|
10
|
+
import { installWorkingStatus } from "./working-status.js";
|
|
11
|
+
export default function piMinimalExtension(pi) {
|
|
12
|
+
const stopWorkingStatus = installWorkingStatus(pi);
|
|
13
|
+
let fastTimer;
|
|
14
|
+
let slowTimer;
|
|
15
|
+
let stealTimer;
|
|
16
|
+
let inFlight;
|
|
17
|
+
let siblings;
|
|
18
|
+
let generation = 0;
|
|
19
|
+
let hideWidgets = true;
|
|
20
|
+
const stopTimers = () => {
|
|
21
|
+
if (fastTimer)
|
|
22
|
+
clearInterval(fastTimer);
|
|
23
|
+
if (slowTimer)
|
|
24
|
+
clearInterval(slowTimer);
|
|
25
|
+
if (stealTimer)
|
|
26
|
+
clearInterval(stealTimer);
|
|
27
|
+
fastTimer = undefined;
|
|
28
|
+
slowTimer = undefined;
|
|
29
|
+
stealTimer = undefined;
|
|
30
|
+
inFlight?.abort();
|
|
31
|
+
inFlight = undefined;
|
|
32
|
+
};
|
|
33
|
+
const scheduleFast = (ctx) => {
|
|
34
|
+
if (fastTimer)
|
|
35
|
+
return;
|
|
36
|
+
if (slowTimer)
|
|
37
|
+
clearInterval(slowTimer);
|
|
38
|
+
slowTimer = undefined;
|
|
39
|
+
fastTimer = setInterval(() => {
|
|
40
|
+
void probe(ctx);
|
|
41
|
+
}, 2000);
|
|
42
|
+
};
|
|
43
|
+
const scheduleSlow = (ctx) => {
|
|
44
|
+
if (slowTimer)
|
|
45
|
+
return;
|
|
46
|
+
if (fastTimer)
|
|
47
|
+
clearInterval(fastTimer);
|
|
48
|
+
fastTimer = undefined;
|
|
49
|
+
slowTimer = setInterval(() => {
|
|
50
|
+
void probe(ctx);
|
|
51
|
+
}, 10000);
|
|
52
|
+
};
|
|
53
|
+
const probe = async (ctx) => {
|
|
54
|
+
if (process.env.HERDR_ENV !== "1")
|
|
55
|
+
return;
|
|
56
|
+
if (inFlight)
|
|
57
|
+
return;
|
|
58
|
+
const ac = new AbortController();
|
|
59
|
+
const gen = generation;
|
|
60
|
+
inFlight = ac;
|
|
61
|
+
try {
|
|
62
|
+
const agentRun = await pi.exec("herdr", ["agent", "list"], {
|
|
63
|
+
timeout: 3000,
|
|
64
|
+
signal: ac.signal,
|
|
65
|
+
});
|
|
66
|
+
if (generation !== gen)
|
|
67
|
+
return;
|
|
68
|
+
const tabRun = await pi.exec("herdr", ["tab", "list"], {
|
|
69
|
+
timeout: 3000,
|
|
70
|
+
signal: ac.signal,
|
|
71
|
+
});
|
|
72
|
+
if (generation !== gen)
|
|
73
|
+
return;
|
|
74
|
+
if (agentRun.code !== 0 || tabRun.code !== 0 || agentRun.killed || tabRun.killed) {
|
|
75
|
+
siblings = undefined;
|
|
76
|
+
scheduleSlow(ctx);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
siblings = listSiblings(parseAgentList(agentRun.stdout), parseTabList(tabRun.stdout), process.env.HERDR_PANE_ID, process.env.HERDR_TAB_ID);
|
|
80
|
+
if (siblings)
|
|
81
|
+
scheduleFast(ctx);
|
|
82
|
+
else
|
|
83
|
+
scheduleSlow(ctx);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
if (generation !== gen)
|
|
87
|
+
return;
|
|
88
|
+
siblings = undefined;
|
|
89
|
+
scheduleSlow(ctx);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
if (inFlight === ac)
|
|
93
|
+
inFlight = undefined;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const hideForeignChrome = (ctx, reason) => {
|
|
97
|
+
if (ctx.mode !== "tui")
|
|
98
|
+
return;
|
|
99
|
+
applyReclaim(reason, ctx.ui, {
|
|
100
|
+
widgets: () => {
|
|
101
|
+
if (hideWidgets)
|
|
102
|
+
hideGauntletWidgets(ctx);
|
|
103
|
+
},
|
|
104
|
+
chrome: () => {
|
|
105
|
+
installHeader(ctx);
|
|
106
|
+
installFooter(ctx, () => siblings);
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
const pinChrome = (ctx, reason) => {
|
|
111
|
+
hideForeignChrome(ctx, reason);
|
|
112
|
+
if (ctx.mode !== "tui" || !reclaimPolicy(reason).reinstallChrome)
|
|
113
|
+
return;
|
|
114
|
+
ctx.ui.setWidget("tw-cwd", undefined);
|
|
115
|
+
installCwd(ctx);
|
|
116
|
+
};
|
|
117
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
118
|
+
if (ctx.mode !== "tui")
|
|
119
|
+
return;
|
|
120
|
+
const config = loadPiMinimalConfig({
|
|
121
|
+
agentDir: getAgentDir(),
|
|
122
|
+
cwd: ctx.cwd,
|
|
123
|
+
projectTrusted: ctx.isProjectTrusted(),
|
|
124
|
+
});
|
|
125
|
+
hideWidgets = config.hideGauntletWidgets;
|
|
126
|
+
installHeader(ctx);
|
|
127
|
+
installFooter(ctx, () => siblings);
|
|
128
|
+
installCwd(ctx);
|
|
129
|
+
wrapBuiltinTools(pi, ctx);
|
|
130
|
+
pinChrome(ctx, "session_start");
|
|
131
|
+
ctx.ui.setHiddenThinkingLabel("◆ Thought");
|
|
132
|
+
ctx.ui.setWorkingIndicator();
|
|
133
|
+
setTimeout(() => pinChrome(ctx, "session_settle"), 0);
|
|
134
|
+
setTimeout(() => pinChrome(ctx, "session_settle"), 50);
|
|
135
|
+
if (!stealTimer) {
|
|
136
|
+
stealTimer = setInterval(() => hideForeignChrome(ctx, "interval"), 200);
|
|
137
|
+
}
|
|
138
|
+
if (process.env.HERDR_ENV === "1") {
|
|
139
|
+
await probe(ctx);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
pi.on("turn_end", (_event, ctx) => {
|
|
143
|
+
pinChrome(ctx, "turn_end");
|
|
144
|
+
});
|
|
145
|
+
pi.on("tool_execution_end", (_event, ctx) => {
|
|
146
|
+
pinChrome(ctx, "tool_execution_end");
|
|
147
|
+
});
|
|
148
|
+
pi.on("session_shutdown", async () => {
|
|
149
|
+
stopTimers();
|
|
150
|
+
stopWorkingStatus();
|
|
151
|
+
stopLiveWatch();
|
|
152
|
+
generation += 1;
|
|
153
|
+
siblings = undefined;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export function createLiveWatch(intervalMs = 400) {
|
|
2
|
+
let frame = 0;
|
|
3
|
+
let timer;
|
|
4
|
+
const invalidators = new Map();
|
|
5
|
+
const tick = () => {
|
|
6
|
+
frame = frame === 0 ? 1 : 0;
|
|
7
|
+
const snapshot = [...invalidators.values()];
|
|
8
|
+
for (const invalidate of snapshot) {
|
|
9
|
+
try {
|
|
10
|
+
invalidate();
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// A single renderer failure must not stop the shared activity frame.
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
watch(id, invalidate) {
|
|
19
|
+
if (!id)
|
|
20
|
+
return;
|
|
21
|
+
invalidators.set(id, invalidate);
|
|
22
|
+
if (!timer)
|
|
23
|
+
timer = setInterval(tick, intervalMs);
|
|
24
|
+
},
|
|
25
|
+
unwatch(id) {
|
|
26
|
+
if (!id)
|
|
27
|
+
return;
|
|
28
|
+
invalidators.delete(id);
|
|
29
|
+
if (invalidators.size === 0 && timer) {
|
|
30
|
+
clearInterval(timer);
|
|
31
|
+
timer = undefined;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
has(id) {
|
|
35
|
+
return Boolean(id) && invalidators.has(id);
|
|
36
|
+
},
|
|
37
|
+
stop() {
|
|
38
|
+
if (timer)
|
|
39
|
+
clearInterval(timer);
|
|
40
|
+
timer = undefined;
|
|
41
|
+
invalidators.clear();
|
|
42
|
+
frame = 0;
|
|
43
|
+
},
|
|
44
|
+
tick,
|
|
45
|
+
get frame() {
|
|
46
|
+
return frame;
|
|
47
|
+
},
|
|
48
|
+
get size() {
|
|
49
|
+
return invalidators.size;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
package/dist/reclaim.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function reclaimPolicy(reason) {
|
|
2
|
+
if (reason === "session_start") {
|
|
3
|
+
return { resetEditor: true, reinstallChrome: true };
|
|
4
|
+
}
|
|
5
|
+
if (reason === "interval") {
|
|
6
|
+
return { resetEditor: false, reinstallChrome: false };
|
|
7
|
+
}
|
|
8
|
+
// session_settle / turn_end / tool_execution_end: restore chrome, keep editor
|
|
9
|
+
return { resetEditor: false, reinstallChrome: true };
|
|
10
|
+
}
|
|
11
|
+
export function applyReclaim(reason, ui, install) {
|
|
12
|
+
const policy = reclaimPolicy(reason);
|
|
13
|
+
install.widgets();
|
|
14
|
+
if (policy.resetEditor)
|
|
15
|
+
ui.setEditorComponent(undefined);
|
|
16
|
+
if (policy.reinstallChrome)
|
|
17
|
+
install.chrome();
|
|
18
|
+
return policy;
|
|
19
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
let activityGroup;
|
|
2
|
+
export function resetActivityGroup() {
|
|
3
|
+
activityGroup = undefined;
|
|
4
|
+
}
|
|
5
|
+
export function shortName(path) {
|
|
6
|
+
const parts = path.split("/").filter(Boolean);
|
|
7
|
+
if (parts.length <= 1)
|
|
8
|
+
return path || "";
|
|
9
|
+
return parts.slice(-2).join("/");
|
|
10
|
+
}
|
|
11
|
+
export function toolItem(kind, args) {
|
|
12
|
+
if (kind === "bash") {
|
|
13
|
+
const command = typeof args?.command === "string" ? args.command.split("\n")[0]?.trim() ?? "" : "";
|
|
14
|
+
return command || "bash";
|
|
15
|
+
}
|
|
16
|
+
if (kind === "grep" || kind === "find") {
|
|
17
|
+
return typeof args?.pattern === "string" && args.pattern ? args.pattern : kind;
|
|
18
|
+
}
|
|
19
|
+
const path = (typeof args?.path === "string" && args.path) ||
|
|
20
|
+
(typeof args?.file_path === "string" && args.file_path) ||
|
|
21
|
+
(typeof args?.target_directory === "string" && args.target_directory) ||
|
|
22
|
+
"";
|
|
23
|
+
return path ? shortName(path) : kind;
|
|
24
|
+
}
|
|
25
|
+
export function nextGroupedRow(verb, item, id) {
|
|
26
|
+
if (activityGroup && activityGroup.verb === verb) {
|
|
27
|
+
activityGroup.seen.add(id);
|
|
28
|
+
if (id === activityGroup.leadId)
|
|
29
|
+
return { header: verb, item };
|
|
30
|
+
return { header: undefined, item };
|
|
31
|
+
}
|
|
32
|
+
activityGroup = { verb, leadId: id, seen: new Set([id]) };
|
|
33
|
+
return { header: verb, item };
|
|
34
|
+
}
|