pi-bro 0.30.0 → 0.31.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/CHANGELOG.md +13 -0
- package/README.md +4 -1
- package/THIRD_PARTY_NOTICES.md +28 -0
- package/docs/README.md +1 -0
- package/docs/overthink.md +48 -0
- package/docs/reference/commands.md +3 -2
- package/docs/reference/data-and-permissions.md +2 -0
- package/package.json +7 -2
- package/src/bro.ts +13 -1
- package/src/config-navigation-ui.ts +14 -2
- package/src/config-navigation.ts +3 -2
- package/src/doctor.ts +19 -1
- package/src/explain-modal.ts +6 -1
- package/src/modal-kit.ts +7 -4
- package/src/overthink-questions-ui.ts +131 -0
- package/src/overthink-state.ts +204 -0
- package/src/overthink.ts +323 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to pi-bro are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.31.0] - 2026-11-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Automatic Overthink second-look notes after fully settled substantive Pi work (after retry, compaction and queued continuations), using the active model, thinking level and current session projection to allow prompt-cache reuse.
|
|
10
|
+
- `/bro overthink` private notes viewer with force retry, help, copy and session-question editing.
|
|
11
|
+
- Session-position markers label older notes OUTDATED after later work or branch movement; a generation check prevents a stale automatic-settings read from scheduling a check for newer work.
|
|
12
|
+
- One editable question-list UI for global defaults (`/bro config → Features → Overthink`) and independent per-session question snapshots. Reset global to factory and session to global; store latest notes as session-only custom entries.
|
|
13
|
+
|
|
14
|
+
### Limits
|
|
15
|
+
|
|
16
|
+
- Same-model criticism is not an independent review. Automatic checks start on; Features → Overthink can turn them off while keeping manual retry. Usage and cost are shown only when reported by the provider; high-effort reasoning can still be expensive. The second request retains Pi's context, which may include tool descriptions, but does not execute tools. Actual cache reuse and billable savings require live verification. Full TUI and PiG qualification still require real sessions.
|
|
17
|
+
|
|
5
18
|
## [0.30.0] - 2026-11-06
|
|
6
19
|
|
|
7
20
|
### Added
|
package/README.md
CHANGED
|
@@ -51,6 +51,7 @@ For a first success, type this in Pi after setting up a backend:
|
|
|
51
51
|
| `/bro show` | Draws the current session's recent conversation, if any |
|
|
52
52
|
| `/bro architecture` | Creates or refreshes `docs/architecture/` for this project |
|
|
53
53
|
| `/bro grill-me Should agents talk freely to each other?` | Questions your assumptions privately, one at a time |
|
|
54
|
+
| `/bro overthink` | Views private automatic second-look notes; Ctrl+R forces a new check |
|
|
54
55
|
| `/bro elevate Improve how we search company documents` | Find expert gaps before giving Pi a task |
|
|
55
56
|
|
|
56
57
|
File and webpage examples need your own real path or URL; see [how to explain them](docs/features/explain.md#explain-a-document). For a step-by-step first run, see [Getting started](docs/getting-started.md).
|
|
@@ -64,6 +65,7 @@ File and webpage examples need your own real path or URL; see [how to explain th
|
|
|
64
65
|
| `/bro architecture [focus]` | **[Architecture Docs](docs/features/architecture.md).** Inspects the project and writes Markdown, diagrams and an offline interactive `architecture.html` under `docs/architecture/`. Uses BTW model settings. |
|
|
65
66
|
| `/bro elevate` or **Ctrl+Shift+E** | **[Elevate](docs/elevate.md).** Discover missing expert considerations, select useful improvements, and rewrite your task without submitting it. Shares BTW backend settings. |
|
|
66
67
|
| `/bro remember-me [thought]` | **[Remember Me](docs/remember-me.md).** Privately clarify and approve short personal memories with unfoldable background. Agents can call `about_user` to read summaries and expand IDs. |
|
|
68
|
+
| `/bro overthink` | **[Overthink](docs/overthink.md).** Quietly checks meaningful work with the active Pi model, using editable global/session questions and a private notes modal. Automatic checks can be switched off in config. No separate backend. |
|
|
67
69
|
| `/bro grill-me` | **[Grill Me](docs/grill-me.md) (experimental).** A private, opinionated decision interview: light, balanced or exhaustive; one question per round; `/consolidate` drafts a brief without ending the interview; `/insert-last-message` explicitly returns the latest reply to the editor. Uses BTW model settings; read-only restrictions vary by backend. |
|
|
68
70
|
| `/bro btw` | **[Side questions](docs/features/btw.md#bro-btw-side-conversation).** A separate conversation seeded with recent context, so you can ask without interrupting the agent. ⚠️ [Full-permission mode](#what-leaves-your-machine) can edit files. |
|
|
69
71
|
| `/bro guided-review <PR>` | **[Guided Review](docs/guided-review.md).** An explanation and quality assessment of a GitHub PR, with ranked findings, source evidence and private questions. Claude, Muse, or Pi is recommended for untrusted PRs. |
|
|
@@ -77,7 +79,7 @@ Guided Review has its own [guide](docs/guided-review.md).
|
|
|
77
79
|
|
|
78
80
|
## Requirements
|
|
79
81
|
|
|
80
|
-
- Pi `>=1.3.0` and Node.js `>=22.19.0`.
|
|
82
|
+
- Pi `>=1.3.0` and Node.js `>=22.19.0`. Overthink always uses the host Pi model, independently of Bro's backend configuration.
|
|
81
83
|
- macOS or Linux. Windows is untested; there, cancelling stops only the
|
|
82
84
|
backend's direct process, not anything it started.
|
|
83
85
|
- At least one backend on your `PATH`, signed in once from your terminal, **or the built-in Pi backend**. You only need the one you use:
|
|
@@ -108,6 +110,7 @@ which may keep it under their own policies. Paths below are under
|
|
|
108
110
|
| Show | User and assistant text from recent turns (no tool calls, tool results, reasoning or images), plus your preferences | Diagram pages in `pi-bro-<uid>/` in your system temp folder |
|
|
109
111
|
| Side questions | Your questions and up to the last 8 turns of conversation text, plus your preferences | Nothing; the thread stays in memory |
|
|
110
112
|
| Grill Me | Topic, interview turns, up to the last 8 main-session text turns and preferences; any workspace files the selected inspector reads | Nothing; interview and brief are in memory |
|
|
113
|
+
| Overthink | Current session projection, including tool evidence, plus private user-editable questions, to the active Pi provider | Global questions in `bro-overthink-defaults.json`; per-session questions and latest private note in Pi session custom entries |
|
|
111
114
|
| Elevate | Your objective, selected findings, all current-branch user/assistant text; Full also includes tool-call arguments and text results, and preferences | Nothing; the workflow stays in memory |
|
|
112
115
|
| Guided Review | The PR description and diff, the files the backend reads, your questions, plus your preferences | Reviews and captured source in `bro-reviews/`, with no automatic cleanup |
|
|
113
116
|
| Advisor | The agent's system instructions, tool list and whole conversation, including tool calls and results (reasoning and images omitted), plus your steering | Steering in `bro-advisor.md` and the session file |
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -26,6 +26,34 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
26
26
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
27
|
SOFTWARE.
|
|
28
28
|
|
|
29
|
+
## pi-heads-up
|
|
30
|
+
|
|
31
|
+
The automatic same-model follow-up request and private note UI in Overthink are adapted from
|
|
32
|
+
[pi-heads-up](https://github.com/Pringled/pi-extensions/tree/main/packages/pi-heads-up),
|
|
33
|
+
copyright (c) 2026 Thomas van Dongen, licensed under the MIT License:
|
|
34
|
+
|
|
35
|
+
MIT License
|
|
36
|
+
|
|
37
|
+
Copyright (c) 2026 Thomas van Dongen
|
|
38
|
+
|
|
39
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
40
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
41
|
+
in the Software without restriction, including without limitation the rights
|
|
42
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
43
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
44
|
+
furnished to do so, subject to the following conditions:
|
|
45
|
+
|
|
46
|
+
The above copyright notice and this permission notice shall be included in all
|
|
47
|
+
copies or substantial portions of the Software.
|
|
48
|
+
|
|
49
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
50
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
51
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
52
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
53
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
54
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
55
|
+
SOFTWARE.
|
|
56
|
+
|
|
29
57
|
## Speak like you eat
|
|
30
58
|
|
|
31
59
|
The prompt benchmark fixtures and mechanical checks are adapted from
|
package/docs/README.md
CHANGED
|
@@ -15,6 +15,7 @@ Start with what you want to do. The [README](../README.md) explains the product,
|
|
|
15
15
|
- [Document a repository's architecture](features/architecture.md) — create or refresh Markdown, diagrams and an offline HTML map.
|
|
16
16
|
- [Ask a private side question](features/btw.md#bro-btw-side-conversation) — BTW.
|
|
17
17
|
- [Remember personal context for agents](remember-me.md) — approved short memories, expandable background and the read-only `about_user` tool.
|
|
18
|
+
- [Review work quietly with Overthink](overthink.md) — automatic same-model second look with editable session questions.
|
|
18
19
|
- [Get grilled on a decision](grill-me.md) — one question at a time in a private modal, then copy a decision brief.
|
|
19
20
|
- [Improve a request before sending it](elevate.md) — Elevate: discover expert gaps, choose improvements, rewrite the task.
|
|
20
21
|
- [Investigate a PR](guided-review.md) — Guided Review.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Overthink — private second look
|
|
2
|
+
|
|
3
|
+
Overthink gives Pi's work a small, skeptical second pass. It checks the direction, the assumptions, the risks and the next move, without making a new agent or interrupting the original conversation. It is inspired by [pi-heads-up](https://github.com/Pringled/pi-extensions/tree/main/packages/pi-heads-up).
|
|
4
|
+
|
|
5
|
+
## Normal use
|
|
6
|
+
|
|
7
|
+
With **automatic checks on** (the factory default), after a **fully settled substantial task** (currently at least three tool results across the run), Overthink sends one extra private assessment to the **same active Pi model, at the same thinking level**. A short note appears above the input only if there is something material to flag. If the reply already covered the issue, or there is nothing important to add, Overthink stays quiet. A question-only turn is not enough to trigger a check.
|
|
8
|
+
|
|
9
|
+
Run `/bro overthink` to **view** the latest assessment. Opening the modal makes **no model request**. The notes never enter Pi's user/assistant conversation and do not direct the agent to perform work.
|
|
10
|
+
|
|
11
|
+
In the modal:
|
|
12
|
+
|
|
13
|
+
- **Ctrl+R** — force a fresh assessment (also works for smaller tasks). A check uses the **current** session context.
|
|
14
|
+
- **Ctrl+E** — edit the questions for **this Pi session**.
|
|
15
|
+
- **?** — show or hide help.
|
|
16
|
+
- **Ctrl+C** — copy the latest findings.
|
|
17
|
+
- **Esc** — close or leave Help.
|
|
18
|
+
- **↑/↓** — scroll.
|
|
19
|
+
|
|
20
|
+
If the last assessment found nothing, the modal says so. The note records the model-relevant session entry it assessed. When a later task (even a trivial turn), compaction, context edit, or branch move changes that position, the viewer explicitly marks the old note **OUTDATED**. Older notes without position metadata are marked **source position not recorded**. Editing questions also marks an existing note as using earlier questions.
|
|
21
|
+
|
|
22
|
+
## Question editor
|
|
23
|
+
|
|
24
|
+
The same ordered, editable list is used in both places:
|
|
25
|
+
|
|
26
|
+
- **Global defaults:** `/bro config → Features → Overthink → Default questions`.
|
|
27
|
+
- **Automatic checks:** `/bro config → Features → Overthink → Automatic checks` toggles the global on/off switch. It starts **On**. Turning it off does not delete questions or notes; the viewer and manual **Ctrl+R** still work.
|
|
28
|
+
- **Session questions:** `/bro overthink`, then **Ctrl+E**.
|
|
29
|
+
|
|
30
|
+
Use **↑/↓** to choose an entry; **Enter** to edit; **Ctrl+N** to add; **Ctrl+X** to delete; **Alt+↑/↓** to reorder; **Ctrl+S** to save; **Esc** to cancel. Saving an empty list disables checks. Unsaved changes are protected by a second-Esc warning.
|
|
31
|
+
|
|
32
|
+
**Ctrl+D** twice resets the editor draft. In the global editor it restores Bro's factory questions; in the session editor it restores the **current global defaults**. Press Ctrl+S to save that reset. A new session inherits the current global defaults once and can then be edited independently. Existing sessions do not change when the global defaults are updated.
|
|
33
|
+
|
|
34
|
+
## Model, cache and privacy
|
|
35
|
+
|
|
36
|
+
Overthink **never** uses the BTW or Bro backend/model setting. It calls Pi's current `modelRegistry.streamSimple` using `buildSessionProjection()` plus a single appended private review instruction. It uses the active physical session model and reasoning level, makes **one call for all questions**, **never executes tools**, and does not add the assessment to the main model context. The request may still contain Pi's tool descriptions as part of the original context; the prompt tells the model to respond in text and not call tools. An unexpected tool-use response is treated as a failed check, not executed. The call sends the session context (including tool evidence exposed by Pi's projection) to that model's provider, under the provider's data policy.
|
|
37
|
+
|
|
38
|
+
This largely preserves the request prefix and **can** reuse provider prompt-cache input. The benefit varies by provider, model, elapsed time, tool/context changes, other extensions' context hooks, and prompt-cache policy. It does not replicate every live Pi context modification. In Pi 1.3 and 1.5, `context_with_system` can observe a prepared context just before a provider call, while `before_provider_request` can observe a provider-specific payload; neither provides a portable, complete, settled-session request that can simply be appended and replayed through `streamSimple`. A recorded pre-call context also lacks the final agent response that Overthink needs to review. Bro therefore keeps the explicit projection path until a real provider-side comparison shows a safe improvement. **A cached prefix is not free:** high thinking effort can add substantial reasoning tokens on top of the visible-output limit, and both output and reasoning can cost money. The viewer shows *provider-reported* input, cache-read, cache-write, output and reasoning tokens when available. It labels USD cost separately as **estimated cost** from Pi's model price table, not a provider-billed amount; subscription or OAuth users may pay differently. A reported cache-read value of **0** could mean no hit **or unsupported caching**; it is not proof that caching works. Some providers report no usage fields. Check actual billed cost before treating Overthink as cheap.
|
|
39
|
+
|
|
40
|
+
Overthink stores editable global defaults in `getAgentDir()/bro-overthink-defaults.json` and session questions plus the **latest** private check in Pi session `custom` entries (`bro-overthink`). Those entries are outside LLM prompts. Session forks inherit their branch's last state; later edits are independent. The viewer resolves only the **latest** note. Pi stores session custom entries by appending, so **each completed check adds another entry to the session file**, even though Bro does not provide a history browser or archive. This is a deliberate reuse of Pi's branch-aware persistence, not a claim that only one physical record exists. Keep this storage growth in mind for long sessions. It does not write to your project files.
|
|
41
|
+
|
|
42
|
+
## Limits and verification
|
|
43
|
+
|
|
44
|
+
- The second look is self-criticism by the **same** model; it is not an independent external review. Use `bro_advisor` when a genuinely separate view matters.
|
|
45
|
+
- An automatic check can fail without an on-screen error; it must not disturb the main run. Force retry in the modal to see the error.
|
|
46
|
+
- Automatic work is triggered at Pi's `agent_settled` lifecycle event (supported from Pi 1.3), not `agent_end`. The earlier event collects tool-use evidence; only a normal final settlement, without queued messages, may start the check. A new agent turn, model change, question edit or session navigation cancels or invalidates stale checks. A returned note is not proof that every listed concern is true.
|
|
47
|
+
- Running Heads-up and Overthink together can cause **two extra calls** after a substantive turn. Disable one if that cost is not wanted.
|
|
48
|
+
- Host events and cache accounting vary. See [manual qualification](TESTING.md) before claiming live TUI, PiG or cache-cost behavior.
|
|
@@ -20,7 +20,7 @@ text directly captures a new source the same way.
|
|
|
20
20
|
|
|
21
21
|
## Default command suggestions
|
|
22
22
|
|
|
23
|
-
Typing `/bro ` displays
|
|
23
|
+
Typing `/bro ` displays ten task-level suggestions: `simplify`, `show`, `btw`, `guided-review`, `architecture`, `elevate`, `grill-me`, `remember-me`, `overthink`, and `config`. Every other documented command remains callable and discoverable by typing its prefix (e.g., `/bro doc`).
|
|
24
24
|
|
|
25
25
|
## Commands
|
|
26
26
|
|
|
@@ -37,11 +37,12 @@ Typing `/bro ` displays nine task-level suggestions: `simplify`, `show`, `btw`,
|
|
|
37
37
|
| `/bro doctor` | Check Bro's settings, preferences, and each selected backend, with the effective backend/model/effort per feature. |
|
|
38
38
|
| `/bro mode [brief\|balanced\|faithful]` | View or choose the explanation mode. |
|
|
39
39
|
| `/bro preferences` | View or edit what Bro knows about you and how you like answers; see [Preferences](settings.md#preferences). |
|
|
40
|
-
| `/bro config` | Open Global (defaults, preferences and Doctor) or Features (per-feature overrides, BTW inheritance and Advisor steering). |
|
|
40
|
+
| `/bro config` | Open Global (defaults, preferences and Doctor) or Features (per-feature overrides, BTW inheritance, Overthink automatic checks/default questions and Advisor steering). |
|
|
41
41
|
| `/bro architecture [focus]` | Read the current project and create or refresh `docs/architecture/` with Markdown, an editable graph and offline HTML. Inherits BTW backend settings. [Guide](../features/architecture.md). |
|
|
42
42
|
| `/bro guided-review <PR number or URL>` | Automatically explain and assess a captured PR in local Pi, with source evidence and optional private questions. Opening the same PR restores its saved review. |
|
|
43
43
|
| `/bro guided-review` | List saved reviews; `resume` is an alias. |
|
|
44
44
|
| `/bro remember-me [thought]` | Start a private memory-writing conversation, browse and expand memories, and approve saves explicitly. [Guide](../remember-me.md). |
|
|
45
|
+
| `/bro overthink` | View the latest private automatic second-look notes, without a model call. Ctrl+R retry, Ctrl+E edit session questions, ? help, Ctrl+C copy. [Guide](../overthink.md). |
|
|
45
46
|
| `/bro grill-me [topic]` | Private one-question-at-a-time decision interview. Supply a topic to start fresh; omit it to resume. `/consolidate` drafts a brief; `/insert-last-message` inserts explicitly without sending. `/help` lists modal controls. [Guide](../grill-me.md). |
|
|
46
47
|
| `/bro elevate [request]` | Improve an assignment before execution. Discover up to five gaps, choose useful findings, and insert a revised request without submitting it. **Ctrl+Shift+E** can use your unsent draft. See [Elevate](../elevate.md). |
|
|
47
48
|
| `/bro btw [question]` | Open a side conversation in a modal, seeded with recent main-session context. Starts conversation-only; type `/mode` inside to toggle full permission (read and edit the workspace) without losing the thread. |
|
|
@@ -94,6 +94,8 @@ See [feature guide](../features/architecture.md).
|
|
|
94
94
|
|
|
95
95
|
## Privacy and safety
|
|
96
96
|
|
|
97
|
+
- **Overthink requests:** after a substantive Pi turn, Bro makes one additional provider call using Pi's active model and current session conversation projection (including exposed tool evidence), appending the current session's questions. Overthink never executes tool calls. It may still send tool descriptions copied from Pi's existing context to support cache reuse; a tool-use response is rejected without executing the tool. The provider may retain input under its policy. Results and questions are stored in private Pi session custom entries, not LLM turns; global questions use `bro-overthink-defaults.json` in the agent home. This cannot promise a cache hit or fixed incremental cost. Features → Overthink has an on/off switch (on by default). The viewer displays provider-reported input/cache-read/cache-write/output/reasoning tokens and USD cost when available; a reported cache-read value of zero may mean either a cache miss or unsupported caching. See [Overthink](../overthink.md).
|
|
98
|
+
|
|
97
99
|
- **External requests**: Bro sends the latest completed assistant response,
|
|
98
100
|
pasted text, extracted document text, extracted webpage text, or recent
|
|
99
101
|
session conversation text (tool calls, tool results, reasoning, and images
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-bro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "A sidekick for you and your Pi agent: plain-language explanations, side questions, diagrams, PR review and second opinions, without cluttering the agent's context.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"show",
|
|
31
31
|
"btw",
|
|
32
32
|
"side-conversation",
|
|
33
|
+
"overthink",
|
|
33
34
|
"advisor",
|
|
34
35
|
"second-opinion",
|
|
35
36
|
"codex",
|
|
@@ -51,6 +52,9 @@
|
|
|
51
52
|
"src/elevate-engine.ts",
|
|
52
53
|
"src/grill.ts",
|
|
53
54
|
"src/grill-engine.ts",
|
|
55
|
+
"src/overthink.ts",
|
|
56
|
+
"src/overthink-state.ts",
|
|
57
|
+
"src/overthink-questions-ui.ts",
|
|
54
58
|
"src/remember-store.ts",
|
|
55
59
|
"src/remember-engine.ts",
|
|
56
60
|
"src/remember-tool.ts",
|
|
@@ -84,6 +88,7 @@
|
|
|
84
88
|
"docs/guided-review.md",
|
|
85
89
|
"docs/elevate.md",
|
|
86
90
|
"docs/grill-me.md",
|
|
91
|
+
"docs/overthink.md",
|
|
87
92
|
"docs/remember-me.md",
|
|
88
93
|
"docs/examples.md",
|
|
89
94
|
"docs/features",
|
|
@@ -99,7 +104,7 @@
|
|
|
99
104
|
"scripts": {
|
|
100
105
|
"docs:check": "node scripts/check-docs.mjs && node --test test/docs-quality.test.mjs",
|
|
101
106
|
"typecheck": "tsc --noEmit",
|
|
102
|
-
"test": "npm run typecheck && npm run docs:check && node .github/scripts/release-utils.mjs selftest && node --test test/architecture.test.mjs test/architecture-review.test.mjs test/architecture-controller.test.mjs test/elevate-docs.test.mjs test/elevate.test.ts test/simplify.test.ts test/simplify-modal.test.mjs test/grill.test.ts test/config-navigation.test.ts test/config-navigation-modal.test.mjs test/remember.test.ts test/remember-modal.test.mjs test/review-ux.test.mjs test/review-doctor.test.mjs test/review-versions.test.mjs test/review-regeneration.test.mjs test/review-modal.test.mjs test/review.test.ts test/review-ui.test.mjs test/review-session.test.mjs test/helpers.test.mjs test/prompt.test.ts test/backend.test.ts test/claude.test.ts test/grok.test.ts test/codex.test.ts test/muse.test.ts test/settings.test.ts test/sources.test.ts test/release.test.ts test/show-html.test.ts test/smoke-rpc.test.ts test/ui-capabilities.test.ts test/modal-contract.test.mjs benchmark/*.test.ts && sh ./test/smoke-test.sh",
|
|
107
|
+
"test": "npm run typecheck && npm run docs:check && node .github/scripts/release-utils.mjs selftest && node --test test/architecture.test.mjs test/architecture-review.test.mjs test/architecture-controller.test.mjs test/elevate-docs.test.mjs test/elevate.test.ts test/overthink.test.ts test/simplify.test.ts test/simplify-modal.test.mjs test/grill.test.ts test/config-navigation.test.ts test/config-navigation-modal.test.mjs test/remember.test.ts test/remember-modal.test.mjs test/review-ux.test.mjs test/review-doctor.test.mjs test/review-versions.test.mjs test/review-regeneration.test.mjs test/review-modal.test.mjs test/review.test.ts test/review-ui.test.mjs test/review-session.test.mjs test/helpers.test.mjs test/prompt.test.ts test/backend.test.ts test/claude.test.ts test/grok.test.ts test/codex.test.ts test/muse.test.ts test/settings.test.ts test/sources.test.ts test/release.test.ts test/show-html.test.ts test/smoke-rpc.test.ts test/ui-capabilities.test.ts test/modal-contract.test.mjs benchmark/*.test.ts && sh ./test/smoke-test.sh",
|
|
103
108
|
"benchmark:dry-run": "node benchmark/run.ts dry-run",
|
|
104
109
|
"benchmark:run": "node benchmark/run.ts run",
|
|
105
110
|
"benchmark:report": "node benchmark/run.ts report",
|
package/src/bro.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { setPiHostContext } from "./backend.ts";
|
|
|
12
12
|
import { openElevateModal } from "./elevate.ts";
|
|
13
13
|
import { openArchitectureDocs } from "./architecture.ts";
|
|
14
14
|
import { openGrillModal } from "./grill.ts";
|
|
15
|
+
import { registerOverthink } from "./overthink.ts";
|
|
15
16
|
import { openRememberModal } from "./remember.ts";
|
|
16
17
|
import { openSimplifyWorkflow, type SimplifyState } from "./simplify-ui.ts";
|
|
17
18
|
import { registerAboutUserTool } from "./remember-tool.ts";
|
|
@@ -68,6 +69,8 @@ export * from "./remember-engine.ts";
|
|
|
68
69
|
export * from "./explain-modal.ts";
|
|
69
70
|
export * from "./simplify-engine.ts";
|
|
70
71
|
export * from "./simplify-ui.ts";
|
|
72
|
+
export * from "./overthink-state.ts";
|
|
73
|
+
export * from "./overthink.ts";
|
|
71
74
|
|
|
72
75
|
const COMMANDS = [
|
|
73
76
|
{ value: "architecture", label: "architecture", description: "Document this repository in docs/architecture/ with diagrams and an offline map" },
|
|
@@ -85,6 +88,7 @@ const COMMANDS = [
|
|
|
85
88
|
{ value: "elevate", label: "elevate", description: "Discover missing expert considerations, then improve a request" },
|
|
86
89
|
{ value: "remember-me", label: "remember-me", description: "Help write and review personal memories; agents can unfold them on demand" },
|
|
87
90
|
{ value: "grill-me", label: "grill-me", description: "A private, one-question-at-a-time decision interview" },
|
|
91
|
+
{ value: "overthink", label: "overthink", description: "View private automatic second-look notes and session questions" },
|
|
88
92
|
{ value: "config", label: "config", description: "Global defaults, diagnostics and feature settings" },
|
|
89
93
|
{ value: "advisor", label: "advisor", description: "Check whether the executor's advisor tool is available right now" },
|
|
90
94
|
{ value: "advisor-steer", label: "advisor-steer", description: "View, edit, save, or clear the advisor's persistent steering brief" },
|
|
@@ -93,7 +97,7 @@ const COMMANDS = [
|
|
|
93
97
|
const KNOWN_ACTIONS = new Set(COMMANDS.map((command) => command.value));
|
|
94
98
|
/** Only capability-level commands appear by default; all existing commands remain discoverable by prefix. */
|
|
95
99
|
export const FEATURED_BRO_COMMANDS = [
|
|
96
|
-
"simplify","show","btw","guided-review","architecture","elevate","grill-me","remember-me","config",
|
|
100
|
+
"simplify","show","btw","guided-review","architecture","elevate","grill-me","remember-me","overthink","config",
|
|
97
101
|
] as const;
|
|
98
102
|
const FEATURED_COMMANDS = new Set<string>(FEATURED_BRO_COMMANDS);
|
|
99
103
|
|
|
@@ -121,6 +125,7 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
121
125
|
|
|
122
126
|
registerAdvisorTool(pi);
|
|
123
127
|
registerAboutUserTool(pi);
|
|
128
|
+
const overthink = registerOverthink(pi);
|
|
124
129
|
|
|
125
130
|
pi.registerCommand("bro", {
|
|
126
131
|
description: "Explain text, documents, and webpages; draw session turns; open a side conversation; configure Bro and the executor's advisor tool",
|
|
@@ -321,6 +326,13 @@ export default async function bro(pi: ExtensionAPI) {
|
|
|
321
326
|
return;
|
|
322
327
|
}
|
|
323
328
|
|
|
329
|
+
if (action === "overthink") {
|
|
330
|
+
if (value) { ctx.ui.notify("Use /bro overthink to open private notes; retry inside the modal.", "warning"); return; }
|
|
331
|
+
try { await overthink.open(ctx); }
|
|
332
|
+
catch (error) { ctx.ui.notify("Overthink: " + errorMessage(error), "error"); }
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
324
336
|
if (action === "elevate") {
|
|
325
337
|
if (!hasBroCustomUi(ctx)) { ctx.ui.notify("Use /bro elevate in Pi interactive UI.", "warning"); return; }
|
|
326
338
|
try { await openElevateModal(ctx, value); }
|
|
@@ -11,10 +11,14 @@ import {
|
|
|
11
11
|
} from "./config-navigation.ts";
|
|
12
12
|
|
|
13
13
|
type MenuItem = {value:string;label:string};
|
|
14
|
-
const rowsFor=(screen:ConfigNavScreen):readonly MenuItem[]=>{
|
|
14
|
+
const rowsFor=(screen:ConfigNavScreen, automatic:boolean):readonly MenuItem[]=>{
|
|
15
15
|
if(screen==="root")return CONFIG_ROOT;
|
|
16
16
|
if(screen==="global")return CONFIG_GLOBAL;
|
|
17
17
|
if(screen==="features")return CONFIG_FEATURES;
|
|
18
|
+
if(screen==="overthink")return [
|
|
19
|
+
{value:"overthink-defaults",label:"Default questions — edit starting set"},
|
|
20
|
+
{value:"overthink-toggle",label:"Automatic checks — "+(automatic?"On":"Off")+" (Enter to toggle)"},
|
|
21
|
+
];
|
|
18
22
|
if(screen==="advisor")return [
|
|
19
23
|
{value:"advisor-settings",label:"Backend, model and effort"},
|
|
20
24
|
{value:"advisor-steer",label:"Persistent advisor steering brief"},
|
|
@@ -25,6 +29,7 @@ const rowsFor=(screen:ConfigNavScreen):readonly MenuItem[]=>{
|
|
|
25
29
|
export function createConfigNavigationModal(
|
|
26
30
|
state:ConfigNavState,
|
|
27
31
|
settings:BroSettings,
|
|
32
|
+
automatic = true,
|
|
28
33
|
): (tui:TuiLike,theme:Theme,keys:unknown,done:(result:ConfigNavAction)=>void)=>Component {
|
|
29
34
|
return (tui,theme,_keys,done)=>{
|
|
30
35
|
let closed=false;
|
|
@@ -33,7 +38,7 @@ export function createConfigNavigationModal(
|
|
|
33
38
|
const finish=(result:ConfigNavAction)=>{if(closed)return;closed=true;done(result);};
|
|
34
39
|
const go=(screen:ConfigNavScreen)=>{
|
|
35
40
|
state.screen=screen;
|
|
36
|
-
const items=rowsFor(screen) as SelectItem[];
|
|
41
|
+
const items=rowsFor(screen, automatic) as SelectItem[];
|
|
37
42
|
list=new SelectList(items,Math.min(items.length,Math.max(1,modalHeight(tui)-10)),getSelectListTheme());
|
|
38
43
|
list.onSelect=item=>choose(item.value);
|
|
39
44
|
list.onCancel=back;
|
|
@@ -53,11 +58,16 @@ export function createConfigNavigationModal(
|
|
|
53
58
|
}
|
|
54
59
|
if(state.screen==="features"){
|
|
55
60
|
if(value==="advisor"){go("advisor");return;}
|
|
61
|
+
if(value==="overthink"){go("overthink");return;}
|
|
56
62
|
if(value==="elevate"||value==="grill-me"||value==="remember-me"||value==="architecture"){
|
|
57
63
|
state.inherited=value;go("inherited");return;
|
|
58
64
|
}
|
|
59
65
|
if(value==="explain"||value==="show"||value==="btw"||value==="review")return finish({kind:"settings",page:value});
|
|
60
66
|
}
|
|
67
|
+
if(state.screen==="overthink"){
|
|
68
|
+
if(value==="overthink-defaults")return finish({kind:"overthink-defaults"});
|
|
69
|
+
if(value==="overthink-toggle")return finish({kind:"overthink-toggle"});
|
|
70
|
+
}
|
|
61
71
|
if(state.screen==="advisor"){
|
|
62
72
|
if(value==="advisor-steer")return finish({kind:"advisor-steer"});
|
|
63
73
|
if(value==="advisor-settings")return finish({kind:"settings",page:"advisor"});
|
|
@@ -73,6 +83,7 @@ export function createConfigNavigationModal(
|
|
|
73
83
|
s==="global"?"Bro · config / Global":
|
|
74
84
|
s==="features"?"Bro · config / Features":
|
|
75
85
|
s==="advisor"?"Bro · config / Features / Advisor":
|
|
86
|
+
s==="overthink"?"Bro · config / Features / Overthink":
|
|
76
87
|
"Bro · config / Features / "+(state.inherited??"Inherited");
|
|
77
88
|
};
|
|
78
89
|
const subtitle=()=>{
|
|
@@ -80,6 +91,7 @@ export function createConfigNavigationModal(
|
|
|
80
91
|
if(state.screen==="global")return "Defaults are shared. Doctor runs only when selected.";
|
|
81
92
|
if(state.screen==="features")return "Feature model overrides are optional; inherited features use BTW.";
|
|
82
93
|
if(state.screen==="advisor")return "Steering keeps its own file; changing models does not change steering.";
|
|
94
|
+
if(state.screen==="overthink")return "Automatic checks use the active Pi model. No backend override.";
|
|
83
95
|
if(state.inherited)return inheritedConfigDescription(settings,state.inherited);
|
|
84
96
|
return "Inherits BTW";
|
|
85
97
|
};
|
package/src/config-navigation.ts
CHANGED
|
@@ -4,11 +4,11 @@ import { capabilityBackend, capabilityOverride, capabilityPair } from "./setting
|
|
|
4
4
|
import { reviewBackendNotice } from "./backend.ts";
|
|
5
5
|
|
|
6
6
|
export type ConfigSettingsPage = "defaults" | Capability;
|
|
7
|
-
export type ConfigNavScreen = "root" | "global" | "features" | "advisor" | "inherited";
|
|
7
|
+
export type ConfigNavScreen = "root" | "global" | "features" | "advisor" | "inherited" | "overthink";
|
|
8
8
|
export type ConfigNavState = { screen: ConfigNavScreen; inherited?: "elevate"|"grill-me"|"remember-me"|"architecture" };
|
|
9
9
|
export type ConfigNavAction =
|
|
10
10
|
| {kind:"settings";page:ConfigSettingsPage}
|
|
11
|
-
| {kind:"preferences"|"doctor"|"advisor-steer"|"close"};
|
|
11
|
+
| {kind:"preferences"|"doctor"|"advisor-steer"|"overthink-defaults"|"overthink-toggle"|"close"};
|
|
12
12
|
|
|
13
13
|
export const CONFIG_ROOT = [
|
|
14
14
|
{value:"global",label:"Global — shared defaults, preferences and Doctor"},
|
|
@@ -28,6 +28,7 @@ export const CONFIG_FEATURES = [
|
|
|
28
28
|
{value:"elevate",label:"Elevate — inherits BTW"},
|
|
29
29
|
{value:"grill-me",label:"Grill Me — inherits BTW"},
|
|
30
30
|
{value:"remember-me",label:"Remember Me — inherits BTW"},
|
|
31
|
+
{value:"overthink",label:"Overthink — automatic checks and default questions"},
|
|
31
32
|
{value:"architecture",label:"Architecture Docs — inherits BTW"},
|
|
32
33
|
] as const;
|
|
33
34
|
|
package/src/doctor.ts
CHANGED
|
@@ -22,6 +22,8 @@ import { createConfigNavigationModal } from "./config-navigation-ui.ts";
|
|
|
22
22
|
import { type ConfigNavAction, type ConfigNavState } from "./config-navigation.ts";
|
|
23
23
|
import { showBroModal, showPreferencesModal, showAdvisorSteerModal } from "./explain-modal.ts";
|
|
24
24
|
import { openBroOverlay } from "./modal-kit.ts";
|
|
25
|
+
import { openOverthinkDefaults } from "./overthink.ts";
|
|
26
|
+
import { readAutomaticChecks, writeAutomaticChecks } from "./overthink-state.ts";
|
|
25
27
|
import {
|
|
26
28
|
type AgyEffort,
|
|
27
29
|
type AgyModelFamily,
|
|
@@ -661,8 +663,11 @@ export async function showBroConfigModal(ctx: ExtensionCommandContext, pi: Exten
|
|
|
661
663
|
ctx.ui.notify("Settings unavailable: "+errorMessage(error)+". Global > Doctor can diagnose.", "warning");
|
|
662
664
|
current = { backend:"agy",model:"(unavailable)",effort:"default",mode:"balanced",showTurns:1,overrides:{} };
|
|
663
665
|
}
|
|
666
|
+
let automatic = true;
|
|
667
|
+
try { automatic = await readAutomaticChecks(); }
|
|
668
|
+
catch (error) { ctx.ui.notify("Overthink automatic setting: " + errorMessage(error), "warning"); }
|
|
664
669
|
const selected = await openBroOverlay<ConfigNavAction>(
|
|
665
|
-
ctx, createConfigNavigationModal(navigation,current), {mouse:true}
|
|
670
|
+
ctx, createConfigNavigationModal(navigation,current,automatic), {mouse:true}
|
|
666
671
|
);
|
|
667
672
|
if (!selected || selected.kind === "close") return;
|
|
668
673
|
if (selected.kind === "preferences") {
|
|
@@ -673,6 +678,19 @@ export async function showBroConfigModal(ctx: ExtensionCommandContext, pi: Exten
|
|
|
673
678
|
await showAdvisorSteerModal(ctx,pi);
|
|
674
679
|
continue;
|
|
675
680
|
}
|
|
681
|
+
if (selected.kind === "overthink-toggle") {
|
|
682
|
+
try {
|
|
683
|
+
const next = !(await readAutomaticChecks());
|
|
684
|
+
await writeAutomaticChecks(next);
|
|
685
|
+
ctx.ui.notify("Overthink automatic checks: " + (next ? "On" : "Off"), "info");
|
|
686
|
+
} catch (error) { ctx.ui.notify("Overthink automatic checks: " + errorMessage(error), "error"); }
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (selected.kind === "overthink-defaults") {
|
|
690
|
+
try { await openOverthinkDefaults(ctx); }
|
|
691
|
+
catch (error) { ctx.ui.notify("Overthink defaults: " + errorMessage(error), "error"); }
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
676
694
|
if (selected.kind === "doctor") {
|
|
677
695
|
await showBroModal(ctx,{
|
|
678
696
|
loadingText:"Checking Bro setup…",
|
package/src/explain-modal.ts
CHANGED
|
@@ -414,6 +414,11 @@ Quick reference. Full user guide: https://github.com/tranhoangnguyen03/pi-bro/bl
|
|
|
414
414
|
|
|
415
415
|
- \`/bro grill-me [topic]\` — private decision interview. Use /consolidate, /insert-last-message or /help. Full-access risk: docs/grill-me.md.
|
|
416
416
|
|
|
417
|
+
## Overthink
|
|
418
|
+
|
|
419
|
+
- \`/bro overthink\` — view private automatic second-look notes without running a model. Ctrl+R forces retry; Ctrl+E edits this session's questions; ? shows help; Ctrl+C copies. Uses the active Pi model and thinking level only. No separate backend settings.
|
|
420
|
+
- \`/bro config\` → Features → Overthink provides automatic on/off and editable global starting questions; session edits stay with their Pi session. See docs/overthink.md.
|
|
421
|
+
|
|
417
422
|
## Side conversation
|
|
418
423
|
|
|
419
424
|
- \`/bro btw [question]\` — open a side conversation seeded with recent main-session context. It starts conversation-only; reopening keeps the thread and its mode.
|
|
@@ -429,7 +434,7 @@ Quick reference. Full user guide: https://github.com/tranhoangnguyen03/pi-bro/bl
|
|
|
429
434
|
## Configure and check
|
|
430
435
|
|
|
431
436
|
- \`/bro preferences\` — tell Bro about yourself and how you like answers; added to explain, show, btw, Elevate and Guided Review prompts, never the advisor (**Ctrl+S** save, **Ctrl+K** delete, **Ctrl+C** copy, **Esc** close)
|
|
432
|
-
- \`/bro config\` — Global (shared defaults, preferences, Doctor) and Features (Simplify, Show, BTW, Review, Advisor and BTW-inherited workflows). The same model pickers, safe saves and error handling remain; Esc backs out.
|
|
437
|
+
- \`/bro config\` — Global (shared defaults, preferences, Doctor) and Features (Simplify, Show, BTW, Review, Advisor, Overthink defaults and BTW-inherited workflows). The same model pickers, safe saves and error handling remain; Esc backs out.
|
|
433
438
|
- \`/bro mode [brief|balanced|faithful]\` — view or choose the explanation mode
|
|
434
439
|
- \`/bro doctor\` — check settings, preferences, and every selected backend without running a model turn
|
|
435
440
|
|
package/src/modal-kit.ts
CHANGED
|
@@ -118,10 +118,13 @@ export class Scroller {
|
|
|
118
118
|
private follow = false;
|
|
119
119
|
|
|
120
120
|
/** `followEnd`: scrolling to the end pins the view to new text until the reader scrolls up. */
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
) {}
|
|
121
|
+
private readonly onScroll: () => void;
|
|
122
|
+
private readonly options: { followEnd?: boolean };
|
|
123
|
+
|
|
124
|
+
constructor(onScroll: () => void, options: { followEnd?: boolean } = {}) {
|
|
125
|
+
this.onScroll = onScroll;
|
|
126
|
+
this.options = options;
|
|
127
|
+
}
|
|
125
128
|
|
|
126
129
|
view(lines: string[], rows: number): string[] {
|
|
127
130
|
this.rows = Math.max(1, rows);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/** One question list editor for global defaults and session snapshots. */
|
|
2
|
+
import { Input, matchesKey, truncateToWidth, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
import { modalHeight, openBroOverlay, renderFrame, Scroller, formatNotice, type Theme } from "./modal-kit.ts";
|
|
4
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { checkQuestions, MAX_OVERTHINK_QUESTIONS, MAX_OVERTHINK_QUESTION_CHARS } from "./overthink-state.ts";
|
|
6
|
+
import { errorMessage } from "./util.ts";
|
|
7
|
+
|
|
8
|
+
type EditorOptions = {
|
|
9
|
+
title: string; questions: string[]; reset: () => Promise<string[]>;
|
|
10
|
+
save: (questions: string[]) => Promise<void> | void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function createOverthinkQuestionEditor(options: EditorOptions) {
|
|
14
|
+
return (tui: TUI, theme: Theme, _keys: unknown, done: (value?: void) => void): Component => {
|
|
15
|
+
let questions = [...options.questions];
|
|
16
|
+
let saved = JSON.stringify(questions);
|
|
17
|
+
let cursor = 0;
|
|
18
|
+
let mode: "list" | "edit" = "list";
|
|
19
|
+
let input = new Input();
|
|
20
|
+
let editingIndex = -1;
|
|
21
|
+
let pending = false;
|
|
22
|
+
let closed = false;
|
|
23
|
+
let resetArmed = false;
|
|
24
|
+
let discardArmed = false;
|
|
25
|
+
let notice = "";
|
|
26
|
+
const scroller = new Scroller(() => tui.requestRender());
|
|
27
|
+
const dirty = () => JSON.stringify(questions) !== saved;
|
|
28
|
+
const warn = (message: string, tone: "info" | "success" | "error" | "warning" = "info") => {
|
|
29
|
+
notice = formatNotice(theme, message, tone); tui.requestRender();
|
|
30
|
+
};
|
|
31
|
+
const beginEdit = (index: number) => {
|
|
32
|
+
mode = "edit"; editingIndex = index;
|
|
33
|
+
input = new Input();
|
|
34
|
+
input.setValue(questions[index] ?? "");
|
|
35
|
+
input.onSubmit = (value: string) => {
|
|
36
|
+
const trimmed = value.trim();
|
|
37
|
+
if (!trimmed || trimmed.length > MAX_OVERTHINK_QUESTION_CHARS || /[\r\n]/.test(trimmed)) {
|
|
38
|
+
warn("Enter one nonempty question (max " + MAX_OVERTHINK_QUESTION_CHARS + " characters).", "warning"); return;
|
|
39
|
+
}
|
|
40
|
+
if (editingIndex === questions.length) questions.push(trimmed);
|
|
41
|
+
else questions[editingIndex] = trimmed;
|
|
42
|
+
mode = "list"; cursor = editingIndex; resetArmed = false;
|
|
43
|
+
discardArmed = false; notice = ""; tui.requestRender();
|
|
44
|
+
};
|
|
45
|
+
tui.requestRender();
|
|
46
|
+
};
|
|
47
|
+
const save = async () => {
|
|
48
|
+
if (pending) return;
|
|
49
|
+
let clean: string[];
|
|
50
|
+
try { clean = checkQuestions(questions); }
|
|
51
|
+
catch (error) { warn(errorMessage(error), "error"); return; }
|
|
52
|
+
pending = true; tui.requestRender();
|
|
53
|
+
try { await options.save(clean); saved = JSON.stringify(clean); questions = clean; warn("Questions saved.", "success"); }
|
|
54
|
+
catch (error) { warn("Save failed: " + errorMessage(error), "error"); }
|
|
55
|
+
finally { pending = false; tui.requestRender(); }
|
|
56
|
+
};
|
|
57
|
+
const reset = async () => {
|
|
58
|
+
pending = true; tui.requestRender();
|
|
59
|
+
try {
|
|
60
|
+
questions = checkQuestions(await options.reset());
|
|
61
|
+
cursor = 0; scroller.toStart(); resetArmed = false; discardArmed = false;
|
|
62
|
+
warn("Reset in editor. Ctrl+S to save.", "warning");
|
|
63
|
+
} catch (error) { warn("Reset failed: " + errorMessage(error), "error"); }
|
|
64
|
+
finally { pending = false; tui.requestRender(); }
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
render(width: number) {
|
|
68
|
+
const height = modalHeight(tui);
|
|
69
|
+
const list = questions.length ? questions.map((question, index) =>
|
|
70
|
+
(index === cursor ? theme.fg("accent", "> ") : " ") +
|
|
71
|
+
theme.fg(index === cursor ? "accent" : "text", truncateToWidth(String(index + 1) + ". " + question, Math.max(8, width - 6), "…"))
|
|
72
|
+
) : [theme.fg("muted", "No questions. Add one, or save an empty list to disable checks.")];
|
|
73
|
+
return renderFrame(theme, width, height, {
|
|
74
|
+
header: () => theme.fg("accent", theme.bold(options.title)) + (dirty() ? " *" : ""),
|
|
75
|
+
body: (rows, inner) => mode === "edit"
|
|
76
|
+
? ["Edit question:", ...input.render(inner)].slice(0, rows)
|
|
77
|
+
: scroller.view(list, rows),
|
|
78
|
+
notice,
|
|
79
|
+
hints: mode === "edit" ? "Enter apply · Esc cancel edit" :
|
|
80
|
+
"↑/↓ select · Enter edit · Ctrl+N add · Ctrl+X delete · Alt+↑/↓ move · Ctrl+D reset · Ctrl+S save · Esc close",
|
|
81
|
+
fill: true,
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
invalidate() {},
|
|
85
|
+
handleInput(data: string) {
|
|
86
|
+
if (pending || closed) return;
|
|
87
|
+
if (mode === "edit") {
|
|
88
|
+
if (matchesKey(data, "escape")) { mode = "list"; notice = ""; tui.requestRender(); return; }
|
|
89
|
+
input.handleInput(data); tui.requestRender(); return;
|
|
90
|
+
}
|
|
91
|
+
if (matchesKey(data, "escape")) {
|
|
92
|
+
if (resetArmed) { resetArmed = false; warn("Reset cancelled."); return; }
|
|
93
|
+
if (dirty() && !discardArmed) { discardArmed = true; warn("Unsaved questions. Esc again to discard, Ctrl+S to save.", "warning"); return; }
|
|
94
|
+
closed = true; done(); return;
|
|
95
|
+
}
|
|
96
|
+
discardArmed = false;
|
|
97
|
+
if (matchesKey(data, "ctrl+s")) { void save(); return; }
|
|
98
|
+
if (matchesKey(data, "ctrl+n")) {
|
|
99
|
+
if (questions.length >= MAX_OVERTHINK_QUESTIONS) { warn("Maximum " + MAX_OVERTHINK_QUESTIONS + " questions.", "warning"); return; }
|
|
100
|
+
beginEdit(questions.length); return;
|
|
101
|
+
}
|
|
102
|
+
if (matchesKey(data, "ctrl+d")) {
|
|
103
|
+
if (!resetArmed) { resetArmed = true; warn("Ctrl+D again to reset this list, then Ctrl+S to save.", "warning"); return; }
|
|
104
|
+
void reset(); return;
|
|
105
|
+
}
|
|
106
|
+
resetArmed = false;
|
|
107
|
+
if (matchesKey(data, "ctrl+x") && questions.length) {
|
|
108
|
+
questions.splice(cursor, 1); cursor = Math.min(cursor, Math.max(0, questions.length - 1));
|
|
109
|
+
warn("Question removed from draft. Ctrl+S to save."); return;
|
|
110
|
+
}
|
|
111
|
+
if (matchesKey(data, "alt+up") && cursor > 0) {
|
|
112
|
+
[questions[cursor - 1], questions[cursor]] = [questions[cursor]!, questions[cursor - 1]!];
|
|
113
|
+
cursor--; tui.requestRender(); return;
|
|
114
|
+
}
|
|
115
|
+
if (matchesKey(data, "alt+down") && cursor < questions.length - 1) {
|
|
116
|
+
[questions[cursor + 1], questions[cursor]] = [questions[cursor]!, questions[cursor + 1]!];
|
|
117
|
+
cursor++; tui.requestRender(); return;
|
|
118
|
+
}
|
|
119
|
+
if (matchesKey(data, "up")) { cursor = Math.max(0, cursor - 1); scroller.by(-1); tui.requestRender(); return; }
|
|
120
|
+
if (matchesKey(data, "down")) { cursor = Math.min(questions.length - 1, cursor + 1); scroller.by(1); tui.requestRender(); return; }
|
|
121
|
+
if (matchesKey(data, "return") || matchesKey(data, "enter")) {
|
|
122
|
+
if (questions.length) beginEdit(cursor);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function openOverthinkQuestionEditor(ctx: ExtensionCommandContext, options: EditorOptions): Promise<void> {
|
|
130
|
+
await openBroOverlay<void>(ctx, createOverthinkQuestionEditor(options));
|
|
131
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/** Small private Overthink settings and session entries; custom entries never enter LLM context. */
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { getAgentDir, type ExtensionAPI, type SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { isRecord } from "./util.ts";
|
|
7
|
+
|
|
8
|
+
export const OVERTHINK_ENTRY = "bro-overthink";
|
|
9
|
+
export const OVERTHINK_DEFAULTS_FILE = "bro-overthink-defaults.json";
|
|
10
|
+
export const FACTORY_QUESTIONS = [
|
|
11
|
+
"Are we going in the right direction?",
|
|
12
|
+
"Where are we now, where do we want to be, and where are we actually going?",
|
|
13
|
+
"Are we solving the real problem, or just completing the tasks in front of us?",
|
|
14
|
+
"What are we taking for granted that might be wrong?",
|
|
15
|
+
"What important risks or failure modes are we overlooking?",
|
|
16
|
+
"Have we drifted from the original intent or added complexity without enough value?",
|
|
17
|
+
"What is the most useful correction or next move we can make now?",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
export const MAX_OVERTHINK_QUESTIONS = 32;
|
|
21
|
+
export const MAX_OVERTHINK_QUESTION_CHARS = 500;
|
|
22
|
+
const MAX_NOTE_CHARS = 8000;
|
|
23
|
+
export type OverthinkUsage = { cacheRead?: number; cacheWrite?: number; input?: number; output?: number; reasoning?: number; costTotal?: number };
|
|
24
|
+
export type OverthinkNote = {
|
|
25
|
+
text: string; checkedAt: string; model: string; questionKey: string;
|
|
26
|
+
/** Last model-relevant session entry at the time of the assessment. */
|
|
27
|
+
sourcePosition?: string;
|
|
28
|
+
usage?: OverthinkUsage;
|
|
29
|
+
};
|
|
30
|
+
export type OverthinkSession = { questions?: string[]; note?: OverthinkNote; initialized: boolean };
|
|
31
|
+
|
|
32
|
+
export function checkQuestions(value: unknown): string[] {
|
|
33
|
+
if (!Array.isArray(value) || value.length > MAX_OVERTHINK_QUESTIONS) {
|
|
34
|
+
throw new Error("Overthink allows at most 32 questions.");
|
|
35
|
+
}
|
|
36
|
+
const questions = value.map((item: unknown) => {
|
|
37
|
+
if (typeof item !== "string" || !item.trim() || item.length > MAX_OVERTHINK_QUESTION_CHARS || /[\r\n\u0000-\u001f]/.test(item)) {
|
|
38
|
+
throw new Error("Each question must be a single nonempty line of at most 500 characters.");
|
|
39
|
+
}
|
|
40
|
+
return item.trim();
|
|
41
|
+
});
|
|
42
|
+
return questions;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function questionKey(questions: readonly string[]): string { return JSON.stringify(questions); }
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Session location represented by an assessment. Ignore Bro's custom entries
|
|
49
|
+
* so recording a note or editing questions does not itself make the note stale.
|
|
50
|
+
* Content edits and compaction count: they can change what the model sees.
|
|
51
|
+
*/
|
|
52
|
+
export function overthinkContextPosition(branch: readonly SessionEntry[]): string | undefined {
|
|
53
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
54
|
+
const entry = branch[i]!;
|
|
55
|
+
if (entry.type === "message" || entry.type === "custom_message" ||
|
|
56
|
+
entry.type === "context_edit" || entry.type === "compaction" || entry.type === "branch_summary") {
|
|
57
|
+
return entry.id;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function overthinkIsStale(note: OverthinkNote | undefined, position: string | undefined): boolean {
|
|
64
|
+
return Boolean(note && (!note.sourcePosition || note.sourcePosition !== position));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
export function resolveOverthinkSession(branch: readonly SessionEntry[]): OverthinkSession {
|
|
69
|
+
const state: OverthinkSession = { initialized: false };
|
|
70
|
+
for (const entry of branch) {
|
|
71
|
+
if (entry.type !== "custom" || entry.customType !== OVERTHINK_ENTRY || !isRecord(entry.data) || entry.data.version !== 1) continue;
|
|
72
|
+
if (entry.data.kind === "questions") {
|
|
73
|
+
try { state.questions = checkQuestions(entry.data.questions); state.initialized = true; }
|
|
74
|
+
catch { /* Ignore damaged historical records without granting them authority. */ }
|
|
75
|
+
} else if (entry.data.kind === "note") {
|
|
76
|
+
const data = entry.data;
|
|
77
|
+
if (typeof data.text === "string" && data.text.length <= MAX_NOTE_CHARS &&
|
|
78
|
+
typeof data.checkedAt === "string" && typeof data.model === "string" &&
|
|
79
|
+
typeof data.questionKey === "string") {
|
|
80
|
+
const note: OverthinkNote = {
|
|
81
|
+
text: data.text, checkedAt: data.checkedAt, model: data.model, questionKey: data.questionKey,
|
|
82
|
+
...(typeof data.sourcePosition === "string" && data.sourcePosition ? { sourcePosition: data.sourcePosition } : {}),
|
|
83
|
+
};
|
|
84
|
+
if (isRecord(data.usage)) {
|
|
85
|
+
const usage: OverthinkUsage = {};
|
|
86
|
+
for (const key of ["cacheRead", "cacheWrite", "input", "output", "reasoning", "costTotal"] as const) {
|
|
87
|
+
const val = data.usage[key];
|
|
88
|
+
if (typeof val === "number" && Number.isFinite(val) && val >= 0) usage[key] = val;
|
|
89
|
+
}
|
|
90
|
+
note.usage = usage;
|
|
91
|
+
}
|
|
92
|
+
state.note = note;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return state;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function globalQuestionsFile(dir = getAgentDir()): string {
|
|
100
|
+
return join(dir, OVERTHINK_DEFAULTS_FILE);
|
|
101
|
+
}
|
|
102
|
+
export type OverthinkGlobal = { questions: string[]; automatic: boolean };
|
|
103
|
+
/** Missing flag defaults to on, including files written before the on/off switch existed. */
|
|
104
|
+
export async function readGlobalOverthink(dir = getAgentDir()): Promise<OverthinkGlobal> {
|
|
105
|
+
const file = globalQuestionsFile(dir);
|
|
106
|
+
let info;
|
|
107
|
+
try { info = await lstat(file); }
|
|
108
|
+
catch (error) {
|
|
109
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { questions: [...FACTORY_QUESTIONS], automatic: true };
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > 32768) throw new Error("Unsafe or oversized Overthink defaults file: " + file);
|
|
113
|
+
const data = JSON.parse(await readFile(file, "utf8")) as unknown;
|
|
114
|
+
if (!isRecord(data) || data.version !== 1 ||
|
|
115
|
+
(data.automatic !== undefined && typeof data.automatic !== "boolean")) {
|
|
116
|
+
throw new Error("Unsupported Overthink defaults format: " + file);
|
|
117
|
+
}
|
|
118
|
+
return { questions: checkQuestions(data.questions), automatic: data.automatic ?? true };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function readGlobalQuestions(dir = getAgentDir()): Promise<string[]> {
|
|
122
|
+
return (await readGlobalOverthink(dir)).questions;
|
|
123
|
+
}
|
|
124
|
+
export async function readAutomaticChecks(dir = getAgentDir()): Promise<boolean> {
|
|
125
|
+
return (await readGlobalOverthink(dir)).automatic;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function writeGlobalOverthink(settings: OverthinkGlobal, dir = getAgentDir()): Promise<void> {
|
|
129
|
+
const cleaned = checkQuestions([...settings.questions]);
|
|
130
|
+
if (typeof settings.automatic !== "boolean") throw new Error("Automatic checks must be on or off.");
|
|
131
|
+
await mkdir(dir, { recursive: true });
|
|
132
|
+
const file = globalQuestionsFile(dir);
|
|
133
|
+
try {
|
|
134
|
+
const info = await lstat(file);
|
|
135
|
+
if (!info.isFile() || info.isSymbolicLink()) throw new Error("Unsafe Overthink defaults file: " + file);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
138
|
+
}
|
|
139
|
+
const temp = file + "." + randomUUID() + ".tmp";
|
|
140
|
+
try {
|
|
141
|
+
await writeFile(temp, JSON.stringify({ version: 1, questions: cleaned, automatic: settings.automatic }, null, 2) + "\n", {
|
|
142
|
+
encoding: "utf8", mode: 0o600, flag: "wx", flush: true,
|
|
143
|
+
});
|
|
144
|
+
await rename(temp, file);
|
|
145
|
+
} finally { await rm(temp, { force: true }).catch(() => undefined); }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function writeGlobalQuestions(questions: readonly string[], dir = getAgentDir()): Promise<void> {
|
|
149
|
+
const previous = await readGlobalOverthink(dir);
|
|
150
|
+
await writeGlobalOverthink({ ...previous, questions: checkQuestions([...questions]) }, dir);
|
|
151
|
+
}
|
|
152
|
+
export async function writeAutomaticChecks(automatic: boolean, dir = getAgentDir()): Promise<void> {
|
|
153
|
+
const previous = await readGlobalOverthink(dir);
|
|
154
|
+
await writeGlobalOverthink({ ...previous, automatic }, dir);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function ensureOverthinkSession(pi: ExtensionAPI, branch: readonly SessionEntry[]): Promise<OverthinkSession> {
|
|
158
|
+
const current = resolveOverthinkSession(branch);
|
|
159
|
+
if (current.initialized) return current;
|
|
160
|
+
const questions = await readGlobalQuestions();
|
|
161
|
+
pi.appendEntry(OVERTHINK_ENTRY, { version: 1, kind: "questions", questions });
|
|
162
|
+
return { ...current, initialized: true, questions };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function saveSessionQuestions(pi: ExtensionAPI, questions: readonly string[]): void {
|
|
166
|
+
pi.appendEntry(OVERTHINK_ENTRY, { version: 1, kind: "questions", questions: checkQuestions([...questions]) });
|
|
167
|
+
}
|
|
168
|
+
export function saveOverthinkNote(pi: ExtensionAPI, note: OverthinkNote): void {
|
|
169
|
+
pi.appendEntry(OVERTHINK_ENTRY, { version: 1, kind: "note", ...note });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function overthinkPrompt(questions: readonly string[]): string {
|
|
173
|
+
return [
|
|
174
|
+
"[Bro Overthink: private second look]",
|
|
175
|
+
"Reassess the current work using the preceding Pi conversation as evidence. Consider these user-written questions:",
|
|
176
|
+
...questions.map((q, i) => String(i + 1) + ". " + q),
|
|
177
|
+
"Find only material points that could change the next decision or the quality of the work.",
|
|
178
|
+
"Prefer direction, assumptions, evidence, risks, coherence, and the next useful move. Do not fabricate problems, demand checks for hypothetical edge cases, or restate concerns already clear in the last assistant reply.",
|
|
179
|
+
"Write in the user's language. Keep it short: at most three specific findings with evidence and a useful next move. Answer in plain text only; do not call tools or take actions. No mandatory template or question-by-question answers.",
|
|
180
|
+
"When nothing material is worth bringing up, return exactly NONE.",
|
|
181
|
+
].join("\n");
|
|
182
|
+
}
|
|
183
|
+
export function parseOverthinkAnswer(text: string): string {
|
|
184
|
+
const cleaned = text.replace(/\u001b\[[0-9;]*m/g, "").trim();
|
|
185
|
+
if (/^none\.?$/i.test(cleaned)) return "";
|
|
186
|
+
return cleaned.slice(0, MAX_NOTE_CHARS);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Only use provider-reported values; absence means unknown, never zero. */
|
|
190
|
+
export function readOverthinkUsage(raw: unknown): OverthinkUsage | undefined {
|
|
191
|
+
if (!isRecord(raw)) return undefined;
|
|
192
|
+
const usage: OverthinkUsage = {};
|
|
193
|
+
const fields = { cacheRead: ["cacheReadTokens", "cacheRead"], cacheWrite: ["cacheWriteTokens", "cacheWrite"], input: ["inputTokens", "input"], output: ["outputTokens", "output"], reasoning: ["reasoningTokens", "reasoning"] } as const;
|
|
194
|
+
for (const key of Object.keys(fields) as (keyof typeof fields)[]) {
|
|
195
|
+
for (const field of fields[key]) {
|
|
196
|
+
const value = raw[field];
|
|
197
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) { usage[key] = value; break; }
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (isRecord(raw.cost) && typeof raw.cost.total === "number" && Number.isFinite(raw.cost.total) && raw.cost.total >= 0) {
|
|
201
|
+
usage.costTotal = raw.cost.total;
|
|
202
|
+
}
|
|
203
|
+
return Object.keys(usage).length ? usage : undefined;
|
|
204
|
+
}
|
package/src/overthink.ts
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/** Automatic same-model second look. No separate CLI, agent or model override. */
|
|
2
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { convertToLlm, copyToClipboard } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { normalizeContext } from "@earendil-works/pi-ai";
|
|
5
|
+
import { matchesKey, wrapTextWithAnsi, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
6
|
+
import { modalHeight, openBroOverlay, renderFrame, Scroller, formatNotice, type Theme } from "./modal-kit.ts";
|
|
7
|
+
import { hasBroCustomUi } from "./ui-capabilities.ts";
|
|
8
|
+
import {
|
|
9
|
+
FACTORY_QUESTIONS, ensureOverthinkSession, overthinkPrompt, parseOverthinkAnswer,
|
|
10
|
+
questionKey, readAutomaticChecks, readGlobalQuestions, readOverthinkUsage, resolveOverthinkSession,
|
|
11
|
+
overthinkContextPosition, overthinkIsStale,
|
|
12
|
+
saveOverthinkNote, saveSessionQuestions, writeGlobalQuestions,
|
|
13
|
+
type OverthinkNote,
|
|
14
|
+
} from "./overthink-state.ts";
|
|
15
|
+
import { openOverthinkQuestionEditor } from "./overthink-questions-ui.ts";
|
|
16
|
+
import { errorMessage } from "./util.ts";
|
|
17
|
+
|
|
18
|
+
const MIN_TOOL_RESULTS = 3;
|
|
19
|
+
const WIDGET = "bro-overthink";
|
|
20
|
+
const NOTE_TIMEOUT_MS = 90_000;
|
|
21
|
+
|
|
22
|
+
type ViewerAction = "close" | "edit";
|
|
23
|
+
type CheckResult = OverthinkNote;
|
|
24
|
+
type CheckContext = ExtensionContext | ExtensionCommandContext;
|
|
25
|
+
|
|
26
|
+
export type OverthinkRunEvidence = { toolResults: number; normalFinish: boolean };
|
|
27
|
+
/** Collect evidence at each agent_end, but never start a check until agent_settled. */
|
|
28
|
+
export function accumulateOverthinkEvidence(
|
|
29
|
+
prior: OverthinkRunEvidence | undefined,
|
|
30
|
+
event: { messages: readonly { role: string; stopReason?: string }[] },
|
|
31
|
+
): OverthinkRunEvidence {
|
|
32
|
+
const last = event.messages.at(-1);
|
|
33
|
+
return {
|
|
34
|
+
toolResults: (prior?.toolResults ?? 0) + event.messages.filter(m => m.role === "toolResult").length,
|
|
35
|
+
normalFinish: last?.role === "assistant" && last.stopReason === "stop",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function overthinkReady(evidence: OverthinkRunEvidence | undefined, aborted: boolean, pending: boolean): boolean {
|
|
40
|
+
return !aborted && !pending && Boolean(evidence?.normalFinish && evidence.toolResults >= MIN_TOOL_RESULTS);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
function noteWidget(ctx: CheckContext, note?: string): void {
|
|
45
|
+
if (ctx.mode !== "tui") return;
|
|
46
|
+
ctx.ui.setWidget(WIDGET, note
|
|
47
|
+
? (_tui, theme) => ({
|
|
48
|
+
invalidate() {},
|
|
49
|
+
render(width: number) {
|
|
50
|
+
const label = "◇ Overthink: ";
|
|
51
|
+
const lines = wrapTextWithAnsi(note.replace(/\s+/g, " "), Math.max(12, width - label.length - 2)).slice(0, 3);
|
|
52
|
+
return ["", ...lines.map((line, index) =>
|
|
53
|
+
(index === 0 ? theme.fg("warning", label) : " ".repeat(label.length)) + theme.fg("muted", line)
|
|
54
|
+
), theme.fg("dim", " /bro overthink to view the private assessment")];
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
: undefined);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function questionStatus(note: OverthinkNote | undefined, questions: string[], position?: string): string {
|
|
61
|
+
if (!note) return "No assessment yet.";
|
|
62
|
+
const changed = note.questionKey !== questionKey(questions) ? " · questions changed since check" : "";
|
|
63
|
+
const freshness = overthinkIsStale(note, position)
|
|
64
|
+
? (note.sourcePosition ? " · OUTDATED: conversation has changed since this check" : " · source position not recorded")
|
|
65
|
+
: " · current for this session position";
|
|
66
|
+
return "Checked: " + note.checkedAt + " · " + note.model + freshness + changed;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A reported zero may mean a miss OR a provider that does not support cache accounting. */
|
|
70
|
+
export function usageStatus(note: OverthinkNote | undefined): string[] {
|
|
71
|
+
const u = note?.usage;
|
|
72
|
+
if (!u) return ["Cost/usage: not reported by provider."];
|
|
73
|
+
const totals = [
|
|
74
|
+
u.input !== undefined ? "input " + u.input.toLocaleString("en-US") : undefined,
|
|
75
|
+
u.cacheRead !== undefined ? "cache read " + u.cacheRead.toLocaleString("en-US") : undefined,
|
|
76
|
+
u.cacheWrite !== undefined ? "cache write " + u.cacheWrite.toLocaleString("en-US") : undefined,
|
|
77
|
+
u.output !== undefined ? "output " + u.output.toLocaleString("en-US") : undefined,
|
|
78
|
+
u.reasoning !== undefined ? "reasoning " + u.reasoning.toLocaleString("en-US") : undefined,
|
|
79
|
+
].filter((value): value is string => value !== undefined);
|
|
80
|
+
const cost = u.costTotal !== undefined ? " · est. cost $" + u.costTotal.toFixed(6) + " (Pi price table; not billed amount)" : "";
|
|
81
|
+
return [
|
|
82
|
+
"Provider-reported tokens: " + (totals.length ? totals.join(" · ") : "token counts unavailable") + cost,
|
|
83
|
+
...(u.cacheRead === 0 ? ["Cache read 0 can mean no hit or caching unsupported."] : []),
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createOverthinkViewer(
|
|
88
|
+
read: () => { questions: string[]; note?: OverthinkNote; position?: string },
|
|
89
|
+
retry: () => Promise<CheckResult>,
|
|
90
|
+
) {
|
|
91
|
+
return (tui: TUI, theme: Theme, _keys: unknown, done: (action: ViewerAction) => void): Component => {
|
|
92
|
+
let help = false;
|
|
93
|
+
let busy = false;
|
|
94
|
+
let closed = false;
|
|
95
|
+
let notice = "";
|
|
96
|
+
const scroller = new Scroller(() => tui.requestRender());
|
|
97
|
+
const copy = async () => {
|
|
98
|
+
const note = read().note;
|
|
99
|
+
if (!note?.text) { notice = formatNotice(theme, "Nothing to copy yet.", "warning"); tui.requestRender(); return; }
|
|
100
|
+
try { await copyToClipboard(note.text); notice = formatNotice(theme, "Assessment copied.", "success"); }
|
|
101
|
+
catch (error) { notice = formatNotice(theme, "Copy failed: " + errorMessage(error), "error"); }
|
|
102
|
+
tui.requestRender();
|
|
103
|
+
};
|
|
104
|
+
const rerun = async () => {
|
|
105
|
+
if (busy || closed) return;
|
|
106
|
+
busy = true; notice = ""; tui.requestRender();
|
|
107
|
+
try {
|
|
108
|
+
const result = await retry();
|
|
109
|
+
if (!closed) { scroller.toStart(); notice = formatNotice(theme, result.text ? "Assessment refreshed." : "No material concerns found.", "success"); }
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (!closed) notice = formatNotice(theme, "Retry failed: " + errorMessage(error), "error");
|
|
112
|
+
} finally { busy = false; if (!closed) tui.requestRender(); }
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
render(width: number) {
|
|
116
|
+
const { questions, note, position } = read();
|
|
117
|
+
const inner = Math.max(1, width - 2);
|
|
118
|
+
const body = help
|
|
119
|
+
? [
|
|
120
|
+
"Overthink checks substantive Pi tasks automatically with the active session model.",
|
|
121
|
+
"Results are private and never enter the main conversation.",
|
|
122
|
+
"",
|
|
123
|
+
"Ctrl+R Force a fresh check (even after a small task)",
|
|
124
|
+
"Ctrl+E Edit this session's questions",
|
|
125
|
+
"? Toggle help",
|
|
126
|
+
"Ctrl+C Copy latest findings",
|
|
127
|
+
"Esc Close / back from help",
|
|
128
|
+
"",
|
|
129
|
+
"No separate backend or model picker. Cached input is measured when the provider reports it.",
|
|
130
|
+
]
|
|
131
|
+
: [
|
|
132
|
+
questionStatus(note, questions, position),
|
|
133
|
+
"Questions: " + questions.length + (questions.length ? "" : " (checks disabled)"),
|
|
134
|
+
...usageStatus(note),
|
|
135
|
+
"",
|
|
136
|
+
busy ? "Checking the current Pi session…" : note
|
|
137
|
+
? note.text || "No material concerns found in the last check."
|
|
138
|
+
: "No assessment yet. Bro checks automatically after meaningful work. Ctrl+R forces one now.",
|
|
139
|
+
];
|
|
140
|
+
const lines = body.flatMap(line => wrapTextWithAnsi(line, inner));
|
|
141
|
+
return renderFrame(theme, width, modalHeight(tui), {
|
|
142
|
+
header: () => theme.fg("accent", theme.bold("Bro · Overthink")) + (busy ? " · running" : ""),
|
|
143
|
+
body: (rows) => scroller.view(lines, rows),
|
|
144
|
+
notice,
|
|
145
|
+
hints: "↑/↓ scroll · Ctrl+R retry · Ctrl+E session questions · ? help · Ctrl+C copy · Esc close",
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
invalidate() {},
|
|
149
|
+
handleInput(data: string) {
|
|
150
|
+
if (matchesKey(data, "escape")) {
|
|
151
|
+
if (help) { help = false; scroller.toStart(); tui.requestRender(); }
|
|
152
|
+
else { closed = true; done("close"); }
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (matchesKey(data, "?")) { help = !help; scroller.toStart(); tui.requestRender(); return; }
|
|
156
|
+
if (matchesKey(data, "ctrl+r")) { void rerun(); return; }
|
|
157
|
+
if (matchesKey(data, "ctrl+c")) { void copy(); return; }
|
|
158
|
+
if (matchesKey(data, "ctrl+e") && !busy) { closed = true; done("edit"); return; }
|
|
159
|
+
scroller.handleKey(data);
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function openOverthinkDefaults(ctx: ExtensionCommandContext): Promise<void> {
|
|
166
|
+
const current = await readGlobalQuestions();
|
|
167
|
+
await openOverthinkQuestionEditor(ctx, {
|
|
168
|
+
title: "Bro · Overthink / Default questions",
|
|
169
|
+
questions: current,
|
|
170
|
+
reset: async () => [...FACTORY_QUESTIONS],
|
|
171
|
+
save: writeGlobalQuestions,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function registerOverthink(pi: ExtensionAPI, loadAutomaticChecks = readAutomaticChecks): {
|
|
176
|
+
open(ctx: ExtensionCommandContext): Promise<void>;
|
|
177
|
+
} {
|
|
178
|
+
let active: AbortController | undefined;
|
|
179
|
+
let generation = 0;
|
|
180
|
+
let evidence: OverthinkRunEvidence | undefined;
|
|
181
|
+
const invalidate = (_event: unknown, ctx: ExtensionContext) => {
|
|
182
|
+
generation++;
|
|
183
|
+
evidence = undefined;
|
|
184
|
+
active?.abort(); active = undefined;
|
|
185
|
+
noteWidget(ctx);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// The first session event locks its question defaults; subsequent branch/fork views
|
|
189
|
+
// inherit the nearest private custom entry rather than sharing a project setting.
|
|
190
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
191
|
+
invalidate(_event, ctx);
|
|
192
|
+
try { await ensureOverthinkSession(pi, ctx.sessionManager.getBranch()); }
|
|
193
|
+
catch (error) { if (ctx.mode === "tui") ctx.ui.notify("Overthink settings: " + errorMessage(error), "warning"); }
|
|
194
|
+
});
|
|
195
|
+
pi.on("session_tree", invalidate);
|
|
196
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
197
|
+
// A continuation or retry can begin a new agent loop before the overall
|
|
198
|
+
// run settles. Cancel in-flight checks and stale scheduling, but keep the
|
|
199
|
+
// tool evidence until agent_settled decides whether the full run qualifies.
|
|
200
|
+
generation++;
|
|
201
|
+
active?.abort(); active = undefined;
|
|
202
|
+
noteWidget(ctx);
|
|
203
|
+
});
|
|
204
|
+
pi.on("session_shutdown", async (_event, ctx) => { invalidate(_event, ctx); });
|
|
205
|
+
|
|
206
|
+
type Trigger = { generation: number; sessionId: string; position?: string };
|
|
207
|
+
const run = async (ctx: CheckContext, forced: boolean, trigger?: Trigger): Promise<CheckResult> => {
|
|
208
|
+
if (!ctx.model) throw new Error("No active Pi model; Overthink cannot choose a different one.");
|
|
209
|
+
const marker = trigger?.generation ?? generation;
|
|
210
|
+
const sessionId = trigger?.sessionId ?? ctx.sessionManager.getSessionId();
|
|
211
|
+
const position = trigger?.position ?? overthinkContextPosition(ctx.sessionManager.getBranch());
|
|
212
|
+
const stillCurrent = () => marker === generation &&
|
|
213
|
+
sessionId === ctx.sessionManager.getSessionId() &&
|
|
214
|
+
position === overthinkContextPosition(ctx.sessionManager.getBranch());
|
|
215
|
+
if (!stillCurrent()) throw new Error("Session moved before assessment could start.");
|
|
216
|
+
const state = await ensureOverthinkSession(pi, ctx.sessionManager.getBranch());
|
|
217
|
+
const questions = state.questions ?? [];
|
|
218
|
+
if (!questions.length) throw new Error("No Overthink questions. Edit session questions to enable checks.");
|
|
219
|
+
if (!stillCurrent()) throw new Error("Session changed; assessment cancelled.");
|
|
220
|
+
|
|
221
|
+
const projected = ctx.sessionManager.buildSessionProjection().messages;
|
|
222
|
+
if (!projected.length && forced) throw new Error("Nothing in the current conversation to assess.");
|
|
223
|
+
if (!projected.length) throw new Error("No conversation to assess.");
|
|
224
|
+
|
|
225
|
+
active?.abort();
|
|
226
|
+
const controller = new AbortController();
|
|
227
|
+
active = controller;
|
|
228
|
+
const model = ctx.model;
|
|
229
|
+
const name = model.provider + "/" + model.id;
|
|
230
|
+
const effort = pi.getThinkingLevel();
|
|
231
|
+
const ask = { role: "user" as const, content: [{ type: "text" as const, text: overthinkPrompt(questions) }], timestamp: Date.now() };
|
|
232
|
+
const timer = setTimeout(() => controller.abort(), NOTE_TIMEOUT_MS);
|
|
233
|
+
try {
|
|
234
|
+
// The exact projected message prefix is the basis of provider cache reuse.
|
|
235
|
+
// No second agent session, generated summary, extra tools or backend override.
|
|
236
|
+
const answer = await ctx.modelRegistry.streamSimple(
|
|
237
|
+
model,
|
|
238
|
+
normalizeContext({ messages: convertToLlm([...projected, ask]) }),
|
|
239
|
+
{
|
|
240
|
+
reasoning: model.reasoning && effort !== "off" ? effort : undefined,
|
|
241
|
+
maxTokens: 2200,
|
|
242
|
+
sessionId: sessionId ?? undefined,
|
|
243
|
+
signal: controller.signal,
|
|
244
|
+
},
|
|
245
|
+
).result();
|
|
246
|
+
if (controller.signal.aborted) throw new Error("Assessment cancelled or timed out.");
|
|
247
|
+
if (!stillCurrent() ||
|
|
248
|
+
ctx.model?.provider !== model.provider || ctx.model?.id !== model.id ||
|
|
249
|
+
active !== controller) throw new Error("Session or model changed; stale result discarded.");
|
|
250
|
+
if (answer.stopReason !== "stop") throw new Error("Model did not finish its assessment (" + answer.stopReason + ").");
|
|
251
|
+
const currentQuestions = resolveOverthinkSession(ctx.sessionManager.getBranch()).questions ?? [];
|
|
252
|
+
if (questionKey(currentQuestions) !== questionKey(questions)) throw new Error("Questions changed during check; stale result discarded.");
|
|
253
|
+
const text = parseOverthinkAnswer(answer.content.filter(part => part.type === "text").map(part => part.text).join(""));
|
|
254
|
+
const usage = readOverthinkUsage(answer.usage);
|
|
255
|
+
const note: OverthinkNote = {
|
|
256
|
+
text, checkedAt: new Date().toISOString(), model: name, questionKey: questionKey(questions),
|
|
257
|
+
...(position ? { sourcePosition: position } : {}),
|
|
258
|
+
...(usage ? { usage } : {}),
|
|
259
|
+
};
|
|
260
|
+
saveOverthinkNote(pi, note);
|
|
261
|
+
noteWidget(ctx, text);
|
|
262
|
+
return note;
|
|
263
|
+
} finally {
|
|
264
|
+
clearTimeout(timer);
|
|
265
|
+
if (active === controller) active = undefined;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
pi.on("agent_end", (event, ctx) => {
|
|
270
|
+
if (ctx.mode !== "tui") return;
|
|
271
|
+
evidence = accumulateOverthinkEvidence(evidence, event);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// agent_end is not guaranteed to mean Pi has finished continuation, retry or
|
|
275
|
+
// compaction. agent_settled is available in Pi >=1.3, our supported floor.
|
|
276
|
+
pi.on("agent_settled", (event, ctx) => {
|
|
277
|
+
const completed = evidence;
|
|
278
|
+
evidence = undefined;
|
|
279
|
+
if (ctx.mode !== "tui" || !ctx.model ||
|
|
280
|
+
!overthinkReady(completed, event.aborted, ctx.hasPendingMessages())) return;
|
|
281
|
+
// Capture BEFORE the asynchronous settings read. A new run, fork or
|
|
282
|
+
// compaction must not turn an older settlement into a request for new work.
|
|
283
|
+
const trigger = {
|
|
284
|
+
generation, sessionId: ctx.sessionManager.getSessionId(),
|
|
285
|
+
position: overthinkContextPosition(ctx.sessionManager.getBranch()),
|
|
286
|
+
};
|
|
287
|
+
void (async () => {
|
|
288
|
+
if (!(await loadAutomaticChecks())) return;
|
|
289
|
+
if (trigger.generation !== generation ||
|
|
290
|
+
trigger.sessionId !== ctx.sessionManager.getSessionId() ||
|
|
291
|
+
trigger.position !== overthinkContextPosition(ctx.sessionManager.getBranch()) ||
|
|
292
|
+
ctx.hasPendingMessages()) return;
|
|
293
|
+
await run(ctx, false, trigger);
|
|
294
|
+
})().catch(() => undefined); // Optional review must never fail the main task.
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const open = async (ctx: ExtensionCommandContext): Promise<void> => {
|
|
298
|
+
if (!hasBroCustomUi(ctx)) {
|
|
299
|
+
ctx.ui.notify("Use /bro overthink in Pi's interactive UI.", "warning");
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
await ensureOverthinkSession(pi, ctx.sessionManager.getBranch());
|
|
303
|
+
for (;;) {
|
|
304
|
+
const action = await openBroOverlay<ViewerAction>(ctx, createOverthinkViewer(
|
|
305
|
+
() => {
|
|
306
|
+
const state = resolveOverthinkSession(ctx.sessionManager.getBranch());
|
|
307
|
+
return { questions: state.questions ?? [], note: state.note,
|
|
308
|
+
position: overthinkContextPosition(ctx.sessionManager.getBranch()) };
|
|
309
|
+
},
|
|
310
|
+
() => run(ctx, true),
|
|
311
|
+
));
|
|
312
|
+
if (action !== "edit") return;
|
|
313
|
+
const state = resolveOverthinkSession(ctx.sessionManager.getBranch());
|
|
314
|
+
await openOverthinkQuestionEditor(ctx, {
|
|
315
|
+
title: "Bro · Overthink / Session questions",
|
|
316
|
+
questions: state.questions ?? [],
|
|
317
|
+
reset: readGlobalQuestions,
|
|
318
|
+
save: (questions) => saveSessionQuestions(pi, questions),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
return { open };
|
|
323
|
+
}
|