@oh-my-pi/pi-coding-agent 16.1.6 → 16.1.8
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 +52 -0
- package/dist/cli.js +3160 -3139
- package/dist/types/cli/session-picker.d.ts +0 -1
- package/dist/types/cli/tiny-models-cli.d.ts +2 -0
- package/dist/types/collab/protocol.d.ts +20 -1
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +57 -6
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +5 -1
- package/dist/types/internal-urls/filesystem-resource.d.ts +9 -0
- package/dist/types/mcp/loader.d.ts +3 -2
- package/dist/types/mcp/manager.d.ts +4 -3
- package/dist/types/mcp/startup-events.d.ts +21 -4
- package/dist/types/mnemopi/config.d.ts +1 -0
- package/dist/types/modes/components/agent-hub.d.ts +1 -0
- package/dist/types/modes/components/agent-transcript-viewer.d.ts +1 -0
- package/dist/types/modes/components/assistant-message.d.ts +5 -1
- package/dist/types/modes/components/btw-panel.d.ts +2 -0
- package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +0 -2
- package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/controllers/streaming-reveal.d.ts +3 -2
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +5 -3
- package/dist/types/session/session-history-format.d.ts +25 -0
- package/dist/types/session/snapcompact-inline.d.ts +1 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +7 -4
- package/dist/types/slash-commands/types.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +1 -0
- package/dist/types/tiny/models.d.ts +11 -7
- package/dist/types/tools/todo.d.ts +1 -0
- package/dist/types/utils/thinking-display.d.ts +2 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +104 -0
- package/src/advisor/runtime.ts +38 -2
- package/src/cli/session-picker.ts +1 -2
- package/src/cli/tiny-models-cli.ts +7 -2
- package/src/collab/guest.ts +172 -20
- package/src/collab/host.ts +47 -5
- package/src/collab/protocol.ts +16 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +59 -5
- package/src/edit/renderer.ts +8 -12
- package/src/extensibility/legacy-pi-ai-shim.ts +16 -4
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/filesystem-resource.ts +34 -0
- package/src/internal-urls/local-protocol.ts +7 -1
- package/src/internal-urls/memory-protocol.ts +5 -1
- package/src/internal-urls/skill-protocol.ts +20 -4
- package/src/internal-urls/vault-protocol.ts +5 -2
- package/src/main.ts +8 -8
- package/src/mcp/loader.ts +4 -3
- package/src/mcp/manager.ts +35 -15
- package/src/mcp/startup-events.ts +106 -11
- package/src/mnemopi/config.ts +2 -0
- package/src/mnemopi/state.ts +3 -1
- package/src/modes/components/agent-hub.ts +4 -0
- package/src/modes/components/agent-transcript-viewer.ts +2 -0
- package/src/modes/components/assistant-message.ts +217 -18
- package/src/modes/components/btw-panel.ts +15 -3
- package/src/modes/components/chat-transcript-builder.ts +8 -2
- package/src/modes/components/model-selector.ts +72 -9
- package/src/modes/components/omfg-panel.ts +1 -1
- package/src/modes/components/session-selector.ts +4 -9
- package/src/modes/components/snapcompact-shape-preview.ts +1 -1
- package/src/modes/components/tool-execution.ts +10 -2
- package/src/modes/controllers/btw-controller.ts +32 -7
- package/src/modes/controllers/event-controller.ts +24 -4
- package/src/modes/controllers/input-controller.ts +43 -21
- package/src/modes/controllers/selector-controller.ts +23 -13
- package/src/modes/controllers/streaming-reveal.ts +78 -20
- package/src/modes/controllers/todo-command-controller.ts +9 -10
- package/src/modes/interactive-mode.ts +83 -8
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/advisor/system.md +1 -1
- package/src/prompts/system/side-channel-no-tools.md +3 -0
- package/src/prompts/system/system-prompt.md +164 -156
- package/src/sdk.ts +12 -8
- package/src/session/agent-session.ts +349 -84
- package/src/session/history-storage.ts +15 -16
- package/src/session/session-history-format.ts +41 -1
- package/src/session/snapcompact-inline.ts +9 -6
- package/src/slash-commands/builtin-registry.ts +147 -21
- package/src/slash-commands/helpers/todo.ts +12 -6
- package/src/slash-commands/types.ts +2 -0
- package/src/system-prompt.ts +6 -11
- package/src/tiny/models.ts +7 -3
- package/src/tiny/title-client.ts +8 -2
- package/src/tiny/worker.ts +1 -0
- package/src/tools/search.ts +10 -1
- package/src/tools/sqlite-reader.ts +59 -3
- package/src/tools/todo.ts +6 -0
- package/src/tools/write.ts +4 -6
- package/src/utils/thinking-display.ts +78 -0
|
@@ -31,7 +31,13 @@ import {
|
|
|
31
31
|
JSON_TREE_SCALAR_LEN_EXPANDED,
|
|
32
32
|
renderJsonTreeLines,
|
|
33
33
|
} from "../../tools/json-tree";
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
formatExpandHint,
|
|
36
|
+
formatStatusIcon,
|
|
37
|
+
replaceTabs,
|
|
38
|
+
resolveImageOptions,
|
|
39
|
+
truncateToWidth,
|
|
40
|
+
} from "../../tools/render-utils";
|
|
35
41
|
import { toolRenderers } from "../../tools/renderers";
|
|
36
42
|
import { TODO_STRIKE_TOTAL_FRAMES } from "../../tools/todo";
|
|
37
43
|
import { isFramedBlockComponent, renderStatusLine, WidthAwareText } from "../../tui";
|
|
@@ -865,9 +871,11 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
865
871
|
this.#multiFileBoxes.push(pendingSpacer);
|
|
866
872
|
this.addChild(pendingSpacer);
|
|
867
873
|
const pendingBox = new Box(0, 0);
|
|
874
|
+
const spinner =
|
|
875
|
+
this.#spinnerFrame !== undefined ? formatStatusIcon("running", theme, this.#spinnerFrame) : "";
|
|
868
876
|
const pendingText = renderStatusLine(
|
|
869
877
|
{
|
|
870
|
-
|
|
878
|
+
iconOverride: spinner,
|
|
871
879
|
title: "Edit",
|
|
872
880
|
description: theme.fg("dim", `${remaining} more file${remaining > 1 ? "s" : ""} pending…`),
|
|
873
881
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import btwUserPrompt from "../../prompts/system/btw-user.md" with { type: "text" };
|
|
4
|
+
import { copyToClipboard } from "../../utils/clipboard";
|
|
4
5
|
import { BtwPanelComponent } from "../components/btw-panel";
|
|
5
6
|
import type { InteractiveModeContext } from "../types";
|
|
6
7
|
|
|
@@ -39,6 +40,8 @@ export class BtwController {
|
|
|
39
40
|
#lastAssistantMessage: AssistantMessage | undefined;
|
|
40
41
|
#lastLeafId: string | null | undefined;
|
|
41
42
|
#branchInFlight = false;
|
|
43
|
+
#lastCopyText: string | undefined;
|
|
44
|
+
#copyInFlight = false;
|
|
42
45
|
|
|
43
46
|
constructor(private readonly ctx: InteractiveModeContext) {}
|
|
44
47
|
|
|
@@ -58,6 +61,27 @@ export class BtwController {
|
|
|
58
61
|
);
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
canCopy(): boolean {
|
|
65
|
+
return (
|
|
66
|
+
!this.#copyInFlight && this.#activeRequest?.component.isCopyable() === true && this.#lastCopyText !== undefined
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async handleCopy(): Promise<boolean> {
|
|
71
|
+
if (!this.canCopy() || this.#lastCopyText === undefined) return false;
|
|
72
|
+
this.#copyInFlight = true;
|
|
73
|
+
try {
|
|
74
|
+
await copyToClipboard(this.#lastCopyText);
|
|
75
|
+
this.ctx.showStatus("Copied /btw answer to clipboard");
|
|
76
|
+
return true;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
79
|
+
return true;
|
|
80
|
+
} finally {
|
|
81
|
+
this.#copyInFlight = false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
61
85
|
async handleBranch(): Promise<boolean> {
|
|
62
86
|
if (!this.canBranch() || !this.#lastQuestion || !this.#lastAssistantMessage) return false;
|
|
63
87
|
this.#branchInFlight = true;
|
|
@@ -123,17 +147,17 @@ export class BtwController {
|
|
|
123
147
|
if (!this.#isActiveRequest(request)) {
|
|
124
148
|
return;
|
|
125
149
|
}
|
|
126
|
-
|
|
127
|
-
request.component.setAnswer(replyText);
|
|
128
|
-
}
|
|
150
|
+
request.component.setAnswer(replyText);
|
|
129
151
|
request.component.markComplete();
|
|
130
|
-
|
|
152
|
+
const copyText = request.component.getCopyText();
|
|
153
|
+
if (copyText !== undefined) {
|
|
131
154
|
this.#lastQuestion = request.question;
|
|
132
155
|
this.#lastReplyText = replyText;
|
|
156
|
+
this.#lastCopyText = copyText;
|
|
133
157
|
this.#lastAssistantMessage = assistantMessageWithReplyText(assistantMessage, replyText);
|
|
134
158
|
this.#lastLeafId = request.leafId;
|
|
135
159
|
} else {
|
|
136
|
-
this.#
|
|
160
|
+
this.#clearCompletedState();
|
|
137
161
|
}
|
|
138
162
|
} catch (error) {
|
|
139
163
|
if (!this.#isActiveRequest(request)) {
|
|
@@ -151,7 +175,7 @@ export class BtwController {
|
|
|
151
175
|
const request = this.#activeRequest;
|
|
152
176
|
if (!request) return;
|
|
153
177
|
this.#activeRequest = undefined;
|
|
154
|
-
this.#
|
|
178
|
+
this.#clearCompletedState();
|
|
155
179
|
if (options.abort) {
|
|
156
180
|
request.abortController.abort();
|
|
157
181
|
}
|
|
@@ -160,10 +184,11 @@ export class BtwController {
|
|
|
160
184
|
this.ctx.ui.requestRender();
|
|
161
185
|
}
|
|
162
186
|
|
|
163
|
-
#
|
|
187
|
+
#clearCompletedState(): void {
|
|
164
188
|
this.#lastQuestion = undefined;
|
|
165
189
|
this.#lastReplyText = undefined;
|
|
166
190
|
this.#lastAssistantMessage = undefined;
|
|
191
|
+
this.#lastCopyText = undefined;
|
|
167
192
|
this.#lastLeafId = undefined;
|
|
168
193
|
}
|
|
169
194
|
|
|
@@ -87,6 +87,7 @@ export class EventController {
|
|
|
87
87
|
this.#streamingReveal = new StreamingRevealController({
|
|
88
88
|
getSmoothStreaming: () => this.ctx.settings.get("display.smoothStreaming"),
|
|
89
89
|
getHideThinkingBlock: () => this.ctx.hideThinkingBlock,
|
|
90
|
+
getProseOnlyThinking: () => this.ctx.proseOnlyThinking,
|
|
90
91
|
requestRender: () => this.ctx.ui.requestRender(),
|
|
91
92
|
});
|
|
92
93
|
this.#toolArgsReveal = new ToolArgsRevealController({
|
|
@@ -333,6 +334,7 @@ export class EventController {
|
|
|
333
334
|
() => this.ctx.ui.requestRender(),
|
|
334
335
|
this.ctx.viewSession.extensionRunner?.getAssistantThinkingRenderers(),
|
|
335
336
|
this.ctx.ui.imageBudget,
|
|
337
|
+
this.ctx.proseOnlyThinking,
|
|
336
338
|
);
|
|
337
339
|
this.ctx.streamingMessage = event.message;
|
|
338
340
|
this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
|
|
@@ -920,6 +922,17 @@ export class EventController {
|
|
|
920
922
|
}
|
|
921
923
|
}
|
|
922
924
|
|
|
925
|
+
/**
|
|
926
|
+
* Trailing Esc hint for live maintenance loaders. While a subagent is
|
|
927
|
+
* focused, Esc returns to main instead of cancelling its maintenance
|
|
928
|
+
* (#2819), so the loader drops the hint entirely rather than advertise a
|
|
929
|
+
* cancel that no longer happens. Includes the leading space so the focused
|
|
930
|
+
* label carries no dangling whitespace.
|
|
931
|
+
*/
|
|
932
|
+
#maintenanceEscHint(): string {
|
|
933
|
+
return this.ctx.focusedAgentId ? "" : " (esc to cancel)";
|
|
934
|
+
}
|
|
935
|
+
|
|
923
936
|
async #handleAutoCompactionStart(
|
|
924
937
|
event: Extract<AgentSessionEvent, { type: "auto_compaction_start" }>,
|
|
925
938
|
): Promise<void> {
|
|
@@ -939,12 +952,14 @@ export class EventController {
|
|
|
939
952
|
? "Auto-handoff"
|
|
940
953
|
: event.action === "shake"
|
|
941
954
|
? "Auto-shake"
|
|
942
|
-
:
|
|
955
|
+
: event.action === "snapcompact"
|
|
956
|
+
? "Auto-snapcompact"
|
|
957
|
+
: "Auto context-full maintenance";
|
|
943
958
|
this.ctx.autoCompactionLoader = new Loader(
|
|
944
959
|
this.ctx.ui,
|
|
945
960
|
spinner => theme.fg("accent", spinner),
|
|
946
961
|
text => theme.fg("muted", text),
|
|
947
|
-
`${reasonText}${actionLabel}
|
|
962
|
+
`${reasonText}${actionLabel}…${this.#maintenanceEscHint()}`,
|
|
948
963
|
getSymbolTheme().spinnerFrames,
|
|
949
964
|
);
|
|
950
965
|
this.ctx.statusContainer.addChild(this.ctx.autoCompactionLoader);
|
|
@@ -960,13 +975,16 @@ export class EventController {
|
|
|
960
975
|
}
|
|
961
976
|
const isHandoffAction = event.action === "handoff";
|
|
962
977
|
const isShakeAction = event.action === "shake";
|
|
978
|
+
const isSnapcompactAction = event.action === "snapcompact";
|
|
963
979
|
if (event.aborted) {
|
|
964
980
|
this.ctx.showStatus(
|
|
965
981
|
isHandoffAction
|
|
966
982
|
? "Auto-handoff cancelled"
|
|
967
983
|
: isShakeAction
|
|
968
984
|
? "Auto-shake cancelled"
|
|
969
|
-
:
|
|
985
|
+
: isSnapcompactAction
|
|
986
|
+
? "Auto-snapcompact cancelled"
|
|
987
|
+
: "Auto context-full maintenance cancelled",
|
|
970
988
|
);
|
|
971
989
|
} else if (isShakeAction) {
|
|
972
990
|
// Shake produces no CompactionResult; rebuild on success, suppress benign skips.
|
|
@@ -1005,6 +1023,8 @@ export class EventController {
|
|
|
1005
1023
|
} else if (event.skipped) {
|
|
1006
1024
|
// Benign skip: no model selected, no candidate models available, or nothing
|
|
1007
1025
|
// to compact yet. Not a failure — suppress the warning.
|
|
1026
|
+
} else if (isSnapcompactAction) {
|
|
1027
|
+
this.ctx.showWarning("Auto-snapcompact maintenance failed; continuing without maintenance");
|
|
1008
1028
|
} else {
|
|
1009
1029
|
this.ctx.showWarning("Auto context-full maintenance failed; continuing without maintenance");
|
|
1010
1030
|
}
|
|
@@ -1027,7 +1047,7 @@ export class EventController {
|
|
|
1027
1047
|
this.ctx.ui,
|
|
1028
1048
|
spinner => theme.fg("warning", spinner),
|
|
1029
1049
|
text => theme.fg("muted", text),
|
|
1030
|
-
`Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s
|
|
1050
|
+
`Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s…${this.#maintenanceEscHint()}`,
|
|
1031
1051
|
getSymbolTheme().spinnerFrames,
|
|
1032
1052
|
);
|
|
1033
1053
|
this.ctx.statusContainer.addChild(this.ctx.retryLoader);
|
|
@@ -99,6 +99,7 @@ export class InputController {
|
|
|
99
99
|
#enhancedPaste?: EnhancedPasteController;
|
|
100
100
|
#focusedLeftTapListenerInstalled = false;
|
|
101
101
|
#btwBranchListenerInstalled = false;
|
|
102
|
+
#btwCopyListenerInstalled = false;
|
|
102
103
|
// Tap counter for the double-← gesture; reset whenever a quiet gap
|
|
103
104
|
// (>= LEFT_DOUBLE_TAP_MAX_GAP_MS) starts a fresh sequence. See
|
|
104
105
|
// #detectLeftDoubleTap.
|
|
@@ -169,11 +170,23 @@ export class InputController {
|
|
|
169
170
|
this.ctx.ui.addInputListener(data => {
|
|
170
171
|
if (!matchesKey(data, "b")) return undefined;
|
|
171
172
|
if (!this.ctx.canBranchBtw()) return undefined;
|
|
173
|
+
if (this.ctx.ui.getFocused() !== this.ctx.editor) return undefined;
|
|
172
174
|
if (this.ctx.editor.getText().trim()) return undefined;
|
|
173
175
|
void this.ctx.handleBtwBranchKey();
|
|
174
176
|
return { consume: true };
|
|
175
177
|
});
|
|
176
178
|
}
|
|
179
|
+
if (!this.#btwCopyListenerInstalled) {
|
|
180
|
+
this.#btwCopyListenerInstalled = true;
|
|
181
|
+
this.ctx.ui.addInputListener(data => {
|
|
182
|
+
if (!matchesKey(data, "c")) return undefined;
|
|
183
|
+
if (!this.ctx.canCopyBtw()) return undefined;
|
|
184
|
+
if (this.ctx.ui.getFocused() !== this.ctx.editor) return undefined;
|
|
185
|
+
if (this.ctx.editor.getText().trim()) return undefined;
|
|
186
|
+
void this.ctx.handleBtwCopyKey();
|
|
187
|
+
return { consume: true };
|
|
188
|
+
});
|
|
189
|
+
}
|
|
177
190
|
this.ctx.editor.onEscape = () => {
|
|
178
191
|
// Active context maintenance owns Esc: auto/manual compaction,
|
|
179
192
|
// handoff generation, and auto-retry backoff all advertise
|
|
@@ -182,27 +195,37 @@ export class InputController {
|
|
|
182
195
|
// to clobber the single saved-handler slot (auto-compaction start
|
|
183
196
|
// → /compact → auto end → manual finally), leaving Esc wired to a
|
|
184
197
|
// stale no-op closure until restart.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
198
|
+
//
|
|
199
|
+
// While a subagent is focused, Esc honors the advertised view action
|
|
200
|
+
// ("Esc returns to main") instead of cancelling maintenance —
|
|
201
|
+
// accidentally killing a focused subagent's compaction on the way out
|
|
202
|
+
// was #2819. The auto-maintenance loaders relabel their hint to match
|
|
203
|
+
// (see EventController). Main-session maintenance still owns Esc and
|
|
204
|
+
// stays cancellable from the main view (focused submit gates /compact
|
|
205
|
+
// and handoff, so manual maintenance is main-only anyway).
|
|
206
|
+
if (!this.ctx.focusedAgentId) {
|
|
207
|
+
const viewSession = this.ctx.viewSession;
|
|
208
|
+
let aborted = false;
|
|
209
|
+
if (viewSession.isCompacting) {
|
|
210
|
+
try {
|
|
211
|
+
viewSession.abortCompaction();
|
|
212
|
+
} catch {}
|
|
213
|
+
aborted = true;
|
|
214
|
+
}
|
|
215
|
+
if (viewSession.isGeneratingHandoff) {
|
|
216
|
+
try {
|
|
217
|
+
viewSession.abortHandoff();
|
|
218
|
+
} catch {}
|
|
219
|
+
aborted = true;
|
|
220
|
+
}
|
|
221
|
+
if (viewSession.isRetrying) {
|
|
222
|
+
try {
|
|
223
|
+
viewSession.abortRetry();
|
|
224
|
+
} catch {}
|
|
225
|
+
aborted = true;
|
|
226
|
+
}
|
|
227
|
+
if (aborted) return;
|
|
204
228
|
}
|
|
205
|
-
if (aborted) return;
|
|
206
229
|
|
|
207
230
|
if (this.ctx.loopModeEnabled) {
|
|
208
231
|
this.ctx.pauseLoop();
|
|
@@ -1524,7 +1547,6 @@ export class InputController {
|
|
|
1524
1547
|
toggleThinkingBlockVisibility(): void {
|
|
1525
1548
|
this.ctx.hideThinkingBlock = !this.ctx.hideThinkingBlock;
|
|
1526
1549
|
this.ctx.settings.set("hideThinkingBlock", this.ctx.hideThinkingBlock);
|
|
1527
|
-
this.ctx.session.agent.hideThinkingSummary = this.ctx.hideThinkingBlock;
|
|
1528
1550
|
|
|
1529
1551
|
for (const child of this.ctx.chatContainer.children) {
|
|
1530
1552
|
if (child instanceof AssistantMessageComponent) {
|
|
@@ -71,6 +71,10 @@ import { buildCopyTargets } from "../utils/copy-targets";
|
|
|
71
71
|
|
|
72
72
|
const MANUAL_LOGIN_TIP = "Tip: You can complete pairing with /login <redirect URL>.";
|
|
73
73
|
|
|
74
|
+
export function formatTemporaryModelStatus(modelLabel: string, roleSelectorHint = "Alt+M"): string {
|
|
75
|
+
return `Temporary model selection is session-only: ${modelLabel}. Use ${roleSelectorHint} or /model for role models (default/smol/plan/task/slow/custom roles).`;
|
|
76
|
+
}
|
|
77
|
+
|
|
74
78
|
export class SelectorController {
|
|
75
79
|
constructor(private ctx: InteractiveModeContext) {}
|
|
76
80
|
|
|
@@ -314,9 +318,8 @@ export class SelectorController {
|
|
|
314
318
|
}
|
|
315
319
|
}
|
|
316
320
|
break;
|
|
317
|
-
case "
|
|
321
|
+
case "hideThinkingBlock":
|
|
318
322
|
this.ctx.hideThinkingBlock = value as boolean;
|
|
319
|
-
this.ctx.session.agent.hideThinkingSummary = value as boolean;
|
|
320
323
|
for (const child of this.ctx.chatContainer.children) {
|
|
321
324
|
if (child instanceof AssistantMessageComponent) {
|
|
322
325
|
child.setHideThinkingBlock(value as boolean);
|
|
@@ -327,6 +330,18 @@ export class SelectorController {
|
|
|
327
330
|
// InputController.toggleThinkingBlockVisibility).
|
|
328
331
|
this.ctx.ui.resetDisplay();
|
|
329
332
|
break;
|
|
333
|
+
case "proseOnlyThinking":
|
|
334
|
+
this.ctx.proseOnlyThinking = value as boolean;
|
|
335
|
+
for (const child of this.ctx.chatContainer.children) {
|
|
336
|
+
if (child instanceof AssistantMessageComponent) {
|
|
337
|
+
child.setProseOnlyThinking(value as boolean);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
this.ctx.ui.resetDisplay();
|
|
341
|
+
break;
|
|
342
|
+
case "omitThinking":
|
|
343
|
+
this.ctx.session.agent.hideThinkingSummary = value as boolean;
|
|
344
|
+
break;
|
|
330
345
|
case "display.cacheMissMarker":
|
|
331
346
|
// Rebuild re-runs the usage-based detection under the new setting so
|
|
332
347
|
// markers appear/disappear; full reset retires any already committed
|
|
@@ -483,7 +498,8 @@ export class SelectorController {
|
|
|
483
498
|
}
|
|
484
499
|
this.ctx.statusLine.invalidate();
|
|
485
500
|
this.ctx.updateEditorBorderColor();
|
|
486
|
-
this.ctx.
|
|
501
|
+
const roleSelectorHint = this.ctx.keybindings.getKeys("app.model.select")[0] ?? "Alt+M";
|
|
502
|
+
this.ctx.showStatus(formatTemporaryModelStatus(selector ?? model.id, roleSelectorHint));
|
|
487
503
|
done();
|
|
488
504
|
this.ctx.ui.requestRender();
|
|
489
505
|
} else if (role === "default") {
|
|
@@ -817,14 +833,9 @@ export class SelectorController {
|
|
|
817
833
|
this.ctx.sessionManager.getCwd(),
|
|
818
834
|
this.ctx.sessionManager.getSessionDir(),
|
|
819
835
|
);
|
|
820
|
-
//
|
|
821
|
-
//
|
|
822
|
-
|
|
823
|
-
let startInAllScope = false;
|
|
824
|
-
if (sessions.length === 0) {
|
|
825
|
-
allSessions = await SessionManager.listAll();
|
|
826
|
-
startInAllScope = allSessions.length > 0;
|
|
827
|
-
}
|
|
836
|
+
// Always open in current-folder scope; the empty-state hint in SessionList
|
|
837
|
+
// invites the user to Tab into all-projects rather than silently surfacing
|
|
838
|
+
// every project's history when the cwd has nothing to resume. See #3099.
|
|
828
839
|
const historyStorage = this.ctx.historyStorage;
|
|
829
840
|
const historyMatcher = historyStorage ? (query: string) => historyStorage.matchingSessionIds(query) : undefined;
|
|
830
841
|
this.showSelector(done => {
|
|
@@ -858,8 +869,6 @@ export class SelectorController {
|
|
|
858
869
|
},
|
|
859
870
|
historyMatcher,
|
|
860
871
|
loadAllSessions: () => SessionManager.listAll(),
|
|
861
|
-
allSessions,
|
|
862
|
-
startInAllScope,
|
|
863
872
|
getTerminalRows: () => this.ctx.ui.terminal.rows,
|
|
864
873
|
},
|
|
865
874
|
);
|
|
@@ -1264,6 +1273,7 @@ export class SelectorController {
|
|
|
1264
1273
|
getMessageRenderer: type => this.ctx.session.extensionRunner?.getMessageRenderer(type),
|
|
1265
1274
|
cwd: this.ctx.sessionManager.getCwd(),
|
|
1266
1275
|
hideThinkingBlock: () => this.ctx.hideThinkingBlock,
|
|
1276
|
+
proseOnlyThinking: () => this.ctx.proseOnlyThinking,
|
|
1267
1277
|
focusAgent: id => this.ctx.focusAgentSession(id),
|
|
1268
1278
|
sessionFile: this.ctx.sessionManager.getSessionFile() ?? null,
|
|
1269
1279
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import { getSegmenter } from "@oh-my-pi/pi-tui";
|
|
3
3
|
import { LRUCache } from "lru-cache/raw";
|
|
4
|
-
import {
|
|
4
|
+
import { formatThinkingForDisplay, hasDisplayableThinking } from "../../utils/thinking-display";
|
|
5
5
|
import type { AssistantMessageComponent } from "../components/assistant-message";
|
|
6
6
|
|
|
7
7
|
export const STREAMING_REVEAL_FRAME_MS = 1000 / 30;
|
|
@@ -9,11 +9,13 @@ export const MIN_STEP = 3;
|
|
|
9
9
|
export const CATCHUP_FRAMES = 8;
|
|
10
10
|
|
|
11
11
|
type AssistantContentBlock = AssistantMessage["content"][number];
|
|
12
|
+
type DisplayThinkingContentBlock = Extract<AssistantContentBlock, { type: "thinking" }> & { rawThinking?: string };
|
|
12
13
|
type StreamingRevealComponent = Pick<AssistantMessageComponent, "updateContent">;
|
|
13
14
|
|
|
14
15
|
type StreamingRevealControllerOptions = {
|
|
15
16
|
getSmoothStreaming(): boolean;
|
|
16
17
|
getHideThinkingBlock(): boolean;
|
|
18
|
+
getProseOnlyThinking(): boolean;
|
|
17
19
|
requestRender(): void;
|
|
18
20
|
};
|
|
19
21
|
|
|
@@ -83,13 +85,16 @@ function sliceGraphemes(text: string, units: number): string {
|
|
|
83
85
|
return text;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
export function visibleUnits(message: AssistantMessage, hideThinking: boolean): number {
|
|
88
|
+
export function visibleUnits(message: AssistantMessage, hideThinking: boolean, proseOnly = true): number {
|
|
87
89
|
let total = 0;
|
|
88
90
|
for (const block of message.content) {
|
|
89
91
|
if (block.type === "text") {
|
|
90
92
|
total += countGraphemes(block.text);
|
|
91
|
-
} else if (block.type === "thinking" && !hideThinking
|
|
92
|
-
|
|
93
|
+
} else if (block.type === "thinking" && !hideThinking) {
|
|
94
|
+
const formatted = formatThinkingForDisplay(block.thinking, proseOnly);
|
|
95
|
+
if (hasDisplayableThinking(block.thinking, formatted)) {
|
|
96
|
+
total += countGraphemes(formatted);
|
|
97
|
+
}
|
|
93
98
|
}
|
|
94
99
|
}
|
|
95
100
|
return total;
|
|
@@ -119,6 +124,7 @@ export function buildDisplayMessage(
|
|
|
119
124
|
target: AssistantMessage,
|
|
120
125
|
revealed: number,
|
|
121
126
|
hideThinking: boolean,
|
|
127
|
+
proseOnly = true,
|
|
122
128
|
countOf: (index: number, text: string) => number = (_index, text) => countGraphemes(text),
|
|
123
129
|
): AssistantMessage {
|
|
124
130
|
let remaining = Math.max(0, Math.floor(revealed));
|
|
@@ -129,10 +135,20 @@ export function buildDisplayMessage(
|
|
|
129
135
|
const units = countOf(i, block.text);
|
|
130
136
|
content.push(revealTextBlock(block, remaining, units));
|
|
131
137
|
remaining = Math.max(0, remaining - units);
|
|
132
|
-
} else if (block.type === "thinking" && !hideThinking
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
138
|
+
} else if (block.type === "thinking" && !hideThinking) {
|
|
139
|
+
const formatted = formatThinkingForDisplay(block.thinking, proseOnly);
|
|
140
|
+
if (hasDisplayableThinking(block.thinking, formatted)) {
|
|
141
|
+
const units = countOf(i, formatted);
|
|
142
|
+
const displayBlock: DisplayThinkingContentBlock = {
|
|
143
|
+
...block,
|
|
144
|
+
thinking: formatted,
|
|
145
|
+
rawThinking: block.thinking,
|
|
146
|
+
};
|
|
147
|
+
content.push(revealThinkingBlock(displayBlock, remaining, units));
|
|
148
|
+
remaining = Math.max(0, remaining - units);
|
|
149
|
+
} else {
|
|
150
|
+
content.push(block);
|
|
151
|
+
}
|
|
136
152
|
} else {
|
|
137
153
|
content.push(block);
|
|
138
154
|
}
|
|
@@ -147,12 +163,14 @@ export function nextStep(backlog: number): number {
|
|
|
147
163
|
export class StreamingRevealController {
|
|
148
164
|
readonly #getSmoothStreaming: () => boolean;
|
|
149
165
|
readonly #getHideThinkingBlock: () => boolean;
|
|
166
|
+
readonly #getProseOnlyThinking: () => boolean;
|
|
150
167
|
readonly #requestRender: () => void;
|
|
151
168
|
#target: AssistantMessage | undefined;
|
|
152
169
|
#component: StreamingRevealComponent | undefined;
|
|
153
170
|
#timer: NodeJS.Timeout | undefined;
|
|
154
171
|
#revealed = 0;
|
|
155
172
|
#hideThinkingBlock = false;
|
|
173
|
+
#proseOnlyThinking = true;
|
|
156
174
|
#smoothStreaming = true;
|
|
157
175
|
readonly #unitCounter = new BlockUnitCounter();
|
|
158
176
|
readonly #countOf = (index: number, text: string): number => this.#unitCounter.count(index, text);
|
|
@@ -160,6 +178,7 @@ export class StreamingRevealController {
|
|
|
160
178
|
constructor(options: StreamingRevealControllerOptions) {
|
|
161
179
|
this.#getSmoothStreaming = options.getSmoothStreaming;
|
|
162
180
|
this.#getHideThinkingBlock = options.getHideThinkingBlock;
|
|
181
|
+
this.#getProseOnlyThinking = options.getProseOnlyThinking;
|
|
163
182
|
this.#requestRender = options.requestRender;
|
|
164
183
|
}
|
|
165
184
|
|
|
@@ -169,9 +188,14 @@ export class StreamingRevealController {
|
|
|
169
188
|
this.#target = message;
|
|
170
189
|
this.#revealed = 0;
|
|
171
190
|
this.#hideThinkingBlock = this.#getHideThinkingBlock();
|
|
191
|
+
this.#proseOnlyThinking = this.#getProseOnlyThinking();
|
|
172
192
|
this.#smoothStreaming = this.#getSmoothStreaming();
|
|
173
193
|
if (!this.#smoothStreaming) {
|
|
174
|
-
|
|
194
|
+
const total = this.#visibleUnits(message);
|
|
195
|
+
component.updateContent(
|
|
196
|
+
buildDisplayMessage(message, total, this.#hideThinkingBlock, this.#proseOnlyThinking, this.#countOf),
|
|
197
|
+
{ transient: true },
|
|
198
|
+
);
|
|
175
199
|
return;
|
|
176
200
|
}
|
|
177
201
|
const total = this.#visibleUnits(message);
|
|
@@ -179,9 +203,18 @@ export class StreamingRevealController {
|
|
|
179
203
|
// A tool call is a transcript-order boundary: finish any leading
|
|
180
204
|
// assistant text before EventController renders the separate tool card.
|
|
181
205
|
this.#revealed = total;
|
|
182
|
-
component.updateContent(
|
|
183
|
-
|
|
184
|
-
|
|
206
|
+
component.updateContent(
|
|
207
|
+
buildDisplayMessage(
|
|
208
|
+
message,
|
|
209
|
+
this.#revealed,
|
|
210
|
+
this.#hideThinkingBlock,
|
|
211
|
+
this.#proseOnlyThinking,
|
|
212
|
+
this.#countOf,
|
|
213
|
+
),
|
|
214
|
+
{
|
|
215
|
+
transient: true,
|
|
216
|
+
},
|
|
217
|
+
);
|
|
185
218
|
return;
|
|
186
219
|
}
|
|
187
220
|
this.#renderCurrent();
|
|
@@ -190,9 +223,16 @@ export class StreamingRevealController {
|
|
|
190
223
|
|
|
191
224
|
setTarget(message: AssistantMessage): void {
|
|
192
225
|
this.#target = message;
|
|
226
|
+
this.#hideThinkingBlock = this.#getHideThinkingBlock();
|
|
227
|
+
this.#proseOnlyThinking = this.#getProseOnlyThinking();
|
|
228
|
+
this.#smoothStreaming = this.#getSmoothStreaming();
|
|
193
229
|
if (!this.#component) return;
|
|
194
230
|
if (!this.#smoothStreaming) {
|
|
195
|
-
this.#
|
|
231
|
+
const total = this.#visibleUnits(message);
|
|
232
|
+
this.#component.updateContent(
|
|
233
|
+
buildDisplayMessage(message, total, this.#hideThinkingBlock, this.#proseOnlyThinking, this.#countOf),
|
|
234
|
+
{ transient: true },
|
|
235
|
+
);
|
|
196
236
|
return;
|
|
197
237
|
}
|
|
198
238
|
const total = this.#visibleUnits(message);
|
|
@@ -202,7 +242,13 @@ export class StreamingRevealController {
|
|
|
202
242
|
this.#revealed = total;
|
|
203
243
|
this.#stopTimer();
|
|
204
244
|
this.#component.updateContent(
|
|
205
|
-
buildDisplayMessage(
|
|
245
|
+
buildDisplayMessage(
|
|
246
|
+
message,
|
|
247
|
+
this.#revealed,
|
|
248
|
+
this.#hideThinkingBlock,
|
|
249
|
+
this.#proseOnlyThinking,
|
|
250
|
+
this.#countOf,
|
|
251
|
+
),
|
|
206
252
|
{
|
|
207
253
|
transient: true,
|
|
208
254
|
},
|
|
@@ -231,8 +277,11 @@ export class StreamingRevealController {
|
|
|
231
277
|
const block = message.content[i]!;
|
|
232
278
|
if (block.type === "text") {
|
|
233
279
|
total += this.#unitCounter.count(i, block.text);
|
|
234
|
-
} else if (block.type === "thinking" && !this.#hideThinkingBlock
|
|
235
|
-
|
|
280
|
+
} else if (block.type === "thinking" && !this.#hideThinkingBlock) {
|
|
281
|
+
const formatted = formatThinkingForDisplay(block.thinking, this.#proseOnlyThinking);
|
|
282
|
+
if (hasDisplayableThinking(block.thinking, formatted)) {
|
|
283
|
+
total += this.#unitCounter.count(i, formatted);
|
|
284
|
+
}
|
|
236
285
|
}
|
|
237
286
|
}
|
|
238
287
|
return total;
|
|
@@ -244,7 +293,13 @@ export class StreamingRevealController {
|
|
|
244
293
|
// smooth reveal has temporarily caught up to the current target. The
|
|
245
294
|
// message_end handler performs the only stable non-transient render.
|
|
246
295
|
this.#component.updateContent(
|
|
247
|
-
buildDisplayMessage(
|
|
296
|
+
buildDisplayMessage(
|
|
297
|
+
this.#target,
|
|
298
|
+
this.#revealed,
|
|
299
|
+
this.#hideThinkingBlock,
|
|
300
|
+
this.#proseOnlyThinking,
|
|
301
|
+
this.#countOf,
|
|
302
|
+
),
|
|
248
303
|
{ transient: true },
|
|
249
304
|
);
|
|
250
305
|
}
|
|
@@ -284,9 +339,12 @@ export class StreamingRevealController {
|
|
|
284
339
|
return;
|
|
285
340
|
}
|
|
286
341
|
this.#revealed = Math.min(total, this.#revealed + nextStep(total - this.#revealed));
|
|
287
|
-
component.updateContent(
|
|
288
|
-
|
|
289
|
-
|
|
342
|
+
component.updateContent(
|
|
343
|
+
buildDisplayMessage(target, this.#revealed, this.#hideThinkingBlock, this.#proseOnlyThinking, this.#countOf),
|
|
344
|
+
{
|
|
345
|
+
transient: true,
|
|
346
|
+
},
|
|
347
|
+
);
|
|
290
348
|
this.#requestRender();
|
|
291
349
|
if (this.#revealed >= total) {
|
|
292
350
|
this.#stopTimer();
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
|
-
import { resolveToCwd } from "../../tools/path-utils";
|
|
3
2
|
import {
|
|
4
3
|
applyOpsToPhases,
|
|
5
4
|
getLatestTodoPhasesFromEntries,
|
|
6
5
|
markdownToPhases,
|
|
7
6
|
phasesToMarkdown,
|
|
7
|
+
resolveTodoMarkdownPath,
|
|
8
8
|
type TodoItem,
|
|
9
9
|
type TodoPhase,
|
|
10
10
|
USER_TODO_EDIT_CUSTOM_TYPE,
|
|
@@ -18,8 +18,8 @@ const USAGE = [
|
|
|
18
18
|
" /todo Show current todos",
|
|
19
19
|
" /todo edit Open todos in $EDITOR",
|
|
20
20
|
" /todo copy Copy todos as Markdown to clipboard",
|
|
21
|
-
" /todo export <path>
|
|
22
|
-
" /todo import <path>
|
|
21
|
+
" /todo export [<path>] Write todos to file (default: TODO.md)",
|
|
22
|
+
" /todo import [<path>] Replace todos from file (default: TODO.md)",
|
|
23
23
|
" /todo append [<phase>] <task...> Append a task; phase fuzzy-matched or auto-created",
|
|
24
24
|
" /todo start <task> Mark task in_progress (fuzzy content match)",
|
|
25
25
|
" /todo done [<task|phase>] Mark task/phase/all completed",
|
|
@@ -214,9 +214,7 @@ export class TodoCommandController {
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
#resolveTodoPath(rest: string): string {
|
|
217
|
-
|
|
218
|
-
const raw = trimmed || "TODO.md";
|
|
219
|
-
return resolveToCwd(raw, this.ctx.sessionManager.getCwd());
|
|
217
|
+
return resolveTodoMarkdownPath(rest, this.ctx.sessionManager.getCwd());
|
|
220
218
|
}
|
|
221
219
|
|
|
222
220
|
async #exportToFile(rest: string): Promise<void> {
|
|
@@ -225,22 +223,23 @@ export class TodoCommandController {
|
|
|
225
223
|
this.ctx.showWarning("No todos to export.");
|
|
226
224
|
return;
|
|
227
225
|
}
|
|
228
|
-
const target = this.#resolveTodoPath(rest);
|
|
229
226
|
try {
|
|
227
|
+
const target = this.#resolveTodoPath(rest);
|
|
230
228
|
await fs.writeFile(target, phasesToMarkdown(phases), "utf8");
|
|
231
229
|
this.ctx.showStatus(`Wrote todos to ${target}`);
|
|
232
230
|
} catch (error) {
|
|
233
|
-
this.ctx.showError(`Failed to write
|
|
231
|
+
this.ctx.showError(`Failed to write todos: ${error instanceof Error ? error.message : String(error)}`);
|
|
234
232
|
}
|
|
235
233
|
}
|
|
236
234
|
|
|
237
235
|
async #importFromFile(rest: string): Promise<void> {
|
|
238
|
-
|
|
236
|
+
let source = "";
|
|
239
237
|
let content: string;
|
|
240
238
|
try {
|
|
239
|
+
source = this.#resolveTodoPath(rest);
|
|
241
240
|
content = await fs.readFile(source, "utf8");
|
|
242
241
|
} catch (error) {
|
|
243
|
-
this.ctx.showError(`Failed to read
|
|
242
|
+
this.ctx.showError(`Failed to read todos: ${error instanceof Error ? error.message : String(error)}`);
|
|
244
243
|
return;
|
|
245
244
|
}
|
|
246
245
|
const { phases, errors } = markdownToPhases(content);
|