@openlines/opl-pi-sht 0.1.20 → 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 CHANGED
@@ -1,11 +1,15 @@
1
1
  # opl-pi-sht
2
2
 
3
+ <div align="center">
4
+
3
5
  [![gh stars](https://img.shields.io/github/stars/linellazatin/opl-pi-sht?logo=github&color=ffffe0)](https://github.com/linellazatin/opl-pi-sht)
4
6
  [![gh release](https://img.shields.io/github/v/release/linellazatin/opl-pi-sht?display_name=release&logo=github&color=ffffe0)](https://github.com/linellazatin/opl-pi-sht)
5
7
  [![npm version](https://img.shields.io/npm/v/%40openlines%2Fopl-pi-sht?logo=npm&color=cb3837)](https://www.npmjs.com/package/@openlines/opl-pi-sht)
6
- [![npm downloads](https://img.shields.io/npm/dm/@openlines/opl-pi-sht?logo=npm&color=cb3837)](https://www.npmjs.com/package/@openlines/opl-pi-sht)
8
+ [![npm downloads](https://img.shields.io/npm/dt/@openlines/opl-pi-sht?logo=npm&color=cb3837)](https://www.npmjs.com/package/@openlines/opl-pi-sht)
7
9
  [![license](https://img.shields.io/npm/l/@openlines/opl-pi-sht)](./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, with destructive-pattern checks that fire even inside otherwise-safe commands (also `find -delete`, `git clean`, `truncate`, `sudo`, and quote-obfuscated `r"m"`; `env`/`printenv` no longer safe-listed). The plan to execute lifecycle keeps exploration and mutation cleanly separated. Add custom modes (like below) for your workflow needs.
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
  ![custom mode sample](images/ss-mode-custom.png)
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 writes a baseline before asking the model to refine the guide, so smaller models cannot leave the update only in chat; it re-runs only when the tree actually changes.
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
  ![init](images/ss-init.png)
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; 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. 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
- "\\brm\\b",
65
- "\\brmdir\\b",
66
- "\\bmv\\b",
67
- "\\bcp\\b",
68
- "\\bmkdir\\b",
69
- "\\btouch\\b",
70
- "\\bchmod\\b",
71
- "\\bchown\\b",
72
- "\\btee\\b",
73
- "\\bdd\\b",
74
- "\\bshred\\b",
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|branch\\s+-|clean|update-ref|tag\\s+-|cherry-pick|revert|am|apply)",
83
- "\\bsudo\\b",
84
- "\\bsu\\b",
85
- "\\bkill\\b",
86
- "\\bpkill\\b",
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 #RRGGBB.",
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 scoped = Boolean(p.selector);
94
- const html = scoped
95
- ? await page().locator(p.selector).evaluate((el: Element) => el.outerHTML)
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: scoped });
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 strings.
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
- /\bgit\s+(checkout|switch|branch\s+-[dDmM]|merge|rebase|pull|reset|worktree)/,
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 thinkingLevelFromSession = branch
264
- .filter(isThinkingEvent)
265
- .reduce((_, e) => e.thinkingLevel ?? "off", "off");
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
- const gitBranch = footerDataRef?.getGitBranch() ?? null;
279
- const gitStatus = getGitStatus(gitBranch);
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 || pi.getThinkingLevel(),
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.00") + applyColor(ctx.theme, "dim", " (no pricing)"),
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(2)}`);
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
- const valueStr = applyColor(ctx.theme, state?.appearance?.modeColor ?? "muted", value);
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
- const h = hex.replace("#", "");
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
- return `${hexToAnsi(color)}${text}\x1b[0m`;
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
- const h = color.replace("#", "");
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
- const probed = theme.fg(color as ThemeColor, "X");
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 create or update `AGENTS.md` when its fingerprint is missing or stale.
8
- - No flags or shortcuts.
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 evidence-based guidance for project purpose, commands, architecture, configuration, testing, operational quirks, and key files.
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; reports `turbo.json` and `nx.json` as contextual manifests.
15
- - Collects extension counts, known manifests, and npm scripts without embedding full manifest bodies.
16
- - Incorporates relevant Cursor, Copilot, Claude Code, Windsurf, Cline, and Devin rule sources without copying them verbatim or treating them as more authoritative than repository evidence.
17
- - Writes a deterministic baseline `AGENTS.md` before dispatching the model turn, so lower-capability models cannot leave the guide only in chat; the model is then prompted to refine it with repository-specific guidance and verify the file.
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, baseline writing, foreign-rule discovery, and prompt dispatch. 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. Crawl context remains in the active Pi transcript only; cross-session structured retrieval is not implemented. The model prompt explicitly requires a write-tool call and a read-back verification, but the baseline write is the reliability boundary.
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.