pi-mono-all 1.0.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.
Files changed (161) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENCE.md +7 -0
  3. package/node_modules/pi-common/package.json +22 -0
  4. package/node_modules/pi-common/src/auth-config.ts +290 -0
  5. package/node_modules/pi-common/src/auth.ts +63 -0
  6. package/node_modules/pi-common/src/cache.ts +60 -0
  7. package/node_modules/pi-common/src/errors.ts +47 -0
  8. package/node_modules/pi-common/src/http-client.ts +118 -0
  9. package/node_modules/pi-common/src/index.ts +7 -0
  10. package/node_modules/pi-common/src/rate-limiter.ts +32 -0
  11. package/node_modules/pi-common/src/tool-result.ts +27 -0
  12. package/node_modules/pi-mono-ask-user-question/CHANGELOG.md +185 -0
  13. package/node_modules/pi-mono-ask-user-question/README.md +226 -0
  14. package/node_modules/pi-mono-ask-user-question/index.ts +923 -0
  15. package/node_modules/pi-mono-ask-user-question/package.json +29 -0
  16. package/node_modules/pi-mono-auto-fix/CHANGELOG.md +59 -0
  17. package/node_modules/pi-mono-auto-fix/README.md +77 -0
  18. package/node_modules/pi-mono-auto-fix/index.ts +488 -0
  19. package/node_modules/pi-mono-auto-fix/package.json +23 -0
  20. package/node_modules/pi-mono-btw/CHANGELOG.md +180 -0
  21. package/node_modules/pi-mono-btw/README.md +24 -0
  22. package/node_modules/pi-mono-btw/index.ts +499 -0
  23. package/node_modules/pi-mono-btw/package.json +29 -0
  24. package/node_modules/pi-mono-clear/CHANGELOG.md +180 -0
  25. package/node_modules/pi-mono-clear/README.md +40 -0
  26. package/node_modules/pi-mono-clear/index.ts +45 -0
  27. package/node_modules/pi-mono-clear/package.json +29 -0
  28. package/node_modules/pi-mono-context/CHANGELOG.md +12 -0
  29. package/node_modules/pi-mono-context/README.md +74 -0
  30. package/node_modules/pi-mono-context/index.ts +641 -0
  31. package/node_modules/pi-mono-context/package.json +29 -0
  32. package/node_modules/pi-mono-context-guard/CHANGELOG.md +195 -0
  33. package/node_modules/pi-mono-context-guard/README.md +81 -0
  34. package/node_modules/pi-mono-context-guard/index.ts +212 -0
  35. package/node_modules/pi-mono-context-guard/package.json +23 -0
  36. package/node_modules/pi-mono-figma/CHANGELOG.md +59 -0
  37. package/node_modules/pi-mono-figma/README.md +236 -0
  38. package/node_modules/pi-mono-figma/__tests__/code-connect.test.ts +32 -0
  39. package/node_modules/pi-mono-figma/__tests__/figma-assets.test.ts +38 -0
  40. package/node_modules/pi-mono-figma/__tests__/figma-component-hints.test.ts +23 -0
  41. package/node_modules/pi-mono-figma/__tests__/figma-implementation-layout.test.ts +47 -0
  42. package/node_modules/pi-mono-figma/__tests__/figma-search.test.ts +51 -0
  43. package/node_modules/pi-mono-figma/__tests__/figma-summarizer.test.ts +65 -0
  44. package/node_modules/pi-mono-figma/__tests__/fixtures/complex-auto-layout.json +115 -0
  45. package/node_modules/pi-mono-figma/__tests__/fixtures/component-instance.json +50 -0
  46. package/node_modules/pi-mono-figma/__tests__/fixtures/hidden-and-vectors.json +28 -0
  47. package/node_modules/pi-mono-figma/__tests__/fixtures/variables-and-styles.json +40 -0
  48. package/node_modules/pi-mono-figma/docs/live-selection-bridge.md +16 -0
  49. package/node_modules/pi-mono-figma/index.ts +6 -0
  50. package/node_modules/pi-mono-figma/package.json +33 -0
  51. package/node_modules/pi-mono-figma/skills/figma/SKILL.md +143 -0
  52. package/node_modules/pi-mono-figma/src/code-connect.ts +110 -0
  53. package/node_modules/pi-mono-figma/src/figma-assets.ts +146 -0
  54. package/node_modules/pi-mono-figma/src/figma-cache.ts +6 -0
  55. package/node_modules/pi-mono-figma/src/figma-client.ts +471 -0
  56. package/node_modules/pi-mono-figma/src/figma-component-hints.ts +87 -0
  57. package/node_modules/pi-mono-figma/src/figma-implementation.ts +264 -0
  58. package/node_modules/pi-mono-figma/src/figma-schemas.ts +139 -0
  59. package/node_modules/pi-mono-figma/src/figma-search.ts +195 -0
  60. package/node_modules/pi-mono-figma/src/figma-summarizer.ts +673 -0
  61. package/node_modules/pi-mono-figma/src/figma-tokens.ts +57 -0
  62. package/node_modules/pi-mono-figma/src/figma-tools.ts +352 -0
  63. package/node_modules/pi-mono-linear/CHANGELOG.md +44 -0
  64. package/node_modules/pi-mono-linear/README.md +159 -0
  65. package/node_modules/pi-mono-linear/index.ts +6 -0
  66. package/node_modules/pi-mono-linear/package.json +30 -0
  67. package/node_modules/pi-mono-linear/skills/linear/SKILL.md +107 -0
  68. package/node_modules/pi-mono-linear/src/linear-client.ts +339 -0
  69. package/node_modules/pi-mono-linear/src/linear-queries.ts +101 -0
  70. package/node_modules/pi-mono-linear/src/linear-schemas.ts +90 -0
  71. package/node_modules/pi-mono-linear/src/linear-tools.ts +362 -0
  72. package/node_modules/pi-mono-loop/CHANGELOG.md +163 -0
  73. package/node_modules/pi-mono-loop/README.md +54 -0
  74. package/node_modules/pi-mono-loop/index.ts +291 -0
  75. package/node_modules/pi-mono-loop/package.json +26 -0
  76. package/node_modules/pi-mono-multi-edit/CHANGELOG.md +232 -0
  77. package/node_modules/pi-mono-multi-edit/README.md +244 -0
  78. package/node_modules/pi-mono-multi-edit/__tests__/classic.test.ts +277 -0
  79. package/node_modules/pi-mono-multi-edit/__tests__/diff.test.ts +77 -0
  80. package/node_modules/pi-mono-multi-edit/__tests__/patch.test.ts +287 -0
  81. package/node_modules/pi-mono-multi-edit/benchmark-edits.ts +966 -0
  82. package/node_modules/pi-mono-multi-edit/classic.ts +435 -0
  83. package/node_modules/pi-mono-multi-edit/diff.ts +143 -0
  84. package/node_modules/pi-mono-multi-edit/index.ts +266 -0
  85. package/node_modules/pi-mono-multi-edit/package.json +37 -0
  86. package/node_modules/pi-mono-multi-edit/patch.ts +463 -0
  87. package/node_modules/pi-mono-multi-edit/types.ts +53 -0
  88. package/node_modules/pi-mono-multi-edit/workspace.ts +85 -0
  89. package/node_modules/pi-mono-review/CHANGELOG.md +190 -0
  90. package/node_modules/pi-mono-review/README.md +30 -0
  91. package/node_modules/pi-mono-review/common.ts +930 -0
  92. package/node_modules/pi-mono-review/index.ts +8 -0
  93. package/node_modules/pi-mono-review/package.json +29 -0
  94. package/node_modules/pi-mono-review/review-tui.ts +194 -0
  95. package/node_modules/pi-mono-review/review.ts +119 -0
  96. package/node_modules/pi-mono-review/reviewer.ts +339 -0
  97. package/node_modules/pi-mono-sentinel/CHANGELOG.md +158 -0
  98. package/node_modules/pi-mono-sentinel/README.md +87 -0
  99. package/node_modules/pi-mono-sentinel/__tests__/output-scanner.test.ts +109 -0
  100. package/node_modules/pi-mono-sentinel/__tests__/permissions.test.ts +202 -0
  101. package/node_modules/pi-mono-sentinel/__tests__/whitelist.test.ts +59 -0
  102. package/node_modules/pi-mono-sentinel/guards/execution-tracker.ts +281 -0
  103. package/node_modules/pi-mono-sentinel/guards/output-scanner.ts +232 -0
  104. package/node_modules/pi-mono-sentinel/guards/permission-gate.ts +170 -0
  105. package/node_modules/pi-mono-sentinel/index.ts +43 -0
  106. package/node_modules/pi-mono-sentinel/package.json +26 -0
  107. package/node_modules/pi-mono-sentinel/patterns/permissions.ts +175 -0
  108. package/node_modules/pi-mono-sentinel/patterns/read-targets.ts +104 -0
  109. package/node_modules/pi-mono-sentinel/patterns/secrets.ts +143 -0
  110. package/node_modules/pi-mono-sentinel/session.ts +95 -0
  111. package/node_modules/pi-mono-sentinel/specs/2026/04/sentinel/001-permission-gate.md +145 -0
  112. package/node_modules/pi-mono-sentinel/types.ts +39 -0
  113. package/node_modules/pi-mono-sentinel/whitelist.ts +86 -0
  114. package/node_modules/pi-mono-simplify/CHANGELOG.md +163 -0
  115. package/node_modules/pi-mono-simplify/README.md +56 -0
  116. package/node_modules/pi-mono-simplify/index.ts +78 -0
  117. package/node_modules/pi-mono-simplify/package.json +29 -0
  118. package/node_modules/pi-mono-status-line/CHANGELOG.md +180 -0
  119. package/node_modules/pi-mono-status-line/README.md +96 -0
  120. package/node_modules/pi-mono-status-line/basic.ts +89 -0
  121. package/node_modules/pi-mono-status-line/expert.ts +689 -0
  122. package/node_modules/pi-mono-status-line/index.ts +54 -0
  123. package/node_modules/pi-mono-status-line/package.json +29 -0
  124. package/node_modules/pi-mono-team-mode/CHANGELOG.md +278 -0
  125. package/node_modules/pi-mono-team-mode/README.md +246 -0
  126. package/node_modules/pi-mono-team-mode/__tests__/agent-manager-transient.test.ts +75 -0
  127. package/node_modules/pi-mono-team-mode/__tests__/delegation-manager.test.ts +118 -0
  128. package/node_modules/pi-mono-team-mode/__tests__/formatters.test.ts +104 -0
  129. package/node_modules/pi-mono-team-mode/__tests__/model-config.test.ts +272 -0
  130. package/node_modules/pi-mono-team-mode/__tests__/notification-box.test.ts +34 -0
  131. package/node_modules/pi-mono-team-mode/__tests__/parallel-utils.test.ts +32 -0
  132. package/node_modules/pi-mono-team-mode/__tests__/pi-stream-parser.test.ts +64 -0
  133. package/node_modules/pi-mono-team-mode/__tests__/prompts.test.ts +106 -0
  134. package/node_modules/pi-mono-team-mode/__tests__/store.test.ts +164 -0
  135. package/node_modules/pi-mono-team-mode/__tests__/tasks.test.ts +267 -0
  136. package/node_modules/pi-mono-team-mode/__tests__/teammate-specs.test.ts +114 -0
  137. package/node_modules/pi-mono-team-mode/__tests__/widget.test.ts +41 -0
  138. package/node_modules/pi-mono-team-mode/__tests__/worktree.test.ts +78 -0
  139. package/node_modules/pi-mono-team-mode/core/chain-utils.ts +90 -0
  140. package/node_modules/pi-mono-team-mode/core/fs-utils.ts +44 -0
  141. package/node_modules/pi-mono-team-mode/core/model-config.ts +432 -0
  142. package/node_modules/pi-mono-team-mode/core/parallel-utils.ts +48 -0
  143. package/node_modules/pi-mono-team-mode/core/prompts.ts +158 -0
  144. package/node_modules/pi-mono-team-mode/core/store.ts +156 -0
  145. package/node_modules/pi-mono-team-mode/core/tasks.ts +99 -0
  146. package/node_modules/pi-mono-team-mode/core/teammate-specs.ts +124 -0
  147. package/node_modules/pi-mono-team-mode/core/types.ts +160 -0
  148. package/node_modules/pi-mono-team-mode/index.ts +825 -0
  149. package/node_modules/pi-mono-team-mode/managers/agent-manager.ts +654 -0
  150. package/node_modules/pi-mono-team-mode/managers/delegation-manager.ts +211 -0
  151. package/node_modules/pi-mono-team-mode/managers/task-manager.ts +238 -0
  152. package/node_modules/pi-mono-team-mode/managers/team-manager.ts +59 -0
  153. package/node_modules/pi-mono-team-mode/package.json +33 -0
  154. package/node_modules/pi-mono-team-mode/runtime/pi-stream-parser.ts +194 -0
  155. package/node_modules/pi-mono-team-mode/runtime/subprocess.ts +183 -0
  156. package/node_modules/pi-mono-team-mode/runtime/transient-session.ts +196 -0
  157. package/node_modules/pi-mono-team-mode/runtime/worktree.ts +90 -0
  158. package/node_modules/pi-mono-team-mode/ui/formatters.ts +149 -0
  159. package/node_modules/pi-mono-team-mode/ui/notification-box.ts +55 -0
  160. package/node_modules/pi-mono-team-mode/ui/widget.ts +94 -0
  161. package/package.json +76 -0
@@ -0,0 +1,180 @@
1
+ # pi-mono-btw
2
+
3
+ ## 1.7.2
4
+
5
+ ### Patch Changes
6
+
7
+ ### Fixed: ask-user-question
8
+
9
+ - Remove unused `StringEnum` import from `@mariozechner/pi-ai`.
10
+
11
+
12
+ ## 1.7.1
13
+
14
+ ### Patch Changes
15
+
16
+ ### Fixed: team-mode
17
+
18
+ - Widget no longer mislabels blocked or approval-pending teams as "running smoothly" — blockers and pending approvals are now detected via team summaries.
19
+ - Preserve in-flight work on re-emitted `session_start` events instead of tearing the runtime down and SIGTERM-ing live teammates.
20
+ - Auto-relaunch leaders for `running` teams after a session reset; surface failures as both a team signal and a UI notification.
21
+ - `createTeam` now defaults `repoRoots` to `[process.cwd()]` when the caller passes an empty array.
22
+ - Archive `process.json` into `history/` before a new task reuses the same role slot, so the prior task's final state is no longer silently clobbered.
23
+
24
+ ### Enhanced: team-mode
25
+
26
+ - Durable intent queue for subprocess handoff: `team_spawn_teammate` calls made from a teammate subprocess are written to disk and executed by the main session's `LeaderRuntime` instead of spawning orphaned grand-children.
27
+ - New tool `team_task_create_batch` lets the leader emit the full initial task DAG in one call, removing per-task LLM round-trips during bootstrap.
28
+ - `team_create` / `launchLeader` accept an `awaitBootstrap` option so the user sees the task graph before the tool returns; leader launch retries up to 3 times on transient failures.
29
+ - Persist per-turn debug artifacts (prompt, invocation, stderr, raw event stream) for both leader and teammate subprocesses, exposed via `TeammateSummary.debugArtifacts`.
30
+ - Track `exitCode`, `exitSignal`, `terminationReason`, `stderrTail`, `toolExecutions`, `model` and `modelProvider` on every `TeammateProcess` record.
31
+ - Provider detection now consults pi's `settings.json` and `auth.json` in addition to env vars; default model IDs aligned with the provider/model scheme.
32
+ - `collectPiOutput` supports `AbortSignal` cancellation.
33
+
34
+ ### Tests
35
+
36
+ - New `intent-queue` and `model-config` suites; expanded coverage across `leader-runtime`, `team-manager`, `team-query-tool` and `formatters`.
37
+
38
+
39
+ ## 1.7.0
40
+
41
+ ### Minor Changes
42
+
43
+ ### Enhanced: status-line
44
+
45
+ - Improved progress rendering and colors in expert mode
46
+
47
+ ### Enhanced: team-mode
48
+
49
+ - **LLM-driven leader** — replaced the hardcoded `research → synthesis → implementation → verification` state machine with a pi subprocess coordinator that authors the task graph via tool calls
50
+ - **New tool `team_task_create`** so the leader can author tasks at runtime
51
+ - **New tool `team_handoff`** for explicit teammate → teammate context handoffs (replaces regex-scraping of `Handoffs:` output sections)
52
+ - **File-based teammate specs** — drop `.claude/teammates/<role>.md` frontmatter files (`name`, `description`, `needsWorktree`, `hasMemory`, `modelTier`) to extend or override the seven built-in roles
53
+ - **Event-driven leader wakes** — mailbox messages addressed to the leader (or broadcast) trigger a debounced (~200ms) cycle instead of waiting for the 20s polling tick
54
+ - **Templates accept any string** — `fullstack` / `research` / `refactor` remain as built-ins, but unknown template keys are accepted and no-op gracefully
55
+ - **Provider config per team** — per-team model overrides via `/team models`
56
+ - Reduced leader overhead and parent-session token churn
57
+ - `spawnTeammate` now always appends the full runtime-built context (signals, mailbox, dependencies, team memory) so teammates get the richer snapshot even when the caller's `context` argument is brief
58
+
59
+ ### Breaking changes: team-mode
60
+
61
+ - Removed `LeaderPhase` enum and `currentPhase` field from `TeamRecord` / `TeamSummary`
62
+ - Removed `parseExplicitHandoffs` export and the legacy `Handoffs:` output parser — peer handoffs must go through the `team_handoff` tool
63
+ - Removed the deterministic auto-spawn loop (`ensureBootstrapTasks`) — all task authoring and teammate spawning is now the LLM leader's responsibility
64
+ - Removed `StringEnum` gate on `team_create`'s `template` parameter (now plain string)
65
+
66
+ ### Fixed: review
67
+
68
+ - Annotate diff lines so the model picks correct line numbers
69
+ - Fix slice chunk around lines for comments in the reviewer TUI
70
+
71
+ ### Documentation
72
+
73
+ - Updated root README and sentinel extension README
74
+ - Documented the new file-based teammate spec format and event-driven leader wake in the team-mode README
75
+
76
+ ## 1.6.0
77
+
78
+ ### Minor Changes
79
+
80
+ ### New Extension: sentinel
81
+
82
+ Replaced the `grep` extension with a new security-focused `sentinel` extension for monitoring and guarding sensitive operations.
83
+
84
+ ### Enhanced: team-mode
85
+
86
+ - Added comprehensive test suite with integration tests
87
+ - New mock helpers for subprocess testing
88
+ - Improved signal manager with better error handling
89
+ - Leader runtime refactoring for stability
90
+ - Team query tool with dedicated tests
91
+
92
+ ### Enhanced: status-line
93
+
94
+ - Added basic and expert mode displays
95
+ - Improved index.ts with better state management
96
+
97
+ ### Enhanced: clear
98
+
99
+ - Updated keyboard shortcut to `Ctrl+Shift+L`
100
+ - Better busy-state handling for shortcuts
101
+ - Added warning/cancel handling and error notifications
102
+
103
+ ### Enhanced: context-guard
104
+
105
+ - Improved read deduplication across sessions
106
+ - Added `context-guard:file-modified` event for cache eviction
107
+
108
+ ### Documentation
109
+
110
+ - Added dedicated README for `clear` extension
111
+ - Added dedicated README for `context-guard` extension
112
+ - Updated main README with improved extension descriptions
113
+
114
+ ## 1.5.0
115
+
116
+ ### Minor Changes
117
+
118
+ - ### `multi-edit` — diverge from upstream fork
119
+
120
+ The extension was originally derived from [mitsuhiko/agent-stuff](https://github.com/mitsuhiko/agent-stuff)'s `pi-extensions/multi-edit.ts`. This release rewrites the largest unmodified subsystems so the implementation is structurally distinct from upstream while keeping the public contract intact.
121
+
122
+ - **Modularized layout** — the 953-line `index.ts` is split into purpose-scoped modules: `types.ts`, `workspace.ts`, `classic.ts`, `patch.ts`, `diff.ts`, and a slim `index.ts` (~180 lines of registration + dispatch wiring).
123
+ - **New patch engine** — `patch.ts` is now a recursive-descent parser over a `LineCursor` class with `indexOf`-based hunk anchoring. Hunks are stored as `{ oldBlock, newBlock }` raw strings (previously `{ oldLines[], newLines[] }` arrays), letting the applier splice content directly instead of reconstructing line arrays per apply.
124
+ - **Two-pass diff renderer** — `diff.ts` now walks `diffLines` parts into a typed `Entry[]` stream and makes all gutter / context-collapse decisions in a second pass, replacing the prior single-loop state-flag design.
125
+ - **Polished classic edits** — extracted `groupEditsByPath`, `sortGroupByPosition`, `applyGroupToContent`, and `rollbackSnapshots` helpers; formalized the quote-fallback as an ordered `MATCH_PASSES` array so new normalizers (dashes, NBSP, etc.) can be added by appending one entry.
126
+ - **First contract test suite** — 34 tests under `__tests__/` cover classic edits (positional reordering, redundant-pair skip, quote fallback, atomic rollback, read-only preflight), patch operations (Add/Delete/Update round-trips, move-rejection, multi-op batches), and diff rendering (line-number gutter, context collapse, add/remove-only cases). Runs via `npm test` (`tsx --test`).
127
+ - **Dropped Codex apply_patch edge cases** (documented in `README.md` → "Codex apply_patch compatibility"): `*** End of File` sentinel hunks, 4-pass fuzzy `seekSequence` matching, implicit first hunk without `@@`, whitespace-tolerant anchoring. Common paths (Add/Delete/Update-single-chunk, Update with multiple hunks, Add+Update+Delete batches) are fully tested and preserved.
128
+ - **README attribution** — new "Origins" section crediting `mitsuhiko/agent-stuff` as the original source.
129
+
130
+ ## 1.4.0
131
+
132
+ ### Minor Changes
133
+
134
+ - Add teammate progress heartbeats and widget refresh improvements to team mode.
135
+
136
+ ## 1.3.0
137
+
138
+ ## 1.2.0
139
+
140
+ ### Minor Changes
141
+
142
+ - ### `multi-edit` — robustness improvements
143
+
144
+ - **No-op write guard**: skip file write and `context-guard:file-modified` event when new content is identical to what was last read — prevents unnecessary watcher churn
145
+ - **Early write-access check**: virtual workspace `checkWriteAccess` now validates real-filesystem permissions during the preflight pass so read-only files fail fast before any real file is touched
146
+ - **Curly-quote normalization**: new `findActualString` helper falls back to normalized quote matching (`"` / `'` ↔ `"` / `'`) when exact `oldText` search fails — the most common class of preflight mismatch
147
+ - **Atomic batch rollback**: `applyClassicEdits` gains a `rollbackOnError` option that restores all successfully written files when a later edit in the same batch fails
148
+
149
+ ### `ask-user-question` — UX fixes
150
+
151
+ - **Reliable text capture on submit**: answer is read directly from the editor before it clears itself, fixing a race where the stored value was always empty
152
+ - **Unified advance logic**: `advanceTab()` and `saveOtherModeText()` helpers replace scattered single-question fast-paths — behaviour is now consistent regardless of form length
153
+ - **Auto-advance on Enter / Tab**: pressing Enter or Tab in any question (text, radio with "Other", checkbox with "Other") advances to the next tab without requiring a separate click
154
+
155
+ ### `team-mode` — stability fixes
156
+
157
+ - **Infinite retry loop eliminated**: subprocess guard (`PI_TEAM_SUBPROCESS=1`) prevents spawned pi subprocesses from launching a ghost `LeaderRuntime` that immediately marks in-progress tasks as stalled
158
+ - **Stall detection grace period**: tasks updated within the last 2 × `LEADER_POLL_MS` (10 s) are skipped by `detectStalledTasks` — eliminates false positives on the spawning cycle
159
+ - **Circuit breaker**: tasks that stall more than `MAX_TASK_RETRIES` (3) times are permanently cancelled with a clear error signal instead of being silently re-queued
160
+ - **Concurrent cycle guard**: `runLeaderCycle` returns early if a cycle is already in-flight for the same team, preventing overlapping read-modify-write from the poll interval and completion handlers
161
+ - **Widget cleanup**: cancelled and completed teams are no longer shown in the team widget — only `initializing | running | paused | failed` states are displayed
162
+ - **Shorter auto-generated names**: `objectiveToName` now splits on non-alphanumeric characters (handles path separators), filters stopwords and extreme-length tokens, and hard-caps at 32 characters
163
+
164
+ ## 1.1.1
165
+
166
+ ### Patch Changes
167
+
168
+ - chore: update all packages for consistency and include team-mode fixes
169
+
170
+ ## 1.1.0
171
+
172
+ ## 1.0.0
173
+
174
+ ### Major Changes
175
+
176
+ - 199c367: First version of the extensions to upload to GitHub
177
+
178
+ ### Patch Changes
179
+
180
+ - Bump all packages to 0.1.1
@@ -0,0 +1,24 @@
1
+ # btw extension
2
+
3
+ This extension adds Claude Code-style ` /btw ` behavior to pi.
4
+
5
+ ## Behavior
6
+
7
+ - intercepts `/btw <question>` through the input pipeline instead of a normal extension command
8
+ - starts a separate model request immediately
9
+ - does not queue the question into the main agent loop
10
+ - does not interrupt the current task
11
+ - renders answers in a passive widget below the editor while pi keeps working
12
+ - stores hidden history as custom session entries (`btw-history`)
13
+
14
+ ## Why it is implemented this way
15
+
16
+ In pi, normal extension commands are checked before input expansion and are not the same as prompt templates or skills. To make `/btw` work while pi is already busy, this extension handles raw input that starts with `/btw` and launches its own background completion.
17
+
18
+ ## Extra shortcut
19
+
20
+ - `Ctrl+Shift+B` asks the current editor text as a side question
21
+
22
+ ## Files
23
+
24
+ - `index.ts` — extension entry point
@@ -0,0 +1,499 @@
1
+ import { complete, type UserMessage } from "@mariozechner/pi-ai";
2
+ import type { ExtensionAPI, ExtensionContext, Theme } from "@mariozechner/pi-coding-agent";
3
+ import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
4
+
5
+ const BTW_ENTRY_TYPE = "btw-history";
6
+ const BTW_WIDGET_ID = "btw-widget";
7
+ const COMPLETED_ITEM_TTL_MS = 90_000;
8
+ const MAX_TRANSCRIPT_CHARS = 14_000;
9
+ const MAX_TOOL_RESULT_CHARS = 800;
10
+ const MAX_RENDER_ITEMS = 2;
11
+ const MAX_RENDERED_ANSWER_LINES = 6;
12
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
13
+
14
+ const SIDE_QUESTION_SYSTEM_PROMPT = [
15
+ "You are answering a quick side question while the user's main pi session continues working.",
16
+ "Use the provided session transcript only as background context.",
17
+ "Answer directly and concisely.",
18
+ "Prefer compact bullets or short paragraphs.",
19
+ "If the transcript is insufficient, say that briefly instead of guessing.",
20
+ ].join("\n");
21
+
22
+ type TextBlock = { type?: string; text?: string };
23
+ type ToolCallBlock = { type?: string; name?: string; arguments?: Record<string, unknown> };
24
+
25
+ type SessionEntryLike = {
26
+ type: string;
27
+ message?: {
28
+ role?: string;
29
+ content?: unknown;
30
+ toolName?: string;
31
+ };
32
+ };
33
+
34
+ type BtwRecord = {
35
+ question: string;
36
+ answer?: string;
37
+ error?: string;
38
+ askedAt: string;
39
+ answeredAt: string;
40
+ model: string;
41
+ sessionFile?: string;
42
+ };
43
+
44
+ type BtwItem = {
45
+ id: string;
46
+ question: string;
47
+ state: "loading" | "answer" | "error";
48
+ askedAt: string;
49
+ answeredAt?: string;
50
+ answer?: string;
51
+ error?: string;
52
+ model: string;
53
+ expiresAt?: number;
54
+ };
55
+
56
+ type BtwRuntime = {
57
+ sessionKey: string;
58
+ items: BtwItem[];
59
+ spinnerFrame: number;
60
+ requestRender?: () => void;
61
+ spinnerTimer?: ReturnType<typeof setInterval>;
62
+ expiryTimer?: ReturnType<typeof setTimeout>;
63
+ };
64
+
65
+ const runtimes = new Map<string, BtwRuntime>();
66
+ const pendingPersistence = new Map<string, BtwRecord[]>();
67
+ let nextItemId = 1;
68
+
69
+ function getSessionKey(ctx: ExtensionContext): string {
70
+ return ctx.sessionManager.getSessionFile() ?? `memory:${ctx.sessionManager.getSessionId()}`;
71
+ }
72
+
73
+ function getRuntime(ctx: ExtensionContext): BtwRuntime {
74
+ const sessionKey = getSessionKey(ctx);
75
+ let runtime = runtimes.get(sessionKey);
76
+ if (!runtime) {
77
+ runtime = {
78
+ sessionKey,
79
+ items: [],
80
+ spinnerFrame: 0,
81
+ };
82
+ runtimes.set(sessionKey, runtime);
83
+ }
84
+ return runtime;
85
+ }
86
+
87
+ function extractTextParts(content: unknown): string[] {
88
+ if (typeof content === "string") {
89
+ return [content];
90
+ }
91
+
92
+ if (!Array.isArray(content)) {
93
+ return [];
94
+ }
95
+
96
+ const textParts: string[] = [];
97
+ for (const part of content) {
98
+ if (!part || typeof part !== "object") continue;
99
+ const block = part as TextBlock;
100
+ if (block.type === "text" && typeof block.text === "string") {
101
+ textParts.push(block.text);
102
+ }
103
+ }
104
+ return textParts;
105
+ }
106
+
107
+ function extractToolCalls(content: unknown): string[] {
108
+ if (!Array.isArray(content)) {
109
+ return [];
110
+ }
111
+
112
+ const toolCalls: string[] = [];
113
+ for (const part of content) {
114
+ if (!part || typeof part !== "object") continue;
115
+ const block = part as ToolCallBlock;
116
+ if (block.type !== "toolCall" || typeof block.name !== "string") continue;
117
+ toolCalls.push(`Assistant called tool ${block.name} with ${JSON.stringify(block.arguments ?? {})}`);
118
+ }
119
+ return toolCalls;
120
+ }
121
+
122
+ function clip(text: string, maxChars: number): string {
123
+ if (text.length <= maxChars) return text;
124
+ return `${text.slice(0, maxChars)}\n...[truncated]`;
125
+ }
126
+
127
+ function buildTranscriptText(entries: SessionEntryLike[]): string {
128
+ const relevantEntries = entries.filter((entry) => entry.type === "message").slice(-20);
129
+ const sections: string[] = [];
130
+
131
+ for (const entry of relevantEntries) {
132
+ const message = entry.message;
133
+ if (!message?.role) continue;
134
+
135
+ const role = message.role;
136
+ const text = extractTextParts(message.content).join("\n").trim();
137
+ const lines: string[] = [];
138
+
139
+ switch (role) {
140
+ case "user":
141
+ if (text) lines.push(`User: ${text}`);
142
+ break;
143
+ case "assistant":
144
+ if (text) lines.push(`Assistant: ${text}`);
145
+ lines.push(...extractToolCalls(message.content));
146
+ break;
147
+ case "toolResult":
148
+ if (text) {
149
+ const toolName = message.toolName ?? "tool";
150
+ lines.push(`Tool result from ${toolName}: ${clip(text, MAX_TOOL_RESULT_CHARS)}`);
151
+ }
152
+ break;
153
+ case "bashExecution":
154
+ if (text) lines.push(`User bash output: ${clip(text, MAX_TOOL_RESULT_CHARS)}`);
155
+ break;
156
+ case "custom":
157
+ if (text) lines.push(`Extension message: ${text}`);
158
+ break;
159
+ case "branchSummary":
160
+ case "compactionSummary":
161
+ if (text) lines.push(`Summary: ${text}`);
162
+ break;
163
+ }
164
+
165
+ if (lines.length > 0) {
166
+ sections.push(lines.join("\n"));
167
+ }
168
+ }
169
+
170
+ const transcript = sections.join("\n\n");
171
+ if (transcript.length <= MAX_TRANSCRIPT_CHARS) return transcript;
172
+ return `...[earlier session context omitted]\n\n${transcript.slice(-MAX_TRANSCRIPT_CHARS)}`;
173
+ }
174
+
175
+ function buildSideQuestionPrompt(question: string, transcript: string): string {
176
+ return [
177
+ "Current pi session transcript:",
178
+ "<session>",
179
+ transcript || "(No useful session transcript found.)",
180
+ "</session>",
181
+ "",
182
+ "Side question:",
183
+ "<question>",
184
+ question,
185
+ "</question>",
186
+ ].join("\n");
187
+ }
188
+
189
+ function getModelLabel(ctx: ExtensionContext): string {
190
+ if (!ctx.model) return "unknown-model";
191
+ return `${ctx.model.provider}/${ctx.model.id}`;
192
+ }
193
+
194
+ async function askSideQuestion(question: string, ctx: ExtensionContext): Promise<string> {
195
+ if (!ctx.model) {
196
+ throw new Error("No model selected.");
197
+ }
198
+
199
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
200
+ if (!auth.ok) {
201
+ throw new Error(auth.error);
202
+ }
203
+ if (!auth.apiKey) {
204
+ throw new Error(`No API key available for ${getModelLabel(ctx)}.`);
205
+ }
206
+
207
+ const transcript = buildTranscriptText(ctx.sessionManager.getBranch() as SessionEntryLike[]);
208
+ const userMessage: UserMessage = {
209
+ role: "user",
210
+ content: [{ type: "text", text: buildSideQuestionPrompt(question, transcript) }],
211
+ timestamp: Date.now(),
212
+ };
213
+
214
+ const response = await complete(
215
+ ctx.model,
216
+ {
217
+ systemPrompt: SIDE_QUESTION_SYSTEM_PROMPT,
218
+ messages: [userMessage],
219
+ },
220
+ {
221
+ apiKey: auth.apiKey,
222
+ headers: auth.headers,
223
+ },
224
+ );
225
+
226
+ if (response.stopReason === "aborted") {
227
+ throw new Error("Cancelled.");
228
+ }
229
+
230
+ const answer = response.content
231
+ .filter((item): item is { type: "text"; text: string } => item.type === "text")
232
+ .map((item) => item.text)
233
+ .join("\n")
234
+ .trim();
235
+
236
+ return answer || "No response received.";
237
+ }
238
+
239
+ function ensureWidget(ctx: ExtensionContext, runtime: BtwRuntime) {
240
+ if (!ctx.hasUI) return;
241
+
242
+ ctx.ui.setWidget(
243
+ BTW_WIDGET_ID,
244
+ (tui, theme) => {
245
+ runtime.requestRender = () => tui.requestRender();
246
+ return new BtwWidget(theme, runtime);
247
+ },
248
+ { placement: "belowEditor" },
249
+ );
250
+ }
251
+
252
+ function persistOrQueue(pi: ExtensionAPI, ctx: ExtensionContext, record: BtwRecord) {
253
+ if (ctx.isIdle()) {
254
+ pi.appendEntry(BTW_ENTRY_TYPE, record);
255
+ return;
256
+ }
257
+
258
+ const key = getSessionKey(ctx);
259
+ const queue = pendingPersistence.get(key) ?? [];
260
+ queue.push(record);
261
+ pendingPersistence.set(key, queue);
262
+ }
263
+
264
+ function flushPendingForCurrentSession(pi: ExtensionAPI, ctx: ExtensionContext) {
265
+ if (!ctx.isIdle()) return;
266
+
267
+ const key = getSessionKey(ctx);
268
+ const queue = pendingPersistence.get(key);
269
+ if (!queue || queue.length === 0) return;
270
+
271
+ for (const record of queue) {
272
+ pi.appendEntry(BTW_ENTRY_TYPE, record);
273
+ }
274
+ pendingPersistence.delete(key);
275
+ }
276
+
277
+ function cleanupExpiredItems(runtime: BtwRuntime) {
278
+ const now = Date.now();
279
+ runtime.items = runtime.items.filter((item) => item.state === "loading" || !item.expiresAt || item.expiresAt > now);
280
+ }
281
+
282
+ function syncRuntimeTimers(runtime: BtwRuntime) {
283
+ cleanupExpiredItems(runtime);
284
+
285
+ const hasLoading = runtime.items.some((item) => item.state === "loading");
286
+ if (hasLoading && !runtime.spinnerTimer) {
287
+ runtime.spinnerTimer = setInterval(() => {
288
+ runtime.spinnerFrame = (runtime.spinnerFrame + 1) % SPINNER_FRAMES.length;
289
+ runtime.requestRender?.();
290
+ }, 120);
291
+ }
292
+ if (!hasLoading && runtime.spinnerTimer) {
293
+ clearInterval(runtime.spinnerTimer);
294
+ runtime.spinnerTimer = undefined;
295
+ }
296
+
297
+ if (runtime.expiryTimer) {
298
+ clearTimeout(runtime.expiryTimer);
299
+ runtime.expiryTimer = undefined;
300
+ }
301
+
302
+ const now = Date.now();
303
+ const nextExpiry = runtime.items
304
+ .filter((item) => item.expiresAt && item.expiresAt > now)
305
+ .map((item) => item.expiresAt as number)
306
+ .sort((a, b) => a - b)[0];
307
+
308
+ if (nextExpiry) {
309
+ runtime.expiryTimer = setTimeout(() => {
310
+ cleanupExpiredItems(runtime);
311
+ syncRuntimeTimers(runtime);
312
+ runtime.requestRender?.();
313
+ }, Math.max(0, nextExpiry - now));
314
+ }
315
+ }
316
+
317
+ async function startBtw(question: string, pi: ExtensionAPI, ctx: ExtensionContext) {
318
+ if (!ctx.model) {
319
+ ctx.ui.notify("No model selected", "error");
320
+ return;
321
+ }
322
+
323
+ const runtime = getRuntime(ctx);
324
+ ensureWidget(ctx, runtime);
325
+
326
+ const item: BtwItem = {
327
+ id: `btw-${nextItemId++}`,
328
+ question,
329
+ state: "loading",
330
+ askedAt: new Date().toISOString(),
331
+ model: getModelLabel(ctx),
332
+ };
333
+
334
+ runtime.items.unshift(item);
335
+ runtime.items = runtime.items.slice(0, 6);
336
+ syncRuntimeTimers(runtime);
337
+ runtime.requestRender?.();
338
+
339
+ try {
340
+ const answer = await askSideQuestion(question, ctx);
341
+ item.state = "answer";
342
+ item.answer = answer;
343
+ item.answeredAt = new Date().toISOString();
344
+ item.expiresAt = Date.now() + COMPLETED_ITEM_TTL_MS;
345
+ persistOrQueue(pi, ctx, {
346
+ question,
347
+ answer,
348
+ askedAt: item.askedAt,
349
+ answeredAt: item.answeredAt,
350
+ model: item.model,
351
+ sessionFile: ctx.sessionManager.getSessionFile(),
352
+ });
353
+ } catch (error) {
354
+ item.state = "error";
355
+ item.error = error instanceof Error ? error.message : String(error);
356
+ item.answeredAt = new Date().toISOString();
357
+ item.expiresAt = Date.now() + COMPLETED_ITEM_TTL_MS;
358
+ persistOrQueue(pi, ctx, {
359
+ question,
360
+ error: item.error,
361
+ askedAt: item.askedAt,
362
+ answeredAt: item.answeredAt,
363
+ model: item.model,
364
+ sessionFile: ctx.sessionManager.getSessionFile(),
365
+ });
366
+ }
367
+
368
+ syncRuntimeTimers(runtime);
369
+ runtime.requestRender?.();
370
+ }
371
+
372
+ function normalizeShortcutQuestion(text: string): string {
373
+ const trimmed = text.trim();
374
+ if (!trimmed) return "";
375
+ return trimmed.replace(/^\/btw\b/i, "").trim();
376
+ }
377
+
378
+ function extractBtwQuestion(text: string): string | null {
379
+ const match = text.match(/^\/btw\b([\s\S]*)$/i);
380
+ if (!match) return null;
381
+ return match[1]?.trim() ?? "";
382
+ }
383
+
384
+ class BtwWidget {
385
+ constructor(
386
+ private readonly theme: Theme,
387
+ private readonly runtime: BtwRuntime,
388
+ ) {}
389
+
390
+ render(width: number): string[] {
391
+ cleanupExpiredItems(this.runtime);
392
+ if (this.runtime.items.length === 0) {
393
+ return [];
394
+ }
395
+
396
+ const innerWidth = Math.max(24, width);
397
+ const lines: string[] = [];
398
+ const activeCount = this.runtime.items.filter((item) => item.state === "loading").length;
399
+ const recentCount = this.runtime.items.filter((item) => item.state !== "loading").length;
400
+ const summaryParts = [activeCount > 0 ? `${activeCount} running` : undefined, recentCount > 0 ? `${recentCount} recent` : undefined]
401
+ .filter(Boolean)
402
+ .join(" · ");
403
+
404
+ lines.push(this.theme.fg("accent", "BTW") + (summaryParts ? this.theme.fg("dim", ` · ${summaryParts}`) : ""));
405
+ lines.push(this.theme.fg("borderMuted", "─".repeat(Math.max(1, innerWidth - 2))));
406
+
407
+ for (const item of this.runtime.items.slice(0, MAX_RENDER_ITEMS)) {
408
+ const questionLines = wrapTextWithAnsi(this.theme.fg("accent", `Q: ${item.question}`), innerWidth);
409
+ lines.push(...questionLines);
410
+
411
+ if (item.state === "loading") {
412
+ const frame = SPINNER_FRAMES[this.runtime.spinnerFrame] ?? SPINNER_FRAMES[0]!;
413
+ lines.push(this.theme.fg("warning", `${frame} Answering with ${item.model}...`));
414
+ } else {
415
+ const body = item.state === "error" ? this.theme.fg("error", item.error ?? "Unknown error") : item.answer ?? "";
416
+ const wrapped = body
417
+ .split("\n")
418
+ .flatMap((line) => wrapTextWithAnsi(line.length > 0 ? line : " ", innerWidth));
419
+ const clipped = wrapped.slice(0, MAX_RENDERED_ANSWER_LINES);
420
+ lines.push(...clipped);
421
+ if (wrapped.length > clipped.length) {
422
+ lines.push(this.theme.fg("dim", `... ${wrapped.length - clipped.length} more line(s)`));
423
+ }
424
+ }
425
+
426
+ lines.push("");
427
+ }
428
+
429
+ if (lines[lines.length - 1] === "") {
430
+ lines.pop();
431
+ }
432
+
433
+ lines.push(this.theme.fg("dim", "Use /btw <question> anytime, even while pi is still working."));
434
+ return lines.map((line) => truncateToWidth(line, width, "...", true));
435
+ }
436
+
437
+ invalidate(): void {}
438
+ }
439
+
440
+ export default function (pi: ExtensionAPI) {
441
+ const attachCurrentSessionWidget = (_event: unknown, ctx: ExtensionContext) => {
442
+ ensureWidget(ctx, getRuntime(ctx));
443
+ flushPendingForCurrentSession(pi, ctx);
444
+ };
445
+
446
+ const flush = (_event: unknown, ctx: ExtensionContext) => {
447
+ flushPendingForCurrentSession(pi, ctx);
448
+ };
449
+
450
+ pi.on("session_start", attachCurrentSessionWidget);
451
+ pi.on("session_switch", attachCurrentSessionWidget);
452
+ pi.on("agent_end", flush);
453
+ pi.on("session_before_switch", flush);
454
+ pi.on("session_before_fork", flush);
455
+ pi.on("session_shutdown", (_event, _ctx) => {
456
+ for (const runtime of runtimes.values()) {
457
+ if (runtime.spinnerTimer) clearInterval(runtime.spinnerTimer);
458
+ if (runtime.expiryTimer) clearTimeout(runtime.expiryTimer);
459
+ }
460
+ runtimes.clear();
461
+ });
462
+
463
+ pi.on("input", (event, ctx) => {
464
+ if (!ctx.hasUI) {
465
+ return { action: "continue" as const };
466
+ }
467
+ if (event.source === "extension") {
468
+ return { action: "continue" as const };
469
+ }
470
+
471
+ const question = extractBtwQuestion(event.text);
472
+ if (question === null) {
473
+ return { action: "continue" as const };
474
+ }
475
+
476
+ if (!question) {
477
+ ctx.ui.notify("Usage: /btw <question>", "warning");
478
+ return { action: "handled" as const };
479
+ }
480
+
481
+ void startBtw(question, pi, ctx);
482
+ return { action: "handled" as const };
483
+ });
484
+
485
+ pi.registerShortcut("ctrl+shift+b", {
486
+ description: "Ask the current editor text as a side question",
487
+ handler: async (ctx) => {
488
+ if (!ctx.hasUI) return;
489
+ const editorText = ctx.ui.getEditorText();
490
+ const question = normalizeShortcutQuestion(editorText);
491
+ if (!question) {
492
+ ctx.ui.notify("Type a question in the editor, then press Ctrl+Shift+B, or submit /btw <question>.", "warning");
493
+ return;
494
+ }
495
+ ctx.ui.setEditorText("");
496
+ void startBtw(question, pi, ctx);
497
+ },
498
+ });
499
+ }