@fyeeme/pi-thinking-ui 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.
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/index.ts +360 -0
- package/internal-patch.ts +441 -0
- package/package.json +62 -0
- package/parse.ts +1145 -0
- package/persistence.ts +86 -0
- package/render.ts +414 -0
- package/state.ts +417 -0
- package/types.ts +63 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- Three-mode thinking visualization: `collapsed`, `summary`, `expanded`
|
|
5
|
+
- Deterministic step derivation from raw thinking text
|
|
6
|
+
- Semantic role inference with icons and colors
|
|
7
|
+
- Scoped persistence (session, project, global) with restore precedence
|
|
8
|
+
- `/thinking-ui` command with tab completions
|
|
9
|
+
- `Alt+T` shortcut for mode cycling
|
|
10
|
+
- Statusline integration via `ctx.ui.setStatus`
|
|
11
|
+
- Degraded session fallback when runtime patching fails
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pi Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# pi-thinking-ui
|
|
2
|
+
|
|
3
|
+
Faithful, terminal-native thinking visualization for [pi](https://pi.dev). Replaces pi's built-in thinking renderer with a three-mode (collapsed / summary / expanded) view that summarizes, classifies, and renders reasoning blocks as they stream.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Three view modes** — collapsed (one line), summary (key steps), expanded (full reasoning)
|
|
8
|
+
- **Semantic step roles** — each step is classified (`inspect`, `plan`, `compare`, `verify`, `write`, `search`, `error`) with a matching icon and color
|
|
9
|
+
- **Extractive summaries** — surfaces failures, decisions, and concrete artifacts (file paths, commands, symbols) instead of meta-chatter
|
|
10
|
+
- **Per-scope state** — view mode is tracked per working directory so different projects keep their own preference
|
|
11
|
+
- **Persistent defaults** — save a default mode for the current project or globally
|
|
12
|
+
- **Live mode switching** — cycle modes mid-session without restarting
|
|
13
|
+
- **Redacted-reasoning fallback** — shows a stable placeholder when the provider hides reasoning
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
Requires the [pi](https://pi.dev) CLI.
|
|
18
|
+
|
|
19
|
+
### From npm (recommended)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Global (user) install — available in every project
|
|
23
|
+
pi install npm:@fyeeme/pi-thinking-ui
|
|
24
|
+
|
|
25
|
+
# Project-local — written to .pi/settings.json, shareable with your team
|
|
26
|
+
pi install -l npm:@fyeeme/pi-thinking-ui
|
|
27
|
+
|
|
28
|
+
# Pinned version — skipped by `pi update`
|
|
29
|
+
pi install npm:@fyeeme/pi-thinking-ui@1.0.0
|
|
30
|
+
|
|
31
|
+
# Try it once without saving (current run only)
|
|
32
|
+
pi -e npm:@fyeeme/pi-thinking-ui
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### From GitHub
|
|
36
|
+
|
|
37
|
+
Source: [`fyeeme/pi-packages`](https://github.com/fyeeme/pi-packages).
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# HTTPS shorthand
|
|
41
|
+
pi install git:github.com/fyeeme/pi-packages
|
|
42
|
+
# Pin to a tag or commit (skipped by `pi update`)
|
|
43
|
+
pi install git:github.com/fyeeme/pi-packages@v1.0.0
|
|
44
|
+
# Raw URL form
|
|
45
|
+
pi install https://github.com/fyeeme/pi-packages
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
See the Pi Packages guide on [pi.dev](https://pi.dev) for the full list of source types, scopes, and `pi update` behavior.
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
| Action | Effect |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `Alt+T` | Cycle the current session's view: collapsed → summary → expanded |
|
|
55
|
+
| `/thinking-ui` | Cycle the view, or open a mode picker in interactive mode |
|
|
56
|
+
| `/thinking-ui collapsed` | Set the session view to `collapsed` (also: `summary`, `expanded`) |
|
|
57
|
+
| `/thinking-ui project expanded` | Save `expanded` as the default for this project |
|
|
58
|
+
| `/thinking-ui global summary` | Save `summary` as the default everywhere |
|
|
59
|
+
| `/thinking-ui project clear` | Remove the saved project default |
|
|
60
|
+
|
|
61
|
+
Aliases are accepted for modes: `c`/`collapse` → `collapsed`, `s`/`summaries` → `summary`, `e`/`expand`/`full` → `expanded`.
|
|
62
|
+
|
|
63
|
+
On startup the view mode is restored from (in order): the current session's saved mode, the project default, the global default, then `summary`.
|
|
64
|
+
|
|
65
|
+
## Requirements
|
|
66
|
+
|
|
67
|
+
Pi patches an internal assistant-message component at runtime. If pi's internals are incompatible with the patch, the extension logs a warning and falls back to pi's native thinking renderer for that session; persisted defaults still apply to future compatible sessions.
|
package/index.ts
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
3
|
+
import { Key } from "@earendil-works/pi-tui";
|
|
4
|
+
import { retainThinkingUIPatch } from "./internal-patch.ts";
|
|
5
|
+
import { clearThinkingUIModePreference, readThinkingUIModePreference, writeThinkingUIModePreference } from "./persistence.ts";
|
|
6
|
+
import { parseThinkingMode } from "./parse.ts";
|
|
7
|
+
import { clearActiveThinkingState, clearThinkingMessageOwnership, getCurrentThinkingScopeKey, getThinkingUIMode, nextThinkingRefreshLabel, recordThinkingMessageScope, registerThinkingPatchRelease, resolveThinkingMessageScope, setActiveThinkingState, setCurrentThinkingScopeKey, setThinkingUIMode, takeThinkingPatchRelease } from "./state.ts";
|
|
8
|
+
import type { PersistedThinkingUIPreferenceScope, ThinkingUIMode } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
type ThinkingUICommandScope = "session" | PersistedThinkingUIPreferenceScope;
|
|
11
|
+
type ThinkingUICommandAction =
|
|
12
|
+
| { type: "set"; scope: ThinkingUICommandScope; mode?: ThinkingUIMode }
|
|
13
|
+
| { type: "clear"; scope: PersistedThinkingUIPreferenceScope };
|
|
14
|
+
|
|
15
|
+
const CUSTOM_ENTRY_TYPE = "thinking-ui.mode";
|
|
16
|
+
const DEFAULT_HIDDEN_LABEL = "Thinking...";
|
|
17
|
+
const MODE_OPTIONS: ThinkingUIMode[] = ["collapsed", "summary", "expanded"];
|
|
18
|
+
const SCOPE_OPTIONS: PersistedThinkingUIPreferenceScope[] = ["project", "global"];
|
|
19
|
+
|
|
20
|
+
function modeChangeMessage(mode: ThinkingUIMode, scope: ThinkingUICommandScope): string {
|
|
21
|
+
if (scope === "session") {
|
|
22
|
+
return `Thinking view: ${mode}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return `Thinking view: ${mode} (saved for ${scope})`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function notifyUser(ctx: ExtensionContext, message: string, level: "info" | "warning"): void {
|
|
29
|
+
if (ctx.hasUI) {
|
|
30
|
+
ctx.ui.notify(message, level);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (level === "warning") {
|
|
35
|
+
console.warn(message);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.info(message);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function readRestoredModePreference(
|
|
43
|
+
ctx: ExtensionContext,
|
|
44
|
+
scope: PersistedThinkingUIPreferenceScope,
|
|
45
|
+
): Promise<ThinkingUIMode | undefined> {
|
|
46
|
+
try {
|
|
47
|
+
return await readThinkingUIModePreference(scope, ctx.cwd);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
reportPersistenceError(ctx, error);
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function restoreMode(ctx: ExtensionContext): Promise<ThinkingUIMode> {
|
|
55
|
+
const entries = ctx.sessionManager.getEntries() as Array<{ type?: string; customType?: string; data?: { mode?: string } }>;
|
|
56
|
+
const savedEntries = entries.filter((entry) => entry.type === "custom" && entry.customType === CUSTOM_ENTRY_TYPE);
|
|
57
|
+
for (let index = savedEntries.length - 1; index >= 0; index -= 1) {
|
|
58
|
+
const sessionMode = parseThinkingMode(savedEntries[index]?.data?.mode ?? "");
|
|
59
|
+
if (sessionMode) return sessionMode;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const projectMode = await readRestoredModePreference(ctx, "project");
|
|
63
|
+
if (projectMode) return projectMode;
|
|
64
|
+
|
|
65
|
+
const globalMode = await readRestoredModePreference(ctx, "global");
|
|
66
|
+
return globalMode ?? "summary";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function refreshThinkingUI(ctx: ExtensionContext): void {
|
|
70
|
+
if (!ctx.hasUI) return;
|
|
71
|
+
setCurrentThinkingScopeKey(ctx.cwd);
|
|
72
|
+
ctx.ui.setHiddenThinkingLabel(nextThinkingRefreshLabel(DEFAULT_HIDDEN_LABEL, ctx.cwd));
|
|
73
|
+
ctx.ui.setStatus("thinking-ui", `${ctx.ui.theme.fg("muted", "thinking:")} ${ctx.ui.theme.fg("accent", getThinkingUIMode(ctx.cwd))}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function applyMode(
|
|
77
|
+
pi: ExtensionAPI,
|
|
78
|
+
ctx: ExtensionContext,
|
|
79
|
+
mode: ThinkingUIMode,
|
|
80
|
+
options?: { persistSession?: boolean; announceScope?: ThinkingUICommandScope },
|
|
81
|
+
): void {
|
|
82
|
+
setCurrentThinkingScopeKey(ctx.cwd);
|
|
83
|
+
setThinkingUIMode(mode, ctx.cwd);
|
|
84
|
+
if (options?.persistSession !== false) {
|
|
85
|
+
pi.appendEntry(CUSTOM_ENTRY_TYPE, { mode });
|
|
86
|
+
}
|
|
87
|
+
refreshThinkingUI(ctx);
|
|
88
|
+
if (options?.announceScope) {
|
|
89
|
+
notifyUser(ctx, modeChangeMessage(mode, options.announceScope), "info");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function cycleMode(current: ThinkingUIMode): ThinkingUIMode {
|
|
94
|
+
if (current === "collapsed") return "summary";
|
|
95
|
+
if (current === "summary") return "expanded";
|
|
96
|
+
return "collapsed";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parsePreferenceScope(input: string): PersistedThinkingUIPreferenceScope | undefined {
|
|
100
|
+
const normalized = input.trim().toLowerCase();
|
|
101
|
+
if (["project", "proj", "p"].includes(normalized)) return "project";
|
|
102
|
+
if (["global", "user", "g"].includes(normalized)) return "global";
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseCommandAction(args: string): ThinkingUICommandAction | undefined {
|
|
107
|
+
const trimmed = args.trim();
|
|
108
|
+
if (!trimmed) {
|
|
109
|
+
return { type: "set", scope: "session" };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const scope = parsePreferenceScope(trimmed.split(/\s+/, 1)[0] ?? "");
|
|
113
|
+
if (!scope) {
|
|
114
|
+
const mode = parseThinkingMode(trimmed);
|
|
115
|
+
return mode ? { type: "set", scope: "session", mode } : undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const tail = trimmed.replace(/^\S+\s*/, "");
|
|
119
|
+
if (!tail) {
|
|
120
|
+
return { type: "set", scope };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (["clear", "reset"].includes(tail.trim().toLowerCase())) {
|
|
124
|
+
return { type: "clear", scope };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const mode = parseThinkingMode(tail);
|
|
128
|
+
return mode ? { type: "set", scope, mode } : undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function buildCompletionItems(values: string[], prefix: string, prefixText = ""): AutocompleteItem[] | null {
|
|
132
|
+
const normalizedPrefix = prefix.trim().toLowerCase();
|
|
133
|
+
const items = values
|
|
134
|
+
.filter((value) => value.startsWith(normalizedPrefix))
|
|
135
|
+
.map((value) => ({ value: `${prefixText}${value}`, label: value }));
|
|
136
|
+
return items.length > 0 ? items : null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function thinkingModeCompletions(prefix: string): AutocompleteItem[] | null {
|
|
140
|
+
const trimmed = prefix.trim();
|
|
141
|
+
const endsWithWhitespace = /\s$/.test(prefix);
|
|
142
|
+
|
|
143
|
+
if (!trimmed) {
|
|
144
|
+
return [
|
|
145
|
+
...MODE_OPTIONS.map((value) => ({ value, label: value })),
|
|
146
|
+
...SCOPE_OPTIONS.map((value) => ({ value, label: value })),
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const parts = trimmed.split(/\s+/);
|
|
151
|
+
if (parts.length === 1 && !endsWithWhitespace) {
|
|
152
|
+
return buildCompletionItems([...MODE_OPTIONS, ...SCOPE_OPTIONS], parts[0] ?? "");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const scope = parsePreferenceScope(parts[0] ?? "");
|
|
156
|
+
if (!scope) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const valuePrefix = `${scope} `;
|
|
161
|
+
const nestedPrefix = endsWithWhitespace ? "" : parts.slice(1).join(" " );
|
|
162
|
+
return buildCompletionItems([...MODE_OPTIONS, "clear"], nestedPrefix, valuePrefix);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function selectMode(ctx: ExtensionContext): Promise<ThinkingUIMode | undefined> {
|
|
166
|
+
if (!ctx.hasUI) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const choice = await ctx.ui.select("Thinking view", MODE_OPTIONS);
|
|
171
|
+
return choice ? parseThinkingMode(choice) : undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function reportPersistenceError(ctx: ExtensionContext, error: unknown): void {
|
|
175
|
+
notifyUser(ctx, `Thinking UI persistence error: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function reportPatchError(ctx: ExtensionContext, error: unknown): void {
|
|
179
|
+
notifyUser(ctx, `Thinking UI patch error: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export default function thinkingUIExtension(pi: ExtensionAPI): void {
|
|
183
|
+
let sessionScopeKey = getCurrentThinkingScopeKey();
|
|
184
|
+
const degradedSessionScopes = new Set<string>();
|
|
185
|
+
const setSessionScopeKey = (scopeKey: string): string => {
|
|
186
|
+
sessionScopeKey = scopeKey;
|
|
187
|
+
setCurrentThinkingScopeKey(scopeKey);
|
|
188
|
+
return sessionScopeKey;
|
|
189
|
+
};
|
|
190
|
+
const markSessionDegraded = (scopeKey: string, degraded: boolean): void => {
|
|
191
|
+
if (degraded) {
|
|
192
|
+
degradedSessionScopes.add(scopeKey);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
degradedSessionScopes.delete(scopeKey);
|
|
196
|
+
};
|
|
197
|
+
const isSessionDegraded = (scopeKey: string): boolean => degradedSessionScopes.has(scopeKey);
|
|
198
|
+
const degradedSessionMessage = (): string => "Thinking UI is using Pi's native thinking renderer for this session; live mode switching is disabled.";
|
|
199
|
+
const futureCompatibleSessionMessage = (scope: PersistedThinkingUIPreferenceScope, action: "saved" | "cleared"): string => `${action === "saved" ? "Saved" : "Cleared"} ${scope} thinking view default for future compatible sessions; the current session is using Pi's native thinking renderer.`;
|
|
200
|
+
|
|
201
|
+
pi.registerCommand("thinking-ui", {
|
|
202
|
+
description: "Switch thinking view or set/clear project/global defaults",
|
|
203
|
+
getArgumentCompletions: thinkingModeCompletions,
|
|
204
|
+
handler: async (args, ctx) => {
|
|
205
|
+
const action = parseCommandAction(args);
|
|
206
|
+
if (!action) {
|
|
207
|
+
notifyUser(ctx, "Usage: /thinking-ui [collapsed|summary|expanded] | [project|global] [collapsed|summary|expanded|clear]", "warning");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const degraded = isSessionDegraded(ctx.cwd);
|
|
212
|
+
if (action.type === "clear") {
|
|
213
|
+
try {
|
|
214
|
+
await clearThinkingUIModePreference(action.scope, ctx.cwd);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
reportPersistenceError(ctx, error);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (degraded) {
|
|
221
|
+
notifyUser(ctx, futureCompatibleSessionMessage(action.scope, "cleared"), "info");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
refreshThinkingUI(ctx);
|
|
226
|
+
notifyUser(ctx, `Cleared ${action.scope} thinking view default`, "info");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const selectedMode = action.mode ?? (await selectMode(ctx));
|
|
231
|
+
if (!selectedMode) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (action.scope !== "session") {
|
|
236
|
+
try {
|
|
237
|
+
await writeThinkingUIModePreference(action.scope, ctx.cwd, selectedMode);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
reportPersistenceError(ctx, error);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (degraded) {
|
|
245
|
+
if (action.scope === "session") {
|
|
246
|
+
notifyUser(ctx, degradedSessionMessage(), "warning");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
notifyUser(ctx, futureCompatibleSessionMessage(action.scope, "saved"), "info");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
applyMode(pi, ctx, selectedMode, { announceScope: action.scope });
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
pi.registerShortcut(Key.alt("t"), {
|
|
258
|
+
description: "Cycle thinking view (collapsed, summary, expanded)",
|
|
259
|
+
handler: async (ctx) => {
|
|
260
|
+
if (isSessionDegraded(ctx.cwd)) {
|
|
261
|
+
notifyUser(ctx, degradedSessionMessage(), "warning");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const nextMode = cycleMode(getThinkingUIMode(ctx.cwd));
|
|
265
|
+
applyMode(pi, ctx, nextMode, { announceScope: "session" });
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
270
|
+
const activeScopeKey = setSessionScopeKey(ctx.cwd);
|
|
271
|
+
clearActiveThinkingState(undefined, activeScopeKey);
|
|
272
|
+
try {
|
|
273
|
+
registerThinkingPatchRelease(activeScopeKey, await retainThinkingUIPatch());
|
|
274
|
+
markSessionDegraded(activeScopeKey, false);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
markSessionDegraded(activeScopeKey, true);
|
|
277
|
+
reportPatchError(ctx, error);
|
|
278
|
+
notifyUser(ctx, degradedSessionMessage(), "warning");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const restoredMode = await restoreMode(ctx);
|
|
283
|
+
applyMode(pi, ctx, restoredMode, { persistSession: false });
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
pi.on("message_start", async (event) => {
|
|
287
|
+
if (event.message.role === "assistant") {
|
|
288
|
+
recordThinkingMessageScope(event.message, sessionScopeKey);
|
|
289
|
+
const ownerScopeKey = resolveThinkingMessageScope(event.message, sessionScopeKey);
|
|
290
|
+
const timestamp = typeof (event.message as { timestamp?: unknown }).timestamp === "number"
|
|
291
|
+
? (event.message as { timestamp: number }).timestamp
|
|
292
|
+
: undefined;
|
|
293
|
+
clearActiveThinkingState(timestamp, ownerScopeKey);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
pi.on("message_update", async (event) => {
|
|
298
|
+
if (event.message.role !== "assistant") return;
|
|
299
|
+
recordThinkingMessageScope(event.message, sessionScopeKey);
|
|
300
|
+
const ownerScopeKey = resolveThinkingMessageScope(event.message, sessionScopeKey);
|
|
301
|
+
const assistantEvent = event.assistantMessageEvent;
|
|
302
|
+
if (assistantEvent.type === "thinking_start" || assistantEvent.type === "thinking_delta") {
|
|
303
|
+
setActiveThinkingState({
|
|
304
|
+
active: true,
|
|
305
|
+
messageTimestamp: event.message.timestamp,
|
|
306
|
+
contentIndex: assistantEvent.contentIndex,
|
|
307
|
+
}, ownerScopeKey);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (
|
|
312
|
+
assistantEvent.type === "thinking_end" ||
|
|
313
|
+
assistantEvent.type === "text_start" ||
|
|
314
|
+
assistantEvent.type === "text_delta" ||
|
|
315
|
+
assistantEvent.type === "text_end" ||
|
|
316
|
+
assistantEvent.type === "toolcall_start" ||
|
|
317
|
+
assistantEvent.type === "toolcall_delta" ||
|
|
318
|
+
assistantEvent.type === "toolcall_end"
|
|
319
|
+
) {
|
|
320
|
+
clearActiveThinkingState(event.message.timestamp, ownerScopeKey);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
pi.on("message_end", async (event) => {
|
|
325
|
+
if (event.message.role === "assistant") {
|
|
326
|
+
recordThinkingMessageScope(event.message, sessionScopeKey);
|
|
327
|
+
const ownerScopeKey = resolveThinkingMessageScope(event.message, sessionScopeKey);
|
|
328
|
+
const timestamp = typeof (event.message as { timestamp?: unknown }).timestamp === "number"
|
|
329
|
+
? (event.message as { timestamp: number }).timestamp
|
|
330
|
+
: undefined;
|
|
331
|
+
clearActiveThinkingState(timestamp, ownerScopeKey);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
pi.on("agent_end", async () => {
|
|
336
|
+
clearActiveThinkingState(undefined, sessionScopeKey);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
340
|
+
const activeScopeKey = setSessionScopeKey(ctx.cwd);
|
|
341
|
+
clearActiveThinkingState(undefined, activeScopeKey);
|
|
342
|
+
clearThinkingMessageOwnership(activeScopeKey);
|
|
343
|
+
markSessionDegraded(activeScopeKey, false);
|
|
344
|
+
if (ctx.hasUI) {
|
|
345
|
+
ctx.ui.setStatus("thinking-ui", undefined);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const releasePatch = takeThinkingPatchRelease(activeScopeKey);
|
|
349
|
+
if (!releasePatch) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
await releasePatch();
|
|
355
|
+
} catch (error) {
|
|
356
|
+
registerThinkingPatchRelease(activeScopeKey, releasePatch);
|
|
357
|
+
reportPatchError(ctx, error);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|