@openlines/opl-pi-sht 0.2.12 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -85
- package/configs/opl-browser.json.sample +3 -2
- package/configs/opl-footer.json.sample +3 -2
- package/configs/opl-guardian.json.sample +8 -2
- package/configs/opl-modes.json.sample +5 -0
- package/configs/opl-webaccess.json.sample +7 -1
- package/extensions/opl-browser/README.md +81 -55
- package/extensions/opl-browser/browser.ts +244 -50
- package/extensions/opl-browser/config.ts +15 -7
- package/extensions/opl-browser/index.ts +5 -4
- package/extensions/opl-browser/package-lock.json +3 -3
- package/extensions/opl-browser/package.json +4 -4
- package/extensions/opl-browser/validate.ts +313 -60
- package/extensions/opl-configurator/README.md +40 -0
- package/extensions/opl-configurator/configurator.ts +151 -0
- package/extensions/opl-configurator/index.ts +12 -0
- package/extensions/opl-ctxtrim/README.md +45 -14
- package/extensions/opl-ctxtrim/index.ts +33 -13
- package/extensions/opl-footer/README.md +31 -6
- package/extensions/opl-footer/config.ts +11 -10
- package/extensions/opl-footer/configure.ts +159 -115
- package/extensions/opl-footer/git-status.ts +58 -1
- package/extensions/opl-footer/icons.ts +3 -0
- package/extensions/opl-footer/index.ts +209 -105
- package/extensions/opl-footer/segments/caveman.ts +9 -1
- package/extensions/opl-footer/segments/chat-mode.ts +8 -1
- package/extensions/opl-footer/segments/compactions.ts +13 -0
- package/extensions/opl-footer/segments/context.ts +6 -5
- package/extensions/opl-footer/segments/index.ts +2 -0
- package/extensions/opl-footer/segments/mode-switcher.ts +14 -1
- package/extensions/opl-footer/segments/path.ts +1 -1
- package/extensions/opl-footer/segments/plan-mode.ts +8 -1
- package/extensions/opl-footer/theme.ts +5 -2
- package/extensions/opl-footer/types.ts +4 -0
- package/extensions/opl-guardian/README.md +17 -5
- package/extensions/opl-guardian/config.ts +72 -6
- package/extensions/opl-guardian/guardian.ts +42 -10
- package/extensions/opl-guardian/index.ts +52 -17
- package/extensions/opl-guardian/policies.ts +52 -16
- package/extensions/opl-init/README.md +12 -4
- package/extensions/opl-init/index.ts +228 -27
- package/extensions/opl-input/README.md +17 -3
- package/extensions/opl-input/config.ts +59 -5
- package/extensions/opl-input/configure.ts +146 -0
- package/extensions/opl-input/index.ts +38 -30
- package/extensions/opl-input/mode-style.ts +6 -2
- package/extensions/opl-modes/README.md +22 -8
- package/extensions/opl-modes/config.ts +62 -8
- package/extensions/opl-modes/index.ts +87 -28
- package/extensions/opl-modes/state.ts +50 -2
- package/extensions/opl-modes/types.ts +6 -0
- package/extensions/opl-modes/utils.ts +18 -8
- package/extensions/opl-questionnaire/index.ts +10 -4
- package/extensions/opl-questionnaire/types.ts +4 -0
- package/extensions/opl-simplebench/README.md +21 -15
- package/extensions/opl-simplebench/artifact.ts +7 -4
- package/extensions/opl-simplebench/benchmark.ts +96 -35
- package/extensions/opl-simplebench/coding.ts +9 -4
- package/extensions/opl-simplebench/index.ts +19 -19
- package/extensions/opl-simplebench/llama-server.ts +3 -2
- package/extensions/opl-simplebench/report.ts +0 -6
- package/extensions/opl-simplebench/research.ts +5 -4
- package/extensions/opl-simplebench/scoring.ts +4 -2
- package/extensions/opl-simplebench/util/config.ts +15 -8
- package/extensions/opl-simplebench/util/exec-env.ts +36 -0
- package/extensions/opl-simplebench/util/format.ts +2 -2
- package/extensions/opl-simplebench/util/providers.ts +23 -194
- package/extensions/opl-todo/README.md +5 -5
- package/extensions/opl-todo/config.ts +7 -2
- package/extensions/opl-todo/index.ts +50 -9
- package/extensions/opl-webaccess/README.md +31 -5
- package/extensions/opl-webaccess/config.ts +36 -7
- package/extensions/opl-webaccess/extract.ts +37 -47
- package/extensions/opl-webaccess/http.ts +259 -0
- package/extensions/opl-webaccess/index.ts +12 -6
- package/extensions/opl-webaccess/package-lock.json +3 -3
- package/extensions/opl-webaccess/package.json +4 -9
- package/extensions/opl-webaccess/pdf.ts +4 -4
- package/extensions/opl-webaccess/search.ts +19 -8
- package/extensions/opl-webaccess/storage.ts +134 -2
- package/extensions/opl-webaccess/types.ts +4 -0
- package/extensions/opl-webaccess/utils.ts +221 -33
- package/package.json +19 -12
|
@@ -3,6 +3,7 @@ import type { TUI, EditorTheme } from "@earendil-works/pi-tui";
|
|
|
3
3
|
import type { KeybindingsManager } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { visibleWidth, truncateToWidth } from "@earendil-works/pi-tui";
|
|
5
5
|
import { CONFIG, COMPANION_PADDING, MIN_WIDTH_FOR_COMPANION } from "./config.js";
|
|
6
|
+
import { registerInputConfiguratorTab } from "./configure.js";
|
|
6
7
|
import { resolveModeStyle, type ModeAppearance } from "./mode-style.js";
|
|
7
8
|
import { applyColor, CompanionAnimator, COMPANION_TICK_MS, IDLE_REPAINT_MS, startRenderTimer } from "./utils.js";
|
|
8
9
|
|
|
@@ -13,6 +14,34 @@ function plainText(line: string): string {
|
|
|
13
14
|
return line.replace(ANSI_RE, "");
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
// ─── pi-tui private render format ─────────────────────────────────────────────
|
|
18
|
+
// The box renderers below must tell the editor's horizontal borders from its scroll
|
|
19
|
+
// indicator, and pi-tui exposes neither as an API nor as a stable class: it is plain text.
|
|
20
|
+
// These two markers are the whole contract, so they live here instead of being duplicated in
|
|
21
|
+
// each renderer, and tests/opl-input-pi-tui-markers.test.mjs asserts the installed pi-tui
|
|
22
|
+
// still emits them. Verified byte-identical in pi-tui 0.87.0, 0.99.1 and 1.1.0
|
|
23
|
+
// (dist/components/editor.js: `─── ${direction} ${hiddenLineCount} more `).
|
|
24
|
+
const BORDER_CHAR = "─";
|
|
25
|
+
const SCROLL_INDICATOR_RE = /((?:↑|↓)\s*\d+\s*more)/;
|
|
26
|
+
|
|
27
|
+
/** Solid border: after stripping ANSI every character is the box-drawing horizontal. */
|
|
28
|
+
export function isSolidBorder(line: string): boolean {
|
|
29
|
+
return plainText(line).replaceAll(BORDER_CHAR, "").length === 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The scroll indicator (`─── ↑ 3 more `) is border-like but carries text worth preserving. */
|
|
33
|
+
export function scrollIndicatorText(line: string): string | null {
|
|
34
|
+
const plain = plainText(line);
|
|
35
|
+
if (!plain.startsWith(BORDER_CHAR)) return null;
|
|
36
|
+
const match = plain.match(SCROLL_INDICATOR_RE);
|
|
37
|
+
return match ? match[1]! : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Anything that marks the top or bottom edge of the stock editor output. */
|
|
41
|
+
export function isBorderLike(line: string): boolean {
|
|
42
|
+
return isSolidBorder(line) || scrollIndicatorText(line) !== null;
|
|
43
|
+
}
|
|
44
|
+
|
|
16
45
|
function activeMode(): { mode: string; appearance?: ModeAppearance } {
|
|
17
46
|
const state = (globalThis as Record<string, unknown>).__agentMode as
|
|
18
47
|
| { mode?: string; appearance?: ModeAppearance }
|
|
@@ -121,19 +150,6 @@ class ChatInput extends CustomEditor {
|
|
|
121
150
|
): string[] {
|
|
122
151
|
const innerWidth = width - 2;
|
|
123
152
|
|
|
124
|
-
// Solid border: after stripping ANSI every char is "─"
|
|
125
|
-
const isSolidBorder = (line: string) => plainText(line).replace(/─/g, "").length === 0;
|
|
126
|
-
|
|
127
|
-
// Scroll indicator: starts with "─" and contains ↑/↓ N more
|
|
128
|
-
const getScrollText = (line: string): string | null => {
|
|
129
|
-
const plain = plainText(line);
|
|
130
|
-
if (!plain.startsWith("─")) return null;
|
|
131
|
-
const m = plain.match(/((?:↑|↓)\s*\d+\s*more)/);
|
|
132
|
-
return m ? m[1] : null;
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const isBorderLike = (line: string) => isSolidBorder(line) || getScrollText(line) !== null;
|
|
136
|
-
|
|
137
153
|
const firstIdx = stock.findIndex(isBorderLike);
|
|
138
154
|
let lastIdx = -1;
|
|
139
155
|
for (let i = stock.length - 1; i >= 0; i--) {
|
|
@@ -158,8 +174,8 @@ class ChatInput extends CustomEditor {
|
|
|
158
174
|
return border("└") + border(mid) + border("─".repeat(remaining)) + border("┘");
|
|
159
175
|
};
|
|
160
176
|
|
|
161
|
-
const topScrollText = firstIdx !== -1 ?
|
|
162
|
-
const bottomScrollText = lastIdx !== -1 && lastIdx !== firstIdx ?
|
|
177
|
+
const topScrollText = firstIdx !== -1 ? scrollIndicatorText(stock[firstIdx]!) : null;
|
|
178
|
+
const bottomScrollText = lastIdx !== -1 && lastIdx !== firstIdx ? scrollIndicatorText(stock[lastIdx]!) : null;
|
|
163
179
|
|
|
164
180
|
const top = buildTop(topScrollText);
|
|
165
181
|
const bottom = buildBottom(bottomScrollText);
|
|
@@ -207,19 +223,6 @@ class ChatInput extends CustomEditor {
|
|
|
207
223
|
accent: (s: string) => string,
|
|
208
224
|
prefix: string,
|
|
209
225
|
): string[] {
|
|
210
|
-
// Solid border: after stripping ANSI every char is "─"
|
|
211
|
-
const isSolidBorder = (line: string) => plainText(line).replace(/─/g, "").length === 0;
|
|
212
|
-
|
|
213
|
-
// Scroll indicator: starts with "─" and contains ↑/↓ N more
|
|
214
|
-
const getScrollText = (line: string): string | null => {
|
|
215
|
-
const plain = plainText(line);
|
|
216
|
-
if (!plain.startsWith("─")) return null;
|
|
217
|
-
const m = plain.match(/((?:↑|↓)\s*\d+\s*more)/);
|
|
218
|
-
return m ? m[1] : null;
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
const isBorderLike = (line: string) => isSolidBorder(line) || getScrollText(line) !== null;
|
|
222
|
-
|
|
223
226
|
const firstIdx = stock.findIndex(isBorderLike);
|
|
224
227
|
let lastIdx = -1;
|
|
225
228
|
for (let i = stock.length - 1; i >= 0; i--) {
|
|
@@ -244,8 +247,8 @@ class ChatInput extends CustomEditor {
|
|
|
244
247
|
return border(mid) + border("─".repeat(remaining));
|
|
245
248
|
};
|
|
246
249
|
|
|
247
|
-
const topScrollText = firstIdx !== -1 ?
|
|
248
|
-
const bottomScrollText = lastIdx !== -1 && lastIdx !== firstIdx ?
|
|
250
|
+
const topScrollText = firstIdx !== -1 ? scrollIndicatorText(stock[firstIdx]!) : null;
|
|
251
|
+
const bottomScrollText = lastIdx !== -1 && lastIdx !== firstIdx ? scrollIndicatorText(stock[lastIdx]!) : null;
|
|
249
252
|
|
|
250
253
|
const top = buildTop(topScrollText);
|
|
251
254
|
const bottom = buildBottom(bottomScrollText);
|
|
@@ -304,4 +307,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
304
307
|
activeEditor?.dispose();
|
|
305
308
|
activeEditor = null;
|
|
306
309
|
});
|
|
310
|
+
|
|
311
|
+
// Expose the settings screen to opl-configurator's generic shell; the shell owns the
|
|
312
|
+
// /configurator command and the tab only writes opl-input.json (a /reload is still
|
|
313
|
+
// required before the running editor picks the change up).
|
|
314
|
+
registerInputConfiguratorTab();
|
|
307
315
|
}
|
|
@@ -19,8 +19,12 @@ export interface ResolvedModeStyle {
|
|
|
19
19
|
prefix: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/** The baseline every unknown mode falls back to, kept as a constant so the fallback is a value
|
|
23
|
+
* rather than an index into a record that TypeScript must treat as possibly missing. */
|
|
24
|
+
const OFF_DEFAULT: Required<ModeAppearance> = { prefix: "❯", prefixColor: "accent", borderColor: "border" };
|
|
25
|
+
|
|
22
26
|
const MODE_DEFAULTS: Record<string, Required<ModeAppearance>> = {
|
|
23
|
-
off:
|
|
27
|
+
off: OFF_DEFAULT,
|
|
24
28
|
chat: { prefix: "»", prefixColor: "borderAccent", borderColor: "borderAccent" },
|
|
25
29
|
plan: { prefix: "⏸", prefixColor: "customMessageLabel", borderColor: "customMessageLabel" },
|
|
26
30
|
execute: { prefix: "⏸", prefixColor: "customMessageLabel", borderColor: "customMessageLabel" },
|
|
@@ -28,7 +32,7 @@ const MODE_DEFAULTS: Record<string, Required<ModeAppearance>> = {
|
|
|
28
32
|
|
|
29
33
|
/** Precedence: bash > active mode appearance > hardcoded mode fallback. */
|
|
30
34
|
export function resolveModeStyle(state: ModeState): ResolvedModeStyle {
|
|
31
|
-
const fallback = MODE_DEFAULTS[state.mode] ??
|
|
35
|
+
const fallback = MODE_DEFAULTS[state.mode] ?? OFF_DEFAULT;
|
|
32
36
|
if (state.bash) return { borderColor: "bashMode", prefixColor: "bashMode", prefix: fallback.prefix };
|
|
33
37
|
return {
|
|
34
38
|
borderColor: state.appearance?.borderColor ?? fallback.borderColor,
|
|
@@ -11,7 +11,7 @@ Unified mode manager for Pi. It provides normal, read-only chat, read-only plann
|
|
|
11
11
|
- `--chat` and `--plan` start Pi in the corresponding read-only mode.
|
|
12
12
|
- The configured cycle shortcut rotates through enabled visible modes. Execute mode is excluded from cycling because it requires an active plan. The default `ctrl+alt+m` does not collide with any built-in Pi keybinding; set `shortcuts.cycleMode` to rebind.
|
|
13
13
|
|
|
14
|
-
Plans are Markdown files under
|
|
14
|
+
Plans are Markdown files under `<session directory>/.pi/plans/` with the `plan-` filename prefix - the directory comes from `ExtensionContext.cwd`, captured at `session_start`, so a session opened in one project never reads or writes another project's plans. A plan name must contain at least one letter or digit, so `/plan .` or `/plan --` is rejected instead of producing an untitled `plan-.md`. Loading a plan appends a TUI-only card (via `registerEntryRenderer`) so the plan text never enters the model context; the execute-mode system prompt is the single model-facing copy of the plan, capped at `plan.maxInjectBytes` (default 24 KB), truncating on a line boundary and ending with a marker that names the plan file — the file on disk stays authoritative and is never rewritten. The card stores at most `plan.maxEntryBytes` (default 4 KB) and shows the same marker. `plan_complete` is available in execute mode and in any mode that sets `allowPlanComplete`. On completion, the plan file is deleted when `cleanup.cleanupOnComplete` is enabled. If execution ends without `plan_complete`, execute mode is exited automatically at Pi's `agent_before_settle` boundary (added in Pi 0.87.0) — but only when the run settled as `completed`: an aborted (ESC) or errored run keeps execute mode active so the plan can be resumed. On Pi < 0.87.0 this auto-exit is unavailable because the boundary is never emitted; `plan_complete` still exits the mode on any version.
|
|
15
15
|
|
|
16
16
|
## Extension features
|
|
17
17
|
|
|
@@ -31,11 +31,13 @@ Any mode can start plan execution via the mode picker's `Execute:` entries or `/
|
|
|
31
31
|
|
|
32
32
|
### Tool inheritance warning
|
|
33
33
|
|
|
34
|
-
A custom mode with no `tools` array inherits
|
|
34
|
+
A custom mode with no `tools` array inherits the resting tool set, including `write` and `edit` when available. A pinned `modes.off.tools` list narrows that baseline. Always specify an explicit read-only tool list for restrictive modes.
|
|
35
35
|
|
|
36
|
-
`off` behaves the same way: it restores the tools that were active before the mode (never a wider set). Set `modes.off.tools` to pin the resting tool set instead — for example to keep `subagent`/`browser` out of normal mode entirely. Pinning replaces the snapshot taken on mode entry, so a later mode that lists no `tools` inherits the pinned set rather than the older, wider one. `load_tools` is offered in `off` because withheld lazy tools are otherwise unreachable there.
|
|
36
|
+
`off` behaves the same way: it restores the tools that were active before the mode (never a wider set). Set `modes.off.tools` to pin the resting tool set instead — for example to keep `subagent`/`browser` out of normal mode entirely. The pinned set also applies before `--chat` or `--plan` startup takes its snapshot, on startup when no mode entry is restored, and as the inherited baseline when plan or execute state is restored. Pinning replaces the snapshot taken on mode entry, so a later mode that lists no `tools` inherits the pinned set rather than the older, wider one. `load_tools` is offered in `off` because withheld lazy tools are otherwise unreachable there.
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
Set `modes.execute.tools` to replace the tools available during execution; otherwise execute inherits the pre-plan active set. The configured list is applied on entry and resume, with lazy loading and `allowPlanComplete` still in effect. Exiting restores the previous set or the pinned OFF baseline.
|
|
39
|
+
|
|
40
|
+
Model overrides are resolved through Pi's model registry when entering a mode. A blank or partial `model` (for example `{ "provider": "", "id": "" }`) is treated as **no override**, so the mode keeps the current model instead of warning `Model not found: /`. The previously active model is captured once per mode and restored on exit; the restore point is persisted in the mode session entry, so `/reload` and `/resume` inside a mode no longer lose it. If the restore target is gone from the registry it is dropped immediately; if it exists but has no credentials (before `/login`, say) the restore is retried once and then released, so a stale point cannot block future captures indefinitely. `pi.setModel` is session-scoped: it never rewrites your configured `defaultProvider`/`defaultModel`, but it does append a model change to the session transcript, which is why the restore point is persisted alongside the mode. Changes are serialized, so a rapid mode exit cannot leave a superseded mode model active. Note that setting `modes.off.model` makes OFF a pinned baseline: it is applied on every mode exit *and* at session start, overriding `--model` and the configured default. Use Pi theme color tokens for widget label colors.
|
|
39
41
|
|
|
40
42
|
Mode state is persisted in session entries and restored on session resume or branch changes. Pi ignores tool names it does not know, so a `tools` list with a typo would silently lose tools: every mode entry checks the list against the registered tools and warns once per unknown name per session. The `mode-switcher` entry type and legacy chat/plan event identifiers are compatibility contracts. On resume, an unknown mode name in a session entry is ignored (falling back to normal), and plan filenames containing path separators or `..` are dropped, so a stale or foreign branch cannot leave the session unrestricted or point plan reads outside `.pi/plans/`.
|
|
41
43
|
|
|
@@ -83,6 +85,8 @@ Create `~/.pi/agent/configs/opl-modes.json` or copy [`configs/opl-modes.json.sam
|
|
|
83
85
|
| `ui.hideNotify` / `ui.hideWidget` | Suppress mode notifications or widgets. |
|
|
84
86
|
| `shortcuts.cycleMode` | Keybinding for cycling enabled visible modes. |
|
|
85
87
|
| `cleanup.cleanupOnComplete` | Delete the active plan after successful `plan_complete`. |
|
|
88
|
+
| `plan.maxInjectBytes` | Bytes of plan text allowed into the system prompt for execute or refine (default `24000`). The UTF-8 cap includes the marker and retains complete lines. If the source path cannot fit, the marker refers to the plan file; very small budgets use a shortened notice. The file itself is never modified. Configured ceilings must be positive integers; invalid values fall back to defaults. |
|
|
89
|
+
| `plan.maxEntryBytes` | Bytes of plan text stored in the `plan-mode` session entry (default `4096`). The cap includes the marker; the card retains the full file path separately. |
|
|
86
90
|
| `defaultNotifyTemplate` | Notification template for custom modes; `{Name}` is capitalized mode name. |
|
|
87
91
|
| `modes.chat.tools` / `modes.plan.tools` | Replace the respective built-in read-only tool lists. |
|
|
88
92
|
| `chatAllowedTools` / `planAllowedTools` | **Deprecated compatibility aliases** for the built-in tool lists. They are used only when the corresponding `modes.<name>.tools` is omitted; migrate to `modes.chat.tools` or `modes.plan.tools`. |
|
|
@@ -113,8 +117,6 @@ Use `modes.<name>.appearance` to keep any mode's visual identity with its defini
|
|
|
113
117
|
|
|
114
118
|
`prefix`, `prefixColor`, and `borderColor` style `opl-input`; omitted fields use its compiled mode defaults. `modeColor` styles the value in `opl-footer`'s `mode_switcher` segment; if omitted, the footer uses hardcoded `muted`. Colors accept Pi theme tokens, six-digit hex, or the three-digit `#abc` shorthand (expanded to `#aabbcc`).
|
|
115
119
|
|
|
116
|
-
Model overrides are resolved through Pi's model registry when entering a mode and the previously active model is restored on exit when applicable. Changes are serialized, so a rapid mode exit cannot leave a superseded mode model active. Use Pi theme color tokens for widget label colors.
|
|
117
|
-
|
|
118
120
|
### Lazy tool loading
|
|
119
121
|
|
|
120
122
|
`lazyTools` withholds heavy tool schemas from the active set until the model actually needs them, keeping the system-prompt prefix (and its prompt-cache write) smaller every session:
|
|
@@ -127,15 +129,27 @@ Model overrides are resolved through Pi's model registry when entering a mode an
|
|
|
127
129
|
|
|
128
130
|
Behavior:
|
|
129
131
|
|
|
130
|
-
- Listed tools are removed from the active set wherever opl-modes computes it. In practice this only affects modes that would otherwise include them: `off`/`execute` (which inherit
|
|
132
|
+
- Listed tools are removed from the active set wherever opl-modes computes it. In practice this only affects modes that would otherwise include them: `off`/`execute` (which inherit the resting tool set unless overridden) and custom modes that permit them. Read-only `chat`/`plan` already exclude these tools, so nothing changes there.
|
|
131
133
|
- When at least one lazy tool is withheld from a mode, opl-modes registers and activates a `load_tools` tool. The model calls `load_tools({ tools: [...] })` (or with no argument to enable all currently-allowed lazy tools) to activate them before use.
|
|
132
|
-
- `load_tools` is bounded by the current mode's tool policy:
|
|
134
|
+
- `load_tools` is bounded by the current mode's tool policy: explicit tool lists take precedence; execute and custom modes without a list inherit `modes.off.tools` when pinned. Both named requests and load-all respect that baseline.
|
|
133
135
|
- Core built-ins (`read`, `edit`, `write`, `bash`, `powershell`, `grep`, `find`, `ls`), `plan_complete`, and `load_tools` itself are protected and silently ignored if listed in `lazyTools`.
|
|
134
136
|
- Switching modes re-applies the policy, so a lazy tool enabled earlier returns to inactive on the next mode change and must be re-enabled.
|
|
135
137
|
- A mode `prompt` that instructs the model to use a lazy tool must also tell it to call `load_tools` first, otherwise the instruction names a tool the model cannot see. The bundled `research` mode does exactly that.
|
|
136
138
|
|
|
137
139
|
Caching note: activating a lazy tool mid-session preserves the cached prefix on models with native deferred tool loading (Anthropic 4.5+, OpenAI gpt-5.4+) and otherwise triggers one prompt-cache rewrite from that point. It is most effective for tools you use occasionally (delegation, browser automation, benchmarking).
|
|
138
140
|
|
|
141
|
+
## Cross-extension seams
|
|
142
|
+
|
|
143
|
+
Extensions install one directory at a time and load in any combination, so `globalThis` is
|
|
144
|
+
the only channel between them. `opl-modes` publishes `__agentMode`, `__planMode` and
|
|
145
|
+
`__chatMode`; `opl-footer` publishes `__footerRequestRender` while a footer is mounted.
|
|
146
|
+
|
|
147
|
+
Nothing may assume the other side is present or healthy. The render trigger is checked for
|
|
148
|
+
being a function and called inside a `try`: a footer whose TUI was already torn down must not
|
|
149
|
+
fail a mode transition. `session_start` re-publishes the three mode globals and
|
|
150
|
+
`session_shutdown` deletes them, so a later session in the same process never reads a mode
|
|
151
|
+
that belongs to a dead one.
|
|
152
|
+
|
|
139
153
|
## Architecture
|
|
140
154
|
|
|
141
155
|
`config.ts` registers built-in modes, merges configured overrides, compiles Bash patterns, and publishes the registry. `state.ts` persists and restores mode/plan state (including the persisted model restore point) and is the single publisher of `globalThis.__agentMode`. `index.ts` wires commands, picker, lifecycle hooks, tool replacement, Bash interception, and `plan_complete`; `utils.ts` handles plan files, the per-segment Bash gate, and shared helpers.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** Config: tool allowlists, bash patterns, prompt templates, plan file constants, user config. */
|
|
2
2
|
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
4
|
import { join } from "node:path";
|
|
5
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import type { ModeSwitcherUserConfig, ModeDefinition, PartialModeDefinition, ModeModelConfig } from "./types.js";
|
|
7
7
|
|
|
8
8
|
// ─── Plan File Constants ────────────────────────────────────────────────────
|
|
@@ -150,7 +150,8 @@ After listing all steps, stop and wait for the user to choose:
|
|
|
150
150
|
Do NOT attempt to make any file changes, run destructive commands, or modify anything.`;
|
|
151
151
|
|
|
152
152
|
/** System prompt injected when in EXECUTE mode. */
|
|
153
|
-
export function buildExecutePrompt(planContent: string): string {
|
|
153
|
+
export function buildExecutePrompt(planContent: string, source?: string): string {
|
|
154
|
+
const capped = capPlan(planContent, USER_CONFIG.plan.maxInjectBytes, source);
|
|
154
155
|
return `\
|
|
155
156
|
You are in EXECUTE MODE. Execute the plan below step by step.
|
|
156
157
|
|
|
@@ -159,16 +160,17 @@ After completing ALL steps, call plan_complete() to signal that execution is fin
|
|
|
159
160
|
If the \`todo\` tool is available, use it to track progress: add all plan steps at the start of execution, then toggle each one done as you complete it.
|
|
160
161
|
|
|
161
162
|
Plan:
|
|
162
|
-
${
|
|
163
|
+
${capped.text}`;
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
/** System prompt injected when refining a plan in PLAN mode. */
|
|
166
|
-
export function buildRefinePrompt(planContent: string): string {
|
|
167
|
+
export function buildRefinePrompt(planContent: string, source?: string): string {
|
|
168
|
+
const capped = capPlan(planContent, USER_CONFIG.plan.maxInjectBytes, source);
|
|
167
169
|
return `\
|
|
168
170
|
You are in PLAN MODE (refining). The user wants to revise the current plan based on their feedback.
|
|
169
171
|
|
|
170
172
|
Current plan:
|
|
171
|
-
${
|
|
173
|
+
${capped.text}
|
|
172
174
|
|
|
173
175
|
Each step MUST be self-contained — write it as if the executor has no memory of this conversation. Include enough context that it can be carried out with only the plan file and the codebase. Assume the executor will read the relevant files fresh — do not rely on findings you discovered during planning.
|
|
174
176
|
|
|
@@ -196,6 +198,10 @@ const DEFAULT_CONFIG = {
|
|
|
196
198
|
HIDE_NOTIFY: false,
|
|
197
199
|
HIDE_WIDGET: true,
|
|
198
200
|
},
|
|
201
|
+
PLAN: {
|
|
202
|
+
MAX_INJECT_BYTES: 24000,
|
|
203
|
+
MAX_ENTRY_BYTES: 4096,
|
|
204
|
+
},
|
|
199
205
|
SHORTCUTS: {
|
|
200
206
|
CYCLE_MODE: "ctrl+alt+m",
|
|
201
207
|
},
|
|
@@ -230,15 +236,18 @@ const DEFAULT_CONFIG = {
|
|
|
230
236
|
},
|
|
231
237
|
};
|
|
232
238
|
|
|
233
|
-
|
|
239
|
+
/** Resolved per call so a custom `PI_AGENT_DIR` (pi's own agent dir) is honoured. */
|
|
240
|
+
export function configPath(): string {
|
|
241
|
+
return join(getAgentDir(), "configs", "opl-modes.json");
|
|
242
|
+
}
|
|
234
243
|
|
|
235
244
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
236
245
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
237
246
|
}
|
|
238
247
|
|
|
239
|
-
export function loadUserConfig(
|
|
248
|
+
export function loadUserConfig(file = configPath()): ModeSwitcherUserConfig {
|
|
240
249
|
try {
|
|
241
|
-
const parsed = JSON.parse(readFileSync(
|
|
250
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
242
251
|
return isRecord(parsed) ? parsed as ModeSwitcherUserConfig : {};
|
|
243
252
|
} catch {
|
|
244
253
|
return {};
|
|
@@ -335,10 +344,55 @@ export function lazyToolsToEnable(
|
|
|
335
344
|
return want.filter((n) => lazy.has(n) && (!allowed || allowed.has(n)) && !active.has(n));
|
|
336
345
|
}
|
|
337
346
|
|
|
347
|
+
/** Positive integer from user config, else the default. Anything else is ignored rather than trusted. */
|
|
348
|
+
function positiveInt(value: unknown, fallback: number): number {
|
|
349
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : fallback;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export interface CappedPlan {
|
|
353
|
+
text: string;
|
|
354
|
+
truncated: boolean;
|
|
355
|
+
omittedBytes: number;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Bound a plan body before it is injected into a prompt or stored as an entry.
|
|
360
|
+
* A plan file is user-authored and uncapped: injected whole it is re-sent on
|
|
361
|
+
* every provider request for the run, and stored whole it lives in the session
|
|
362
|
+
* file forever. Truncation is announced so the reader can open the file.
|
|
363
|
+
*/
|
|
364
|
+
export function capPlan(plan: string, maxBytes: number, source?: string): CappedPlan {
|
|
365
|
+
const bytes = Buffer.byteLength(plan, "utf8");
|
|
366
|
+
if (!Number.isFinite(maxBytes) || maxBytes <= 0 || bytes <= maxBytes) {
|
|
367
|
+
return { text: plan, truncated: false, omittedBytes: 0 };
|
|
368
|
+
}
|
|
369
|
+
const budget = Math.floor(maxBytes);
|
|
370
|
+
const marker = (shown: number, location: string) =>
|
|
371
|
+
`\n\n[… plan truncated: showing ${shown} of ${bytes} bytes, ${bytes - shown} omitted - read ${location} for the rest …]`;
|
|
372
|
+
// Reserve the widest possible counts, including a UTF-8 source path.
|
|
373
|
+
const reserve = (location: string) => Buffer.byteLength(marker(0, location), "utf8") + String(bytes).length - 1;
|
|
374
|
+
let location = source ?? "the plan file";
|
|
375
|
+
if (reserve(location) > budget) location = "the plan file";
|
|
376
|
+
if (reserve(location) > budget) {
|
|
377
|
+
return { text: "[plan truncated]".slice(0, budget), truncated: true, omittedBytes: bytes };
|
|
378
|
+
}
|
|
379
|
+
const buffer = Buffer.from(plan, "utf8");
|
|
380
|
+
const limit = budget - reserve(location);
|
|
381
|
+
const newline = buffer.lastIndexOf(10, limit);
|
|
382
|
+
// Keep only complete lines. A single line that exceeds the budget stays in the file.
|
|
383
|
+
const head = newline < 0 ? "" : buffer.subarray(0, newline).toString("utf8");
|
|
384
|
+
const shown = Buffer.byteLength(head, "utf8");
|
|
385
|
+
return { text: head + marker(shown, location), truncated: true, omittedBytes: bytes - shown };
|
|
386
|
+
}
|
|
387
|
+
|
|
338
388
|
export const USER_CONFIG = {
|
|
339
389
|
cleanup: {
|
|
340
390
|
cleanupOnComplete: userConfig.cleanup?.cleanupOnComplete ?? DEFAULT_CONFIG.CLEANUP.CLEANUP_ON_COMPLETE,
|
|
341
391
|
},
|
|
392
|
+
plan: {
|
|
393
|
+
maxInjectBytes: positiveInt(userConfig.plan?.maxInjectBytes, DEFAULT_CONFIG.PLAN.MAX_INJECT_BYTES),
|
|
394
|
+
maxEntryBytes: positiveInt(userConfig.plan?.maxEntryBytes, DEFAULT_CONFIG.PLAN.MAX_ENTRY_BYTES),
|
|
395
|
+
},
|
|
342
396
|
ui: {
|
|
343
397
|
hideNotify: userConfig.ui?.hideNotify ?? DEFAULT_CONFIG.UI.HIDE_NOTIFY,
|
|
344
398
|
hideWidget: userConfig.ui?.hideWidget ?? DEFAULT_CONFIG.UI.HIDE_WIDGET,
|