@openlines/opl-pi-sht 0.1.21 → 0.2.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 +8 -4
- package/configs/opl-modes.json.sample +24 -28
- package/extensions/opl-browser/browser.ts +4 -4
- package/extensions/opl-footer/README.md +9 -2
- package/extensions/opl-footer/git-status.ts +42 -0
- package/extensions/opl-footer/index.ts +26 -8
- package/extensions/opl-footer/segments/cost.ts +2 -2
- package/extensions/opl-footer/segments/helpers.ts +0 -2
- package/extensions/opl-footer/segments/mode-switcher.ts +4 -2
- package/extensions/opl-footer/theme.ts +24 -5
- package/extensions/opl-init/README.md +23 -9
- package/extensions/opl-init/index.ts +38 -124
- package/extensions/opl-input/README.md +5 -3
- package/extensions/opl-input/index.ts +14 -7
- package/extensions/opl-input/mode-style.ts +1 -1
- package/extensions/opl-input/utils.ts +16 -5
- package/extensions/opl-modes/README.md +19 -13
- package/extensions/opl-modes/config.ts +64 -16
- package/extensions/opl-modes/index.ts +156 -70
- package/extensions/opl-modes/state.ts +40 -18
- package/extensions/opl-modes/types.ts +2 -0
- package/extensions/opl-modes/utils.ts +123 -12
- package/extensions/opl-simplebench/benchmark.ts +24 -7
- package/extensions/opl-simplebench/index.ts +8 -5
- package/extensions/opl-simplebench/research.ts +4 -1
- package/extensions/opl-simplebench/util/providers.ts +6 -1
- package/extensions/opl-todo/index.ts +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# opl-pi-sht
|
|
2
2
|
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
3
5
|
[](https://github.com/linellazatin/opl-pi-sht)
|
|
4
6
|
[](https://github.com/linellazatin/opl-pi-sht)
|
|
5
7
|
[](https://www.npmjs.com/package/@openlines/opl-pi-sht)
|
|
6
|
-
[](https://www.npmjs.com/package/@openlines/opl-pi-sht)
|
|
7
9
|
[](./LICENSE)
|
|
8
10
|
|
|
11
|
+
</div>
|
|
12
|
+
|
|
9
13
|
**Cut token cost, run the agent safely, and drop your MCP servers.**
|
|
10
14
|
|
|
11
15
|
A portable collection of various Pi coding agent extensions. Repository directories and config files use `opl-`; established Pi-facing commands and tool names stay compatible.
|
|
@@ -67,7 +71,7 @@ Copy mode overwrites matching destinations. Link mode skips existing destination
|
|
|
67
71
|
|
|
68
72
|
| Extension | Summary | Commands, tools, and configuration |
|
|
69
73
|
| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
70
|
-
| [`opl-init`](extensions/opl-init/README.md) | Fingerprinted repository-guide generator. | `/init`; no config. |
|
|
74
|
+
| [`opl-init`](extensions/opl-init/README.md) | Fingerprinted repository-guide generator. | `/init [--refine]`; no config. |
|
|
71
75
|
| [`opl-simplebench`](extensions/opl-simplebench/README.md) | Auditable provider-aware model benchmark with JSON artifacts and metrics. | `/simplebench`, `simplebench`; supports Ollama, OpenAI-compatible providers, and Bedrock; optional `opl-simplebench.json`. |
|
|
72
76
|
| [`opl-webaccess`](extensions/opl-webaccess/README.md) | Search plus readable URL/PDF retrieval with session recovery. | `web_search`, `fetch_content`, `get_search_content`; `opl-webaccess.json`. |
|
|
73
77
|
| [`opl-browser`](extensions/opl-browser/README.md) | Chromium automation via Playwright with structured extraction of rendered pages; single dispatcher tool replacing the chrome-devtools MCP. | `browser` (action-based); `opl-browser.json`. |
|
|
@@ -100,14 +104,14 @@ Cold prompt-cache write, measured /init session (opl-modes lazy tools + MCP adap
|
|
|
100
104
|
|
|
101
105
|
### Run the agent without babysitting it
|
|
102
106
|
|
|
103
|
-
- **`opl-modes`** chat and plan modes swap the active toolset for read-only lists and gate Bash to safe inspection patterns
|
|
107
|
+
- **`opl-modes`** chat and plan modes swap the active toolset for read-only lists and gate Bash to safe inspection patterns **per shell segment**, so `cat f && node -e '...'`, `cat x & rm -rf /tmp/x`, and `echo "$(node -e ...)"` can no longer ride the first command's allowance. Destructive checks still fire inside otherwise-safe commands (anchored to command position, so `du -sh` and `find . -name '*.sh'` stay allowed; `find -delete`, `find -fprint`, `git log --output`, `sort -o`, `npm audit fix`, `git clean`, `sudo`, and quote-obfuscated `r"m"`/`-del"ete"` are blocked; `env`/`printenv` are not safe-listed). The plan to execute lifecycle keeps exploration and mutation cleanly separated. Add custom modes (like below) for your workflow needs.
|
|
104
108
|

|
|
105
109
|
- `load_tools` activation is bounded by the current mode, so a read-only mode cannot be tricked into enabling a write-capable tool.
|
|
106
110
|
|
|
107
111
|
|
|
108
112
|
### Move through work faster
|
|
109
113
|
|
|
110
|
-
- **`opl-init`** writes a fingerprinted `AGENTS.md` so any agent starts already oriented in a repo. It
|
|
114
|
+
- **`opl-init`** deterministically writes a fingerprinted `AGENTS.md` so any agent starts already oriented in a repo. It refreshes only when the tree changes, overwriting a stale guide outright, and plain `/init` never sends a synthetic user message; `/init --refine` is the opt-in single request for a model-expanded guide, meant for session start or just before a push, not mid-task.
|
|
111
115
|

|
|
112
116
|
- **`opl-browser`** gives full Chromium automation (navigate, snapshot, extract rendered-page markdown, interact, screenshot, console/network capture, evaluate) through a single tool, with handle+preview output for large results — navigation is http(s)-only and screenshots stay in the project directory.
|
|
113
117
|
- **`opl-webaccess`** adds provider-backed search plus readable URL and PDF extraction, with session recovery of earlier results, an http(s)-only fetch, a 10 MB response cap, and a 30s timeout.
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"_comment_lazyTools": "Tool names withheld from the active set at rest and enabled on demand via the load_tools tool. Only affects modes that would otherwise include them (off/execute and custom modes that list them). Core built-ins, plan_complete, and load_tools are protected and ignored. Omit or leave empty to keep all tools always active.",
|
|
18
18
|
"lazyTools": [],
|
|
19
19
|
"bashPatterns": {
|
|
20
|
-
"_comment_safe": "Replace-only: providing bashPatterns.safePatterns replaces the built-in safe list. This is the default safe allowlist for every mode
|
|
21
|
-
"_comment_destructive": "Replace-only: providing bashPatterns.destructivePatterns replaces the built-in destructive list. This is the default destructive blocklist for every mode; a mode overrides it with its own destructivePatterns or opts out of Bash gating with unrestrictedBash. Shown here with the built-in list.",
|
|
20
|
+
"_comment_safe": "Replace-only: providing bashPatterns.safePatterns replaces the built-in safe list. This is the default safe allowlist for every mode and it is checked per shell segment (&&, ||, a lone &, |, ;, newline, $(...), backticks, <( and >( each start a segment), so every chained or backgrounded command must be allowed. Quote-aware: grep \\\"a|b\\\" stays one command, but a $(...) or backtick payload still runs inside double quotes, so it is split out and checked too. Interpreters and tools that can write files or exec programs from their own arguments (awk, sed, xargs, perl, python, env) are deliberately not listed. A mode overrides it with its own safePatterns or opts out of Bash gating with unrestrictedBash. Shown here with the built-in list.",
|
|
21
|
+
"_comment_destructive": "Replace-only: providing bashPatterns.destructivePatterns replaces the built-in destructive list. This is the default destructive blocklist for every mode; a mode overrides it with its own destructivePatterns or opts out of Bash gating with unrestrictedBash. Program-name entries are anchored to command position (match each segment start) so read-only commands that only mention rm/cp/sh in an argument stay allowed, while flag and redirect entries stay unanchored - that is how find -fprint, sort -o in any position, git log --output, npm audit fix, uniq's output operand, and a bare > file are caught even behind a safe command. Descriptor duplication (2>&1, >&2) and redirects into /dev/null are not writes and stay allowed. Shown here with the built-in list.",
|
|
22
22
|
"safePatterns": [
|
|
23
23
|
"^\\s*cat\\b",
|
|
24
24
|
"^\\s*head\\b",
|
|
@@ -34,6 +34,9 @@
|
|
|
34
34
|
"^\\s*printf\\b",
|
|
35
35
|
"^\\s*wc\\b",
|
|
36
36
|
"^\\s*sort\\b",
|
|
37
|
+
"^\\s*uniq\\b",
|
|
38
|
+
"^\\s*tr\\b",
|
|
39
|
+
"^\\s*cut\\b",
|
|
37
40
|
"^\\s*diff\\b",
|
|
38
41
|
"^\\s*file\\b",
|
|
39
42
|
"^\\s*stat\\b",
|
|
@@ -53,7 +56,7 @@
|
|
|
53
56
|
"^\\s*fd\\b",
|
|
54
57
|
"^\\s*bat\\b",
|
|
55
58
|
"^\\s*jq\\b",
|
|
56
|
-
"^\\s*git\\s+(status|log|diff|show|branch|remote)",
|
|
59
|
+
"^\\s*git\\s+(status|log|diff|show|branch|remote|rev-parse)",
|
|
57
60
|
"^\\s*node\\s+--version",
|
|
58
61
|
"^\\s*python\\s+--version",
|
|
59
62
|
"^\\s*(npx\\s+)?tsc\\b.*--noEmit",
|
|
@@ -61,36 +64,29 @@
|
|
|
61
64
|
"^\\s*yarn\\s+(list|info|why|audit)"
|
|
62
65
|
],
|
|
63
66
|
"destructivePatterns": [
|
|
64
|
-
"\\
|
|
65
|
-
"\\
|
|
66
|
-
"\\
|
|
67
|
-
"\\
|
|
68
|
-
"\\
|
|
69
|
-
"\\
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"\\
|
|
73
|
-
"\\
|
|
74
|
-
"\\
|
|
75
|
-
"\\btruncate\\b",
|
|
76
|
-
"\\s-(delete|exec|execdir)\\b",
|
|
77
|
-
"(^|[^<])>(?!>|&)",
|
|
78
|
-
">>",
|
|
79
|
-
"\\bnpm\\s+(install|uninstall|update|ci)",
|
|
67
|
+
"^\\s*(rm|rmdir|mv|cp|mkdir|touch|chmod|chown|tee|dd|shred|truncate)\\b",
|
|
68
|
+
"\\s-{1,2}(delete|exec|execdir|exec-batch|fprint0?|fprintf|fls|output)\\b",
|
|
69
|
+
"^\\s*fd\\b[^\\n]*\\s-[A-Za-z]*[xX]\\b",
|
|
70
|
+
"^\\s*rg\\b[^\\n]*\\s--pre\\b",
|
|
71
|
+
"^\\s*(sort|tree)\\b[^\\n]*\\s-[A-Za-z]*o\\b",
|
|
72
|
+
"^\\s*jq\\b[^\\n]*(\\$ENV|\\benv\\b)",
|
|
73
|
+
"(^|[^<])>(?!>)(?!&[0-9-])(?! *\\/dev\\/null(?![^\\s;&|)]))",
|
|
74
|
+
">>(?! *\\/dev\\/null(?![^\\s;&|)]))",
|
|
75
|
+
"^\\s*uniq\\b(?:\\s+-\\S+)*\\s+[^-\\s]\\S*(?:\\s+[^-\\s]\\S*)+",
|
|
76
|
+
"\\bnpm\\s+(install|uninstall|update|upgrade|ci)",
|
|
77
|
+
"\\bnpm\\s+audit\\b.*\\bfix\\b",
|
|
80
78
|
"\\byarn\\s+(add|remove|install)",
|
|
81
79
|
"\\bpip\\s+(install|uninstall)",
|
|
82
|
-
"\\bgit\\s+(add|commit|push|merge|rebase|reset|checkout|
|
|
83
|
-
"\\
|
|
84
|
-
"\\
|
|
85
|
-
"\\
|
|
86
|
-
"\\
|
|
87
|
-
"\\b(sh|bash|zsh)\\b",
|
|
88
|
-
"\\b(vim?|nano|emacs|code|subl)\\b"
|
|
80
|
+
"\\bgit\\s+(add|commit|push|merge|rebase|reset|checkout|clean|update-ref|cherry-pick|revert|am|apply|branch\\s+(-{1,2}[dDmMcC]|--(unset-upstream|edit-description))|tag\\s+-)",
|
|
81
|
+
"\\bgit\\s+remote\\s+(add|remove|rename|set-url|set-head|setbranches|prune|update)\\b",
|
|
82
|
+
"^\\s*(sudo|su|kill|pkill)\\b",
|
|
83
|
+
"^\\s*(sh|bash|zsh)\\b",
|
|
84
|
+
"^\\s*(vim?|nano|emacs|code|subl)\\b"
|
|
89
85
|
]
|
|
90
86
|
},
|
|
91
87
|
"_comment_tools": "Set modes.chat.tools or modes.plan.tools to replace their default tools. The legacy top-level chatAllowedTools and planAllowedTools keys remain supported but are deprecated.",
|
|
92
88
|
"_comment_widgetColor": "available colors - accent, border, borderAccent, borderMuted, success, error, warning, muted, dim, text, thinkingText, searchMatchText, userMessageText, customMessageText, customMessageLabel, toolTitle, toolOutput, mdHeading, mdLink, mdLinkUrl, mdCode, mdCodeBlock, mdCodeBlockBorder, mdQuote, mdQuoteBorder, mdHr, mdListBullet, toolDiffAdded, toolDiffRemoved, toolDiffContext, syntaxComment, syntaxKeyword, syntaxFunction, syntaxVariable, syntaxString, syntaxNumber, syntaxType, syntaxOperator, syntaxPunctuation, thinkingOff, thinkingMinimal, thinkingLow, thinkingMedium, thinkingHigh, thinkingXhigh, thinkingMax, bashMode",
|
|
93
|
-
"_comment_appearance": "Every mode owns appearance here. prefix/prefixColor/borderColor style opl-input; modeColor styles opl-footer's Mode value. All color values accept Pi theme tokens or #
|
|
89
|
+
"_comment_appearance": "Every mode owns appearance here. prefix/prefixColor/borderColor style opl-input; modeColor styles opl-footer's Mode value. All color values accept Pi theme tokens, #RRGGBB, or the #abc shorthand.",
|
|
94
90
|
"_comment_modePatterns": "Per-mode safePatterns/destructivePatterns must be arrays of valid regex strings. [] explicitly removes that policy; any malformed non-empty override is ignored and inherits the existing policy.",
|
|
95
91
|
"modes": {
|
|
96
92
|
"off": {
|
|
@@ -252,7 +248,7 @@
|
|
|
252
248
|
"research": {
|
|
253
249
|
"enabled": true,
|
|
254
250
|
"allowExecute": false,
|
|
255
|
-
"prompt": "**SUPERSEDES ALL OTHER BEHAVIOR INSTRUCTIONS.** This overrides any role or style directives (e.g. caveman, roleplay, tone modifiers). The constraints below take absolute priority.\n\nYou are in RESEARCH MODE. Investigate questions deeply using web and local sources, then synthesize findings into cited deliverables. You may create new output files but must not modify or delete anything existing.\n\n## 1. Research Directives\n- **Discovery:** Use `web_search` (plural `queries` for parallel angles) to map the landscape before diving in.\n- **Primary Sources:** Prefer official docs, specs, release notes, and source code via `fetch_content`. Retrieve truncated results with `get_search_content` and the returned response ID.\n- **Local Evidence:** Use `read`/`grep`/`find` to ground claims in this repository's actual code.\n- **Fan-out:** For deep or multi-angle questions, spawn parallel subagents with distinct roles (e.g. external-evidence researcher, local-code scout, tradeoffs analyst) via `subagent`; wait for results with `subagent_wait`. Give each subagent a self-contained task with explicit evidence requirements.\n- **No Invention:** Never present an unverified claim as fact. Mark confidence per finding (high/medium/low) and state gaps explicitly.\n\n## 2. Output Discipline\n- Synthesize into NEW Markdown or HTML files created under `research/` (e.g. `research/<topic>.md`). Never modify, move, or overwrite existing files.\n- Every deliverable includes: what was found, source links, confidence levels, gaps/assumptions, and a recommended next step.\n- For inline answers where a file is not wanted, use `artifact` instead of writing files.\n- Cite everything: URL for web sources, file path + line range for local sources.",
|
|
251
|
+
"prompt": "**SUPERSEDES ALL OTHER BEHAVIOR INSTRUCTIONS.** This overrides any role or style directives (e.g. caveman, roleplay, tone modifiers). The constraints below take absolute priority.\n\nYou are in RESEARCH MODE. Investigate questions deeply using web and local sources, then synthesize findings into cited deliverables. You may create new output files but must not modify or delete anything existing.\n\n## 1. Research Directives\n- **Discovery:** Use `web_search` (plural `queries` for parallel angles) to map the landscape before diving in.\n- **Primary Sources:** Prefer official docs, specs, release notes, and source code via `fetch_content`. Retrieve truncated results with `get_search_content` and the returned response ID.\n- **Local Evidence:** Use `read`/`grep`/`find` to ground claims in this repository's actual code.\n- **Lazy tools:** If `subagent`, `subagent_wait`, or `browser` is not in your active tool set, call `load_tools` first to enable it, then use it.\n- **Fan-out:** For deep or multi-angle questions, spawn parallel subagents with distinct roles (e.g. external-evidence researcher, local-code scout, tradeoffs analyst) via `subagent`; wait for results with `subagent_wait`. Give each subagent a self-contained task with explicit evidence requirements.\n- **No Invention:** Never present an unverified claim as fact. Mark confidence per finding (high/medium/low) and state gaps explicitly.\n\n## 2. Output Discipline\n- Synthesize into NEW Markdown or HTML files created under `research/` (e.g. `research/<topic>.md`). Never modify, move, or overwrite existing files.\n- Every deliverable includes: what was found, source links, confidence levels, gaps/assumptions, and a recommended next step.\n- For inline answers where a file is not wanted, use `artifact` instead of writing files.\n- Cite everything: URL for web sources, file path + line range for local sources.",
|
|
256
252
|
"model": {
|
|
257
253
|
"provider": "amazon-bedrock",
|
|
258
254
|
"id": "zai.glm-5"
|
|
@@ -90,11 +90,11 @@ export async function runAction(p: BrowserParams, cfg: BrowserConfig): Promise<B
|
|
|
90
90
|
return { text: tree || "(empty snapshot)" };
|
|
91
91
|
}
|
|
92
92
|
case "extract": {
|
|
93
|
-
const
|
|
94
|
-
const html =
|
|
95
|
-
? await page().locator(
|
|
93
|
+
const selector = p.selector;
|
|
94
|
+
const html = selector
|
|
95
|
+
? await page().locator(selector).evaluate((el: Element) => el.outerHTML)
|
|
96
96
|
: await page().content();
|
|
97
|
-
const { markdown } = extractMarkdown(html, { raw:
|
|
97
|
+
const { markdown } = extractMarkdown(html, { raw: Boolean(selector) });
|
|
98
98
|
return { text: markdown || "(empty extraction)" };
|
|
99
99
|
}
|
|
100
100
|
case "screenshot": {
|
|
@@ -43,6 +43,13 @@ Create `~/.pi/agent/configs/opl-footer.json` or copy the tracked example from [`
|
|
|
43
43
|
|
|
44
44
|
The config is cached for five seconds. Changes normally appear automatically; use `/reload` or restart Pi if needed.
|
|
45
45
|
|
|
46
|
+
### Git probing
|
|
47
|
+
|
|
48
|
+
The `git` segment reads branch (500 ms cache) and dirty counts (1 s cache) so an idle footer never spawns more than a couple of `git` processes per second. Two guards keep that floor honest:
|
|
49
|
+
|
|
50
|
+
- The probes are skipped entirely when no configured row contains `git`, so a layout without the segment pays nothing for it.
|
|
51
|
+
- In a directory that is not a repository, the first failed probe arms a 30 second back-off instead of re-running `git` every second forever. A mid-session `git init` or `git clone` clears it immediately (matched on tool results), as does any `write`/`edit` invalidation; a probe that fails inside a real repository (for example a locked index) is confirmed with `git rev-parse --is-inside-work-tree` and keeps the normal one-second cadence. A probe that finishes after an invalidation is dropped outright, so the failure that happened *before* a `git init` cannot push the fresh repository back into a 30 second back-off.
|
|
52
|
+
|
|
46
53
|
```json
|
|
47
54
|
{
|
|
48
55
|
"row1LeftSegments": ["pi", "separator", "model", "separator", "path", "git"],
|
|
@@ -54,7 +61,7 @@ The config is cached for five seconds. Changes normally appear automatically; us
|
|
|
54
61
|
}
|
|
55
62
|
```
|
|
56
63
|
|
|
57
|
-
See the tracked [`configs/opl-footer.json.sample`](../../configs/opl-footer.json.sample) for a complete example. Segment IDs, color fields, context-bar options, thinking-level colors, and icon overrides are documented below. Colors accept Pi theme tokens or hex
|
|
64
|
+
See the tracked [`configs/opl-footer.json.sample`](../../configs/opl-footer.json.sample) for a complete example. Segment IDs, color fields, context-bar options, thinking-level colors, and icon overrides are documented below. Colors accept Pi theme tokens, six-digit hex, or the three-digit `#abc` shorthand (expanded to `#aabbcc`); the context bar gradient resolves both forms to RGB. An unknown token or malformed hex renders that text uncolored instead of failing the footer render, so a typo in `colors` or in `opl-modes`' `appearance.modeColor` costs you a color, not the footer.
|
|
58
65
|
|
|
59
66
|
## Architecture
|
|
60
67
|
|
|
@@ -69,7 +76,7 @@ See the tracked [`configs/opl-footer.json.sample`](../../configs/opl-footer.json
|
|
|
69
76
|
| `path` | Current working directory | `segmentOptions.path.mode`: `"full"` (default) · `"abbreviated"` · `"basename"` |
|
|
70
77
|
| `git` | Git branch and dirty indicators | `showBranch`, `showStaged`, `showUnstaged`, `showUntracked` (all bool) |
|
|
71
78
|
| `context_pct` | Gradient bar + `X.X%` + max tokens | Bar fully configurable via `segmentOptions.contextBar` (see below). % and max tokens use `contextLabel` colour. Max tokens formatted with K/M suffix (e.g. `128k`, `2M`). Set `DEBUG_PCT` in `context.ts` to a number (0–100) to pin the bar at a fixed value for visual testing. |
|
|
72
|
-
| `cost` | `$<amount>` | `$` dim, amount in `cost` colour (`muted` by default). Shows dim `(no pricing)` when the session total is zero on a non-local model (provider has no pricing configured), and `(local model)` for local models |
|
|
79
|
+
| `cost` | `$<amount>` (4 decimals, e.g. `$0.0123`) | `$` dim, amount in `cost` colour (`muted` by default). Four decimals keep cheap local or short sessions distinguishable instead of pinning at `$0.00`. Shows dim `(no pricing)` when the session total is zero on a non-local model (provider has no pricing configured), and `(local model)` for local models |
|
|
73
80
|
| `thinking` | `Thinking: <LEVEL>` | Dim label, CAPS level with per-level colour; always visible |
|
|
74
81
|
| `mode_switcher` | Unified active mode label | Reads state published by `opl-modes`; `appearance.modeColor` controls the mode value, with hardcoded `muted` fallback |
|
|
75
82
|
| `caveman` | `Caveman mode: <MODE>` | Hidden when caveman extension not loaded |
|
|
@@ -15,12 +15,34 @@ interface CachedBranch {
|
|
|
15
15
|
|
|
16
16
|
const CACHE_TTL_MS = 1000;
|
|
17
17
|
const BRANCH_TTL_MS = 500;
|
|
18
|
+
// Outside a repository git exits non-zero on every probe. Arm a long back-off on the first
|
|
19
|
+
// failure so an idle footer stops spawning git once per second, and clear it when
|
|
20
|
+
// `rev-parse --is-inside-work-tree` proves the failure was transient — or when `git init`
|
|
21
|
+
// happens mid-session and a cache invalidation/`git init` match clears it.
|
|
22
|
+
const NOT_A_REPO_TTL_MS = 30_000;
|
|
18
23
|
let cachedStatus: CachedGitStatus | null = null;
|
|
19
24
|
let cachedBranch: CachedBranch | null = null;
|
|
20
25
|
let pendingFetch: Promise<void> | null = null;
|
|
21
26
|
let pendingBranchFetch: Promise<void> | null = null;
|
|
22
27
|
let invalidationCounter = 0;
|
|
23
28
|
let branchInvalidationCounter = 0;
|
|
29
|
+
let repoAbsentUntil = 0;
|
|
30
|
+
let repoCheckInFlight = false;
|
|
31
|
+
|
|
32
|
+
/** Assume "not a repo" immediately, then confirm asynchronously so a real repo recovers. */
|
|
33
|
+
function noteProbeFailure(): void {
|
|
34
|
+
repoAbsentUntil = Date.now() + NOT_A_REPO_TTL_MS;
|
|
35
|
+
if (repoCheckInFlight) return;
|
|
36
|
+
repoCheckInFlight = true;
|
|
37
|
+
void runGit(["rev-parse", "--is-inside-work-tree"]).then((out) => {
|
|
38
|
+
if (out === "true") repoAbsentUntil = 0;
|
|
39
|
+
repoCheckInFlight = false;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function repoAbsent(): boolean {
|
|
44
|
+
return Date.now() < repoAbsentUntil;
|
|
45
|
+
}
|
|
24
46
|
|
|
25
47
|
function parseGitStatusOutput(output: string): { staged: number; unstaged: number; untracked: number } {
|
|
26
48
|
let staged = 0;
|
|
@@ -102,6 +124,10 @@ async function fetchGitStatus(): Promise<{ staged: number; unstaged: number; unt
|
|
|
102
124
|
export function getCurrentBranch(providerBranch: string | null): string | null {
|
|
103
125
|
const now = Date.now();
|
|
104
126
|
|
|
127
|
+
if (now < repoAbsentUntil) {
|
|
128
|
+
return cachedBranch ? cachedBranch.branch : providerBranch;
|
|
129
|
+
}
|
|
130
|
+
|
|
105
131
|
if (cachedBranch && now - cachedBranch.timestamp < BRANCH_TTL_MS) {
|
|
106
132
|
return cachedBranch.branch;
|
|
107
133
|
}
|
|
@@ -109,7 +135,10 @@ export function getCurrentBranch(providerBranch: string | null): string | null {
|
|
|
109
135
|
if (!pendingBranchFetch) {
|
|
110
136
|
const fetchId = branchInvalidationCounter;
|
|
111
137
|
pendingBranchFetch = fetchGitBranch().then((result) => {
|
|
138
|
+
// Everything below belongs to the pre-invalidation directory state: a probe that
|
|
139
|
+
// started before `git init` must not re-arm the back-off or publish a null branch.
|
|
112
140
|
if (fetchId === branchInvalidationCounter) {
|
|
141
|
+
if (result === null) noteProbeFailure();
|
|
113
142
|
cachedBranch = {
|
|
114
143
|
branch: result,
|
|
115
144
|
timestamp: Date.now(),
|
|
@@ -124,6 +153,14 @@ export function getCurrentBranch(providerBranch: string | null): string | null {
|
|
|
124
153
|
|
|
125
154
|
export function getGitStatus(providerBranch: string | null): GitStatus {
|
|
126
155
|
const now = Date.now();
|
|
156
|
+
|
|
157
|
+
if (now < repoAbsentUntil) {
|
|
158
|
+
const branch = cachedBranch ? cachedBranch.branch : providerBranch;
|
|
159
|
+
return cachedStatus
|
|
160
|
+
? { branch, staged: cachedStatus.staged, unstaged: cachedStatus.unstaged, untracked: cachedStatus.untracked }
|
|
161
|
+
: { branch, staged: 0, unstaged: 0, untracked: 0 };
|
|
162
|
+
}
|
|
163
|
+
|
|
127
164
|
const branch = getCurrentBranch(providerBranch);
|
|
128
165
|
|
|
129
166
|
if (cachedStatus && now - cachedStatus.timestamp < CACHE_TTL_MS) {
|
|
@@ -138,7 +175,10 @@ export function getGitStatus(providerBranch: string | null): GitStatus {
|
|
|
138
175
|
if (!pendingFetch) {
|
|
139
176
|
const fetchId = invalidationCounter;
|
|
140
177
|
pendingFetch = fetchGitStatus().then((result) => {
|
|
178
|
+
// Same as the branch probe: a failure from before an invalidation is stale, and
|
|
179
|
+
// arming the 30 s not-a-repo back-off from it would hide a just-created repository.
|
|
141
180
|
if (fetchId === invalidationCounter) {
|
|
181
|
+
if (result === null) noteProbeFailure();
|
|
142
182
|
cachedStatus = result
|
|
143
183
|
? { staged: result.staged, unstaged: result.unstaged, untracked: result.untracked, timestamp: Date.now() }
|
|
144
184
|
: { staged: 0, unstaged: 0, untracked: 0, timestamp: Date.now() };
|
|
@@ -162,9 +202,11 @@ export function getGitStatus(providerBranch: string | null): GitStatus {
|
|
|
162
202
|
export function invalidateGitStatus(): void {
|
|
163
203
|
cachedStatus = null;
|
|
164
204
|
invalidationCounter++;
|
|
205
|
+
repoAbsentUntil = 0;
|
|
165
206
|
}
|
|
166
207
|
|
|
167
208
|
export function invalidateGitBranch(): void {
|
|
168
209
|
cachedBranch = null;
|
|
169
210
|
branchInvalidationCounter++;
|
|
211
|
+
repoAbsentUntil = 0;
|
|
170
212
|
}
|
|
@@ -13,10 +13,21 @@ import { getDefaultColors, fg } from "./theme.js";
|
|
|
13
13
|
import { showFooterConfigurator } from "./configure.js";
|
|
14
14
|
|
|
15
15
|
const GIT_BRANCH_PATTERNS: RegExp[] = [
|
|
16
|
-
|
|
16
|
+
// init/clone included: creating a repo mid-session must clear the not-a-repo back-off.
|
|
17
|
+
/\bgit\s+(init|clone|checkout|switch|branch\s+-[dDmM]|merge|rebase|pull|reset|worktree)/,
|
|
17
18
|
/\bgit\s+stash\s+(pop|apply)/,
|
|
18
19
|
];
|
|
19
20
|
|
|
21
|
+
/** Row keys whose configured segments decide whether git must be probed at all. */
|
|
22
|
+
const GIT_LAYOUT_ROWS = [
|
|
23
|
+
"row1LeftSegments",
|
|
24
|
+
"row1RightSegments",
|
|
25
|
+
"row2LeftSegments",
|
|
26
|
+
"row2RightSegments",
|
|
27
|
+
"row3LeftSegments",
|
|
28
|
+
"row3RightSegments",
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
20
31
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
21
32
|
// Status Line Builder
|
|
22
33
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -260,9 +271,12 @@ export default function footer(pi: ExtensionAPI) {
|
|
|
260
271
|
|
|
261
272
|
const isThinkingEvent = (e: SessionEvent): e is ThinkingLevelEvent =>
|
|
262
273
|
e.type === "thinking_level_change";
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
274
|
+
const thinkingEvents = branch.filter(isThinkingEvent);
|
|
275
|
+
// Seed null, not "off": a truthy seed made the ?? fallback below unreachable, so a
|
|
276
|
+
// branch with no thinking_level_change entry rendered "off" instead of Pi's real level.
|
|
277
|
+
const thinkingLevelFromSession = thinkingEvents.length
|
|
278
|
+
? thinkingEvents.reduce((_, e) => e.thinkingLevel ?? "off", "off")
|
|
279
|
+
: null;
|
|
266
280
|
|
|
267
281
|
const lastAssistant = completedMessages.at(-1);
|
|
268
282
|
|
|
@@ -274,9 +288,13 @@ export default function footer(pi: ExtensionAPI) {
|
|
|
274
288
|
const contextWindow = ctx.model?.contextWindow || 0;
|
|
275
289
|
const contextPercent = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
|
|
276
290
|
|
|
277
|
-
// Get git status (cached)
|
|
278
|
-
|
|
279
|
-
const
|
|
291
|
+
// Get git status (cached). Skip the probes entirely when no visible row renders the
|
|
292
|
+
// git segment — otherwise an unused cell keeps spawning git once per second.
|
|
293
|
+
const usesGit = GIT_LAYOUT_ROWS.some((row) => effectiveConfig[row]?.includes("git"));
|
|
294
|
+
const gitBranch = usesGit ? footerDataRef?.getGitBranch() ?? null : null;
|
|
295
|
+
const gitStatus = usesGit
|
|
296
|
+
? getGitStatus(gitBranch)
|
|
297
|
+
: { branch: null, staged: 0, unstaged: 0, untracked: 0 };
|
|
280
298
|
|
|
281
299
|
// Check if using OAuth subscription
|
|
282
300
|
const usingSubscription = ctx.model
|
|
@@ -303,7 +321,7 @@ export default function footer(pi: ExtensionAPI) {
|
|
|
303
321
|
return {
|
|
304
322
|
model: ctx.model,
|
|
305
323
|
isLocalModel,
|
|
306
|
-
thinkingLevel: thinkingLevelFromSession
|
|
324
|
+
thinkingLevel: thinkingLevelFromSession ?? pi.getThinkingLevel(),
|
|
307
325
|
sessionId: ctx.sessionManager?.getSessionId?.(),
|
|
308
326
|
usageStats,
|
|
309
327
|
contextPercent,
|
|
@@ -9,12 +9,12 @@ export const costSegment = {
|
|
|
9
9
|
|
|
10
10
|
if (cost === 0 && !ctx.isLocalModel) {
|
|
11
11
|
return {
|
|
12
|
-
content: color(ctx, "cost", "$0.
|
|
12
|
+
content: color(ctx, "cost", "$0.0000") + applyColor(ctx.theme, "dim", " (no pricing)"),
|
|
13
13
|
visible: true,
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const content = color(ctx, "cost", `$${cost.toFixed(
|
|
17
|
+
const content = color(ctx, "cost", `$${cost.toFixed(4)}`);
|
|
18
18
|
|
|
19
19
|
if (ctx.isLocalModel) {
|
|
20
20
|
return { content: content + applyColor(ctx.theme, "dim", " (local model)"), visible: true };
|
|
@@ -3,9 +3,7 @@ import { fg } from "../theme.js";
|
|
|
3
3
|
|
|
4
4
|
export function formatTokens(n: number): string {
|
|
5
5
|
if (n < 1000) return n.toString();
|
|
6
|
-
if (n < 10000) return `${(n / 1000).toFixed(2)}k`;
|
|
7
6
|
if (n < 1000000) return `${(n / 1000).toFixed(2)}k`;
|
|
8
|
-
if (n < 10000000) return `${(n / 1000000).toFixed(2)}M`;
|
|
9
7
|
return `${(n / 1000000).toFixed(2)}M`;
|
|
10
8
|
}
|
|
11
9
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RenderedSegment, SegmentContext } from "../types.js";
|
|
1
|
+
import type { ColorValue, RenderedSegment, SegmentContext } from "../types.js";
|
|
2
2
|
import { applyColor } from "../theme.js";
|
|
3
3
|
|
|
4
4
|
interface AgentModeState {
|
|
@@ -18,7 +18,9 @@ export const modeSwitcherSegment = {
|
|
|
18
18
|
|
|
19
19
|
const label = applyColor(ctx.theme, "dim", "Mode:");
|
|
20
20
|
const value = mode === "off" ? "Normal" : mode.charAt(0).toUpperCase() + mode.slice(1);
|
|
21
|
-
|
|
21
|
+
// modeColor is authored in opl-modes config (theme token or hex); applyColor() renders an
|
|
22
|
+
// unknown value uncolored, so the cross-extension string is passed through as-is.
|
|
23
|
+
const valueStr = applyColor(ctx.theme, (state?.appearance?.modeColor ?? "muted") as ColorValue, value);
|
|
22
24
|
return { content: `${label} ${valueStr}`, visible: true };
|
|
23
25
|
},
|
|
24
26
|
};
|
|
@@ -31,7 +31,10 @@ function isHexColor(color: ColorValue): color is `#${string}` {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function hexToAnsi(hex: string): string {
|
|
34
|
-
|
|
34
|
+
// Accept the #abc shorthand so a three-digit value renders the same as #aabbcc
|
|
35
|
+
// instead of falling through as an (unknown) theme token.
|
|
36
|
+
let h = hex.replace("#", "");
|
|
37
|
+
if (/^[0-9a-fA-F]{3}$/.test(h)) h = h.split("").map((c) => c + c).join("");
|
|
35
38
|
if (!/^[0-9a-fA-F]{6}$/.test(h)) return "";
|
|
36
39
|
const r = parseInt(h.slice(0, 2), 16);
|
|
37
40
|
const g = parseInt(h.slice(2, 4), 16);
|
|
@@ -45,9 +48,19 @@ export function applyColor(
|
|
|
45
48
|
text: string
|
|
46
49
|
): string {
|
|
47
50
|
if (isHexColor(color)) {
|
|
48
|
-
|
|
51
|
+
// hexToAnsi() returns "" for a malformed value: emit no escape at all, not an empty
|
|
52
|
+
// color plus a stray reset.
|
|
53
|
+
const ansi = hexToAnsi(color);
|
|
54
|
+
if (!ansi) return text;
|
|
55
|
+
return `${ansi}${text}\x1b[0m`;
|
|
56
|
+
}
|
|
57
|
+
// theme.fg() throws on an unknown token; a bad color in opl-footer.json or in an
|
|
58
|
+
// opl-modes appearance must never take down the footer render.
|
|
59
|
+
try {
|
|
60
|
+
return theme.fg(color as ThemeColor, text);
|
|
61
|
+
} catch {
|
|
62
|
+
return text;
|
|
49
63
|
}
|
|
50
|
-
return theme.fg(color as ThemeColor, text);
|
|
51
64
|
}
|
|
52
65
|
|
|
53
66
|
export function fg(
|
|
@@ -102,11 +115,17 @@ export function resolveColorToRgb(
|
|
|
102
115
|
color: ColorValue
|
|
103
116
|
): { r: number; g: number; b: number } | null {
|
|
104
117
|
if (isHexColor(color)) {
|
|
105
|
-
|
|
118
|
+
let h = color.replace("#", "");
|
|
119
|
+
if (/^[0-9a-fA-F]{3}$/.test(h)) h = h.split("").map((c) => c + c).join("");
|
|
106
120
|
if (!/^[0-9a-fA-F]{6}$/.test(h)) return null;
|
|
107
121
|
return { r: parseInt(h.slice(0, 2), 16), g: parseInt(h.slice(2, 4), 16), b: parseInt(h.slice(4, 6), 16) };
|
|
108
122
|
}
|
|
109
|
-
|
|
123
|
+
let probed: string;
|
|
124
|
+
try {
|
|
125
|
+
probed = theme.fg(color as ThemeColor, "X");
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
110
129
|
const match = probed.match(/\x1b\[38;2;(\d+);(\d+);(\d+)m/);
|
|
111
130
|
if (!match) return null;
|
|
112
131
|
return { r: parseInt(match[1]), g: parseInt(match[2]), b: parseInt(match[3]) };
|
|
@@ -4,20 +4,34 @@ Generates or refreshes a repository-specific `AGENTS.md` guide through `/init`.
|
|
|
4
4
|
|
|
5
5
|
## Commands, flags, and shortcuts
|
|
6
6
|
|
|
7
|
-
- `/init`: crawl the current repository and
|
|
8
|
-
-
|
|
7
|
+
- `/init`: crawl the current repository and write `AGENTS.md` when its fingerprint is missing or stale. A stale guide is **overwritten** wholesale, hand-edited prose included; version control is the recovery path.
|
|
8
|
+
- `/init --refine`: same write, then one injected user turn asking the model to expand the guide into a fuller, evidence-based version. Opt-in only: plain `/init` never touches the transcript.
|
|
9
|
+
- No shortcuts.
|
|
9
10
|
|
|
10
11
|
## Extension features
|
|
11
12
|
|
|
12
|
-
- Produces
|
|
13
|
+
- Produces a deterministic repository guide with package commands and a file-type inventory.
|
|
13
14
|
- Crawls deterministically to depth 3, ignores generated/dependency directories, caps directory listings at 40 entries, and caps the rendered tree at 300 lines with explicit omission markers.
|
|
14
|
-
- Re-walks declared `pnpm-workspace.yaml` and Cargo workspace members with their own depth-3 budget
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
- Skips the model turn when the final `<!-- opl-init:fp <fingerprint> -->` marker is current.
|
|
15
|
+
- Re-walks declared `pnpm-workspace.yaml` and Cargo workspace members with their own depth-3 budget.
|
|
16
|
+
- Reads known manifests to include package scripts when present.
|
|
17
|
+
- Writes the deterministic fingerprinted `AGENTS.md` directly. It sends no synthetic user message on its own; `/init --refine` is the single, explicit request for a model-refined guide.
|
|
18
|
+
- Skips the write when the final `<!-- opl-init:fp <fingerprint> -->` marker is current.
|
|
19
19
|
- Excludes `AGENTS.md` itself from the Git status fingerprint, preventing the act of updating the guide from making it immediately stale.
|
|
20
20
|
|
|
21
|
+
## Session timing, reiterations, and re-injections
|
|
22
|
+
|
|
23
|
+
`/init` is an initialization command: run it at the **start** of a session, before the work begins, and again at the **very end** to fold the session's changes into the guide before pushing to the remote. Both are quiet moments, and `--refine` needs a quiet moment.
|
|
24
|
+
|
|
25
|
+
What a `--refine` injection does to a live session:
|
|
26
|
+
|
|
27
|
+
- It is a **user-role turn**. It is persisted in the session and replayed as context on every later turn and on resume, so two `--refine` runs in one session leave two instruction blocks behind and the transcript keeps paying for both.
|
|
28
|
+
- It **always triggers a turn**. With an idle agent the refinement starts immediately. With an agent already streaming it is queued as a follow-up (`deliverAs: "followUp"`), so the guide is rewritten *after* the current turn finishes — later than you asked, and possibly after you have moved on to something else. An injection with no `deliverAs` makes Pi throw `Agent is already processing` and the request is lost.
|
|
29
|
+
- It **cannot be fulfilled in a read-only mode**. Chat and plan modes carry no `write` tool, so the model can only describe the guide it was asked to write: the turn is spent and `AGENTS.md` stays at the deterministic baseline. Run `--refine` in normal (OFF) mode.
|
|
30
|
+
- In **execute mode it ends the plan**. `opl-modes` leaves execute mode when a turn completes without `plan_complete`, so the refinement turn finishing drops the session to OFF mid-plan.
|
|
31
|
+
- A refined guide is **not sticky**. The fingerprint is `HEAD` plus `git status`, so the next commit or dirty file makes the guide stale, and the next plain `/init` overwrites the refined prose with the baseline. Refine last, then commit `AGENTS.md`; if you refine and keep working, expect to refine once more at the end of the session.
|
|
32
|
+
- The refinement's own write does **not** invalidate the marker: `AGENTS.md` is filtered out of the status fingerprint, so a successful `--refine` leaves `/init` reporting "current".
|
|
33
|
+
- There is **no recursion and no automatic re-injection**. The message is sent with `expandPromptTemplates: false`, so a `/init` appearing inside it is not re-dispatched, and the extension registers no event handlers: every injection comes from you typing `--refine`.
|
|
34
|
+
|
|
21
35
|
## Architecture
|
|
22
36
|
|
|
23
|
-
`index.ts` owns crawling, fingerprinting,
|
|
37
|
+
`index.ts` owns crawling, fingerprinting, and deterministic guide writing. Git repositories fingerprint `HEAD` plus relevant `git status --porcelain=v1 --untracked-files=all` output, excluding `AGENTS.md` itself; non-Git repositories use sorted path, size, and mtime tuples. `/init` writes its guide locally and leaves the Pi transcript alone; `--refine` adds one small user message that points the model at the written file instead of re-sending the crawl.
|