@oh-my-pi/pi-coding-agent 17.0.7 → 17.0.9
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 +91 -0
- package/dist/cli.js +5025 -4930
- package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +28 -1
- package/dist/types/config/model-resolver.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +50 -3
- package/dist/types/dap/client.d.ts +19 -0
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +1 -1
- package/dist/types/extensibility/typebox.d.ts +4 -0
- package/dist/types/hindsight/client.d.ts +10 -0
- package/dist/types/hindsight/config.d.ts +8 -0
- package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +6 -2
- package/dist/types/mcp/render.d.ts +2 -2
- package/dist/types/mcp/tool-bridge.d.ts +16 -3
- package/dist/types/mcp/types.d.ts +6 -0
- package/dist/types/mnemopi/state.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +8 -0
- package/dist/types/modes/components/settings-defs.d.ts +1 -0
- package/dist/types/modes/components/user-message.d.ts +25 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +8 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
- package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
- package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
- package/dist/types/modes/types.d.ts +14 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +12 -8
- package/dist/types/sdk.d.ts +1 -0
- package/dist/types/session/agent-session-error-log.test.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +86 -3
- package/dist/types/session/session-loader.d.ts +6 -4
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +1 -0
- package/dist/types/task/prewalk.d.ts +3 -0
- package/dist/types/task/types.d.ts +14 -0
- package/dist/types/tools/ask.d.ts +10 -0
- package/dist/types/tools/hub/jobs.d.ts +1 -1
- package/dist/types/tools/index.d.ts +3 -0
- package/dist/types/tools/report-tool-issue.d.ts +24 -9
- package/dist/types/tools/tool-timeouts.d.ts +8 -2
- package/dist/types/utils/changelog.d.ts +4 -6
- package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +13 -13
- package/scripts/legacy-pi-virtual-module.ts +1 -1
- package/src/cli/grievances-cli.ts +2 -2
- package/src/cli/models-cli.ts +37 -17
- package/src/cli/update-cli.ts +14 -1
- package/src/cli/usage-cli.ts +1 -1
- package/src/config/__tests__/model-registry.test.ts +147 -0
- package/src/config/model-registry.ts +87 -24
- package/src/config/model-resolver.ts +56 -32
- package/src/config/settings-schema.ts +46 -3
- package/src/config/settings.ts +68 -5
- package/src/dap/client.ts +86 -7
- package/src/edit/diff.ts +4 -4
- package/src/eval/py/prelude.py +5 -5
- package/src/export/html/template.js +23 -9
- package/src/extensibility/extensions/runner.ts +9 -4
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
- package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +851 -310
- package/src/extensibility/typebox.ts +22 -9
- package/src/hindsight/client.test.ts +42 -0
- package/src/hindsight/client.ts +40 -3
- package/src/hindsight/config.ts +18 -0
- package/src/internal-urls/registry-helpers.ts +40 -0
- package/src/launch/broker.ts +31 -5
- package/src/lsp/client.ts +23 -0
- package/src/lsp/clients/biome-client.ts +3 -4
- package/src/lsp/index.ts +26 -8
- package/src/main.ts +16 -5
- package/src/mcp/manager.ts +39 -8
- package/src/mcp/render.ts +94 -35
- package/src/mcp/tool-bridge.ts +116 -11
- package/src/mcp/types.ts +7 -0
- package/src/mnemopi/state.ts +70 -3
- package/src/modes/components/agent-dashboard.ts +6 -2
- package/src/modes/components/agent-hub.ts +26 -9
- package/src/modes/components/chat-transcript-builder.ts +13 -2
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/oauth-selector.ts +24 -7
- package/src/modes/components/plan-review-overlay.ts +20 -4
- package/src/modes/components/settings-defs.ts +5 -2
- package/src/modes/components/settings-selector.ts +9 -5
- package/src/modes/components/tool-execution.test.ts +63 -2
- package/src/modes/components/user-message.ts +100 -1
- package/src/modes/controllers/command-controller.ts +36 -12
- package/src/modes/controllers/event-controller.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +18 -0
- package/src/modes/controllers/input-controller.ts +47 -12
- package/src/modes/controllers/mcp-command-controller.ts +70 -40
- package/src/modes/controllers/selector-controller.ts +82 -6
- package/src/modes/interactive-mode.ts +39 -4
- package/src/modes/rpc/rpc-client.ts +94 -3
- package/src/modes/rpc/rpc-frame.ts +164 -4
- package/src/modes/rpc/rpc-messages.ts +127 -0
- package/src/modes/rpc/rpc-mode.ts +79 -7
- package/src/modes/rpc/rpc-types.ts +34 -2
- package/src/modes/setup-wizard/scenes/model.ts +5 -7
- package/src/modes/setup-wizard/scenes/providers.ts +3 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
- package/src/modes/setup-wizard/scenes/theme.ts +13 -3
- package/src/modes/setup-wizard/scenes/types.ts +9 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +6 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/types.ts +16 -3
- package/src/modes/utils/ui-helpers.ts +40 -20
- package/src/prompts/goals/guided-goal-system.md +24 -3
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/task.md +12 -2
- package/src/sdk.ts +179 -51
- package/src/session/agent-session-error-log.test.ts +59 -0
- package/src/session/agent-session.ts +351 -46
- package/src/session/session-context.test.ts +224 -1
- package/src/session/session-context.ts +41 -2
- package/src/session/session-loader.ts +10 -5
- package/src/slash-commands/helpers/usage-report.ts +3 -1
- package/src/system-prompt.ts +1 -2
- package/src/task/executor.ts +44 -27
- package/src/task/index.ts +63 -8
- package/src/task/prewalk.ts +6 -0
- package/src/task/structured-subagent.ts +3 -1
- package/src/task/types.ts +16 -0
- package/src/task/worktree.ts +25 -11
- package/src/tools/ask.ts +15 -0
- package/src/tools/bash.ts +14 -6
- package/src/tools/browser.ts +1 -1
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval.ts +4 -5
- package/src/tools/fetch.ts +1 -1
- package/src/tools/hub/index.ts +1 -1
- package/src/tools/hub/jobs.ts +67 -8
- package/src/tools/hub/launch.ts +7 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/report-tool-issue.ts +79 -28
- package/src/tools/tool-timeouts.ts +10 -3
- package/src/tools/write.ts +31 -0
- package/src/utils/changelog.ts +54 -58
- package/src/utils/git.ts +57 -49
- package/src/web/search/providers/firecrawl.ts +46 -13
- package/src/web/search/types.ts +5 -1
|
@@ -76,26 +76,43 @@ function formatModelBadge(modelId: string, level: ThinkingLevel | undefined): st
|
|
|
76
76
|
return `${model} ${theme.getThinkingBorderColor(level)(display)}`;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/** Format a resolved selector, preserving provider identity when requested. */
|
|
80
|
+
function formatResolvedModelBadge(resolved: string, preserveProvider = false): string {
|
|
81
|
+
// Model ids may themselves contain colons (`qwen3:14b`), so only treat the
|
|
82
|
+
// suffix as a thinking level when it parses as one.
|
|
83
|
+
const colon = resolved.lastIndexOf(":");
|
|
84
|
+
const level = colon >= 0 ? parseThinkingLevel(resolved.slice(colon + 1)) : undefined;
|
|
85
|
+
const selector = level !== undefined ? resolved.slice(0, colon) : resolved;
|
|
86
|
+
const label = preserveProvider ? selector : selector.slice(selector.indexOf("/") + 1);
|
|
87
|
+
return formatModelBadge(label, level);
|
|
88
|
+
}
|
|
89
|
+
|
|
79
90
|
/**
|
|
80
91
|
* Active model + reasoning level for a hub row: live session state when the
|
|
81
92
|
* agent is attached, else the executor-reported `resolvedModel` selector
|
|
82
|
-
* (`provider/id`, optionally `:<level>`).
|
|
93
|
+
* (`provider/id`, optionally `:<level>`). Active retry fallbacks retain their
|
|
94
|
+
* provider and carry an explicit marker. Undefined when no model is known
|
|
83
95
|
* (e.g. a parked historical agent restored from disk).
|
|
84
96
|
*/
|
|
85
97
|
function modelBadge(ref: AgentRef, observed: ObservableSession | undefined): string | undefined {
|
|
98
|
+
const progress = observed?.progress;
|
|
99
|
+
// Prefer the live session's own resolved fallback selector; else honor the
|
|
100
|
+
// executor-reported fallback flag. The latter covers observer-only rows (no
|
|
101
|
+
// live session) AND live rows whose fallback armed no session retry state —
|
|
102
|
+
// e.g. the Fireworks Fast → base degrade, which emits `retry_fallback_applied`
|
|
103
|
+
// without populating `#activeRetryFallback`, so `retryFallbackModel` is undefined.
|
|
104
|
+
const fallbackSelector =
|
|
105
|
+
ref.session?.retryFallbackModel ?? (progress?.resolvedModelIsFallback ? progress.resolvedModel : undefined);
|
|
106
|
+
if (fallbackSelector) {
|
|
107
|
+
return `${theme.fg("warning", "fallback →")} ${formatResolvedModelBadge(fallbackSelector, true)}`;
|
|
108
|
+
}
|
|
86
109
|
const model = ref.session?.model;
|
|
87
110
|
if (model) {
|
|
88
111
|
const level = model.thinking ? ref.session?.thinkingLevel : undefined;
|
|
89
112
|
return formatModelBadge(model.id, level);
|
|
90
113
|
}
|
|
91
|
-
const resolved =
|
|
92
|
-
|
|
93
|
-
// Model ids may themselves contain colons (`qwen3:14b`), so only treat the
|
|
94
|
-
// suffix as a thinking level when it parses as one.
|
|
95
|
-
const colon = resolved.lastIndexOf(":");
|
|
96
|
-
const level = colon >= 0 ? parseThinkingLevel(resolved.slice(colon + 1)) : undefined;
|
|
97
|
-
const selector = level !== undefined ? resolved.slice(0, colon) : resolved;
|
|
98
|
-
return formatModelBadge(selector.slice(selector.indexOf("/") + 1), level);
|
|
114
|
+
const resolved = progress?.resolvedModel;
|
|
115
|
+
return resolved ? formatResolvedModelBadge(resolved) : undefined;
|
|
99
116
|
}
|
|
100
117
|
|
|
101
118
|
/** Result of one host-backed transcript read for the Agent Hub viewer. */
|
|
@@ -56,7 +56,7 @@ import { SkillMessageComponent } from "./skill-message";
|
|
|
56
56
|
import { ToolExecutionComponent } from "./tool-execution";
|
|
57
57
|
import { TranscriptContainer } from "./transcript-container";
|
|
58
58
|
import { createUsageRowBlock } from "./usage-row";
|
|
59
|
-
import { UserMessageComponent } from "./user-message";
|
|
59
|
+
import { CollapsedSyntheticMessageComponent, UserMessageComponent } from "./user-message";
|
|
60
60
|
|
|
61
61
|
export interface ChatTranscriptBuilderDeps {
|
|
62
62
|
ui: TUI;
|
|
@@ -233,7 +233,18 @@ export class ChatTranscriptBuilder {
|
|
|
233
233
|
const textContent = message.role === "user" ? userMessageText(message) : "";
|
|
234
234
|
if (textContent) {
|
|
235
235
|
const isSynthetic = message.role === "developer" ? true : (message.synthetic ?? false);
|
|
236
|
-
|
|
236
|
+
// Synthetic (agent-attributed) inputs — chiefly the advisor's `Session
|
|
237
|
+
// update` replay dumps — can be hundreds of KiB of Markdown each.
|
|
238
|
+
// Rendering their full body on cold open blocked the TUI (issue #6308);
|
|
239
|
+
// collapse them behind a compact summary that builds Markdown only on
|
|
240
|
+
// ctrl+o expand. Real user prompts stay fully rendered.
|
|
241
|
+
if (isSynthetic) {
|
|
242
|
+
const collapsed = new CollapsedSyntheticMessageComponent(textContent);
|
|
243
|
+
this.#trackExpandable(collapsed);
|
|
244
|
+
this.container.addChild(collapsed);
|
|
245
|
+
} else {
|
|
246
|
+
this.container.addChild(new UserMessageComponent(textContent, false));
|
|
247
|
+
}
|
|
237
248
|
}
|
|
238
249
|
break;
|
|
239
250
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { diffWords } from "@oh-my-pi/pi-natives";
|
|
1
2
|
import { DEFAULT_TAB_WIDTH, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
2
|
-
import * as Diff from "diff";
|
|
3
3
|
import { getLanguageFromPath, highlightCode, theme } from "../../modes/theme/theme";
|
|
4
4
|
import { type CodeFrameMarker, formatCodeFrameLine, replaceTabs } from "../../tools/render-utils";
|
|
5
5
|
|
|
@@ -53,7 +53,7 @@ function parseDiffLine(line: string): { prefix: CodeFrameMarker; lineNum: string
|
|
|
53
53
|
* Strips leading whitespace from inverse to avoid highlighting indentation.
|
|
54
54
|
*/
|
|
55
55
|
function renderIntraLineDiff(oldContent: string, newContent: string): { removedLine: string; addedLine: string } {
|
|
56
|
-
const wordDiff =
|
|
56
|
+
const wordDiff = diffWords(oldContent, newContent);
|
|
57
57
|
|
|
58
58
|
let removedLine = "";
|
|
59
59
|
let addedLine = "";
|
|
@@ -60,6 +60,8 @@ export class OAuthSelectorComponent extends Container {
|
|
|
60
60
|
/** First provider index of the visible ScrollView window (last #updateList). */
|
|
61
61
|
#scrollStart = 0;
|
|
62
62
|
#visibleCount = 0;
|
|
63
|
+
/** Visible list window, shrunk by {@link setMaxHeight} on short screens. */
|
|
64
|
+
#maxVisible = OAUTH_SELECTOR_MAX_VISIBLE;
|
|
63
65
|
#mode: "login" | "logout";
|
|
64
66
|
#authStorage: AuthStorage;
|
|
65
67
|
#onSelectCallback: (providerId: string) => void;
|
|
@@ -111,6 +113,24 @@ export class OAuthSelectorComponent extends Container {
|
|
|
111
113
|
this.#validationGeneration += 1;
|
|
112
114
|
this.#stopSpinner();
|
|
113
115
|
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Fit the selector into `lines` rendered rows by shrinking the visible list
|
|
119
|
+
* window (the window is centered on the selection, so the selected row is
|
|
120
|
+
* always visible at any height). Prefers keeping the full chrome — borders,
|
|
121
|
+
* spacers, title, search status — but sacrifices the trailing spacer/border
|
|
122
|
+
* (clipped by the host) before dropping below three visible rows.
|
|
123
|
+
*/
|
|
124
|
+
setMaxHeight(lines: number): void {
|
|
125
|
+
// Above the rows: LIST_ROW_OFFSET; below: search status + spacer + border.
|
|
126
|
+
const strict = lines - LIST_ROW_OFFSET - 3;
|
|
127
|
+
// Keeps only the rows + search status inside `lines`.
|
|
128
|
+
const relaxed = lines - LIST_ROW_OFFSET - 1;
|
|
129
|
+
const rows = Math.min(OAUTH_SELECTOR_MAX_VISIBLE, Math.max(1, strict, Math.min(relaxed, 3)));
|
|
130
|
+
if (rows === this.#maxVisible) return;
|
|
131
|
+
this.#maxVisible = rows;
|
|
132
|
+
this.#updateList();
|
|
133
|
+
}
|
|
114
134
|
#hasSelectableAuth(providerId: string): boolean {
|
|
115
135
|
return this.#mode === "logout" ? this.#authStorage.has(providerId) : this.#authStorage.hasAuth(providerId);
|
|
116
136
|
}
|
|
@@ -227,7 +247,7 @@ export class OAuthSelectorComponent extends Container {
|
|
|
227
247
|
}
|
|
228
248
|
|
|
229
249
|
#isSearchEnabled(): boolean {
|
|
230
|
-
return this.#allProviders.length >
|
|
250
|
+
return this.#allProviders.length > this.#maxVisible;
|
|
231
251
|
}
|
|
232
252
|
|
|
233
253
|
#shouldRenderSearchStatus(): boolean {
|
|
@@ -286,7 +306,7 @@ export class OAuthSelectorComponent extends Container {
|
|
|
286
306
|
this.#listContainer.clear();
|
|
287
307
|
|
|
288
308
|
const total = this.#filteredProviders.length;
|
|
289
|
-
const maxVisible =
|
|
309
|
+
const maxVisible = this.#maxVisible;
|
|
290
310
|
const startIndex =
|
|
291
311
|
total <= maxVisible
|
|
292
312
|
? 0
|
|
@@ -381,7 +401,7 @@ export class OAuthSelectorComponent extends Container {
|
|
|
381
401
|
// Page up - jump up by one visible page
|
|
382
402
|
else if (matchesKey(keyData, "pageUp")) {
|
|
383
403
|
if (this.#filteredProviders.length > 0) {
|
|
384
|
-
this.#selectedIndex = Math.max(0, this.#selectedIndex -
|
|
404
|
+
this.#selectedIndex = Math.max(0, this.#selectedIndex - this.#maxVisible);
|
|
385
405
|
}
|
|
386
406
|
this.#statusMessage = undefined;
|
|
387
407
|
this.#updateList();
|
|
@@ -389,10 +409,7 @@ export class OAuthSelectorComponent extends Container {
|
|
|
389
409
|
// Page down - jump down by one visible page
|
|
390
410
|
else if (matchesKey(keyData, "pageDown")) {
|
|
391
411
|
if (this.#filteredProviders.length > 0) {
|
|
392
|
-
this.#selectedIndex = Math.min(
|
|
393
|
-
this.#filteredProviders.length - 1,
|
|
394
|
-
this.#selectedIndex + OAUTH_SELECTOR_MAX_VISIBLE,
|
|
395
|
-
);
|
|
412
|
+
this.#selectedIndex = Math.min(this.#filteredProviders.length - 1, this.#selectedIndex + this.#maxVisible);
|
|
396
413
|
}
|
|
397
414
|
this.#statusMessage = undefined;
|
|
398
415
|
this.#updateList();
|
|
@@ -152,6 +152,14 @@ export class PlanReviewOverlay implements Component {
|
|
|
152
152
|
* motion mouse reports and cleared when the pointer leaves the option rows. */
|
|
153
153
|
#hoveredOption: number | undefined;
|
|
154
154
|
|
|
155
|
+
// Once a choice fires, the promise-based caller resolves but keeps this
|
|
156
|
+
// overlay mounted while it runs slow async approval work (e.g. context
|
|
157
|
+
// compaction). Lock input and switch to a "submitting" indicator so the
|
|
158
|
+
// overlay stops looking interactive and repeat Enter/Esc are not silently
|
|
159
|
+
// swallowed with zero feedback (#5926).
|
|
160
|
+
#committed = false;
|
|
161
|
+
/** Label of the committed choice, shown while the async approval settles. */
|
|
162
|
+
#committedLabel: string | undefined;
|
|
155
163
|
#annotating = false;
|
|
156
164
|
#input: Input;
|
|
157
165
|
|
|
@@ -283,11 +291,14 @@ export class PlanReviewOverlay implements Component {
|
|
|
283
291
|
#confirmSelection(): void {
|
|
284
292
|
const index = this.#selectedIndex;
|
|
285
293
|
if (index >= 0 && index < this.#options.length && !this.#disabled.has(index)) {
|
|
294
|
+
this.#committed = true;
|
|
295
|
+
this.#committedLabel = this.#options[index]!;
|
|
286
296
|
this.callbacks.onPick(this.#options[index]!);
|
|
287
297
|
}
|
|
288
298
|
}
|
|
289
299
|
|
|
290
300
|
handleInput(keyData: string): void {
|
|
301
|
+
if (this.#committed) return;
|
|
291
302
|
if (keyData.startsWith("\x1b[<") && this.#handleMouse(keyData)) return;
|
|
292
303
|
if (this.#annotating) {
|
|
293
304
|
if (this.callbacks.onAnnotationExternalEditor && matchesAppExternalEditor(keyData)) {
|
|
@@ -300,6 +311,7 @@ export class PlanReviewOverlay implements Component {
|
|
|
300
311
|
return;
|
|
301
312
|
}
|
|
302
313
|
if (matchesSelectCancel(keyData)) {
|
|
314
|
+
this.#committed = true;
|
|
303
315
|
this.callbacks.onCancel();
|
|
304
316
|
return;
|
|
305
317
|
}
|
|
@@ -838,10 +850,14 @@ export class PlanReviewOverlay implements Component {
|
|
|
838
850
|
const innerWidth = Math.max(1, width - 4);
|
|
839
851
|
const bodyContentWidth = sidebarShown ? splitBodyWidth(width, sidebarWidth) : innerWidth;
|
|
840
852
|
|
|
841
|
-
const
|
|
842
|
-
const
|
|
853
|
+
const committed = this.#committed;
|
|
854
|
+
const sliderLines = committed ? [] : this.#renderSliderLines();
|
|
855
|
+
const submittingLabel = this.#committedLabel ? `${this.#committedLabel} — submitting…` : "Submitting…";
|
|
856
|
+
const optionLines = committed ? [theme.bold(theme.fg("accent", submittingLabel))] : this.#renderOptionLines();
|
|
843
857
|
const promptLines = this.#promptTitle ? [theme.bold(theme.fg("accent", this.#promptTitle))] : [];
|
|
844
|
-
const footerLines =
|
|
858
|
+
const footerLines = committed
|
|
859
|
+
? [theme.fg("dim", "Applying your selection — this can take a moment while context is prepared.")]
|
|
860
|
+
: this.#renderFooterLines(innerWidth);
|
|
845
861
|
|
|
846
862
|
// Chrome rows: top border, two dividers, bottom border, plus the
|
|
847
863
|
// prompt/slider/option/footer rows between them.
|
|
@@ -884,7 +900,7 @@ export class PlanReviewOverlay implements Component {
|
|
|
884
900
|
for (const line of promptLines) out.push(row(line, width));
|
|
885
901
|
for (const line of sliderLines) out.push(row(line, width));
|
|
886
902
|
for (let i = 0; i < optionLines.length; i++) {
|
|
887
|
-
this.#optionClickRows.set(out.length, i);
|
|
903
|
+
if (!committed) this.#optionClickRows.set(out.length, i);
|
|
888
904
|
out.push(row(optionLines[i]!, width));
|
|
889
905
|
}
|
|
890
906
|
out.push(divider(width));
|
|
@@ -66,6 +66,7 @@ export interface SubmenuSettingDef extends BaseSettingDef {
|
|
|
66
66
|
|
|
67
67
|
export interface TextInputSettingDef extends BaseSettingDef {
|
|
68
68
|
type: "text";
|
|
69
|
+
secret: boolean;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
export interface ProviderLimitsSettingDef extends BaseSettingDef {
|
|
@@ -176,11 +177,13 @@ function pathToSettingDef(path: SettingPath): SettingDef | null {
|
|
|
176
177
|
if (options) {
|
|
177
178
|
return { ...base, type: "submenu", options };
|
|
178
179
|
}
|
|
179
|
-
return { ...base, type: "text" };
|
|
180
|
+
return { ...base, type: "text", secret: ui.secret === true };
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
if (schemaType === "record") {
|
|
183
|
-
return path === "providers.maxInFlightRequests"
|
|
184
|
+
return path === "providers.maxInFlightRequests"
|
|
185
|
+
? { ...base, type: "providerLimits" }
|
|
186
|
+
: { ...base, type: "text", secret: false };
|
|
184
187
|
}
|
|
185
188
|
|
|
186
189
|
return null;
|
|
@@ -65,6 +65,7 @@ class TextInputSubmenu extends Container {
|
|
|
65
65
|
label: string,
|
|
66
66
|
description: string,
|
|
67
67
|
currentValue: string,
|
|
68
|
+
secret: boolean,
|
|
68
69
|
private readonly onSubmit: (value: string) => void,
|
|
69
70
|
private readonly onCancel: () => void,
|
|
70
71
|
) {
|
|
@@ -78,6 +79,7 @@ class TextInputSubmenu extends Container {
|
|
|
78
79
|
this.addChild(new Spacer(1));
|
|
79
80
|
|
|
80
81
|
this.#input = new Input();
|
|
82
|
+
this.#input.mask = secret;
|
|
81
83
|
if (currentValue) {
|
|
82
84
|
this.#input.setValue(currentValue);
|
|
83
85
|
}
|
|
@@ -293,6 +295,7 @@ class ProviderLimitsSubmenu extends Container {
|
|
|
293
295
|
`Max In-Flight Requests: ${provider}`,
|
|
294
296
|
"Enter a positive number. Decimals round down. Clear the field to make this provider unlimited.",
|
|
295
297
|
limits[provider]?.toString() ?? "",
|
|
298
|
+
false,
|
|
296
299
|
value => {
|
|
297
300
|
const next = { ...limits };
|
|
298
301
|
const trimmed = value.trim();
|
|
@@ -837,7 +840,7 @@ export class SettingsSelectorComponent implements Component {
|
|
|
837
840
|
id: def.path,
|
|
838
841
|
label: def.label,
|
|
839
842
|
description: def.description,
|
|
840
|
-
currentValue: this.#formatTextInputValue(def
|
|
843
|
+
currentValue: this.#formatTextInputValue(def, currentValue),
|
|
841
844
|
submenu: (cv, done) => this.#createTextInput(def, cv, done),
|
|
842
845
|
changed,
|
|
843
846
|
};
|
|
@@ -992,12 +995,13 @@ export class SettingsSelectorComponent implements Component {
|
|
|
992
995
|
def.label,
|
|
993
996
|
def.description,
|
|
994
997
|
this.#formatTextInputEditValue(def.path, settings.get(def.path)),
|
|
998
|
+
def.secret,
|
|
995
999
|
value => {
|
|
996
1000
|
// Empty string clears the setting; undefined-typed string settings
|
|
997
1001
|
// store "" which the browser.ts expandPath ignores (no-op fallback).
|
|
998
1002
|
this.#setSettingValue(def.path, value);
|
|
999
1003
|
this.callbacks.onChange(def.path, settings.get(def.path));
|
|
1000
|
-
wrappedDone(this.#formatTextInputValue(def
|
|
1004
|
+
wrappedDone(this.#formatTextInputValue(def, settings.get(def.path)));
|
|
1001
1005
|
},
|
|
1002
1006
|
() => wrappedDone(),
|
|
1003
1007
|
);
|
|
@@ -1022,9 +1026,9 @@ export class SettingsSelectorComponent implements Component {
|
|
|
1022
1026
|
return entries.map(([provider, limit]) => `${provider}: ${limit}`).join(", ");
|
|
1023
1027
|
}
|
|
1024
1028
|
|
|
1025
|
-
#formatTextInputValue(
|
|
1026
|
-
if (
|
|
1027
|
-
return this.#formatTextInputEditValue(path, value);
|
|
1029
|
+
#formatTextInputValue(def: SettingDef & { type: "text" }, value: unknown): string {
|
|
1030
|
+
if (def.secret) return value ? "••••••••" : "";
|
|
1031
|
+
return this.#formatTextInputEditValue(def.path, value);
|
|
1028
1032
|
}
|
|
1029
1033
|
|
|
1030
1034
|
#formatTextInputEditValue(_path: SettingPath, value: unknown): string {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { beforeAll, describe, expect, it } from "bun:test";
|
|
1
|
+
import { afterEach, beforeAll, describe, expect, it } from "bun:test";
|
|
2
2
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import { type Component, Text } from "@oh-my-pi/pi-tui";
|
|
4
|
-
import { Settings } from "../../config/settings";
|
|
4
|
+
import { Settings, settings } from "../../config/settings";
|
|
5
|
+
import { renderMCPResult } from "../../mcp/render";
|
|
6
|
+
import type { MCPToolDetails } from "../../mcp/tool-bridge";
|
|
5
7
|
import { getThemeByName, setThemeInstance, theme } from "../theme/theme";
|
|
6
8
|
import { ToolExecutionComponent, type ToolExecutionUi } from "./tool-execution";
|
|
7
9
|
|
|
@@ -26,6 +28,10 @@ describe("ToolExecutionComponent custom renderer failures", () => {
|
|
|
26
28
|
setThemeInstance(loaded);
|
|
27
29
|
});
|
|
28
30
|
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
settings.set("mcp.renderMarkdownResults", true);
|
|
33
|
+
});
|
|
34
|
+
|
|
29
35
|
it("falls back to the custom tool label when a renderCall child component throws during render", () => {
|
|
30
36
|
const tool: AgentTool = {
|
|
31
37
|
name: "graphify_graph",
|
|
@@ -99,3 +105,58 @@ describe("ToolExecutionComponent custom renderer failures", () => {
|
|
|
99
105
|
expect(text).toContain(rawResultText);
|
|
100
106
|
});
|
|
101
107
|
});
|
|
108
|
+
|
|
109
|
+
describe("MCP result Markdown rendering", () => {
|
|
110
|
+
const details: MCPToolDetails = {
|
|
111
|
+
serverName: "context-mode",
|
|
112
|
+
mcpToolName: "ctx_search",
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
beforeAll(async () => {
|
|
116
|
+
await Settings.init({ inMemory: true });
|
|
117
|
+
const loaded = await getThemeByName("dark");
|
|
118
|
+
if (!loaded) throw new Error("theme unavailable");
|
|
119
|
+
setThemeInstance(loaded);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
afterEach(() => {
|
|
123
|
+
settings.set("mcp.renderMarkdownResults", true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("renders inline Markdown by default", () => {
|
|
127
|
+
const component = renderMCPResult(
|
|
128
|
+
{ content: [{ type: "text", text: "**bold result** and `code`" }], details },
|
|
129
|
+
{ expanded: true, isPartial: false },
|
|
130
|
+
theme,
|
|
131
|
+
);
|
|
132
|
+
const rendered = visibleText(component.render(80));
|
|
133
|
+
|
|
134
|
+
expect(rendered).toContain("bold result and code");
|
|
135
|
+
expect(rendered).not.toContain("**bold result**");
|
|
136
|
+
expect(rendered).not.toContain("`code`");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("keeps Markdown syntax literal when the setting is disabled", () => {
|
|
140
|
+
settings.set("mcp.renderMarkdownResults", false);
|
|
141
|
+
const component = renderMCPResult(
|
|
142
|
+
{ content: [{ type: "text", text: "**bold result**" }], details },
|
|
143
|
+
{ expanded: true, isPartial: false },
|
|
144
|
+
theme,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
expect(visibleText(component.render(80))).toContain("**bold result**");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("preserves structured JSON rendering when Markdown is enabled", () => {
|
|
151
|
+
settings.set("mcp.renderMarkdownResults", true);
|
|
152
|
+
const component = renderMCPResult(
|
|
153
|
+
{ content: [{ type: "text", text: '{"status":"**ok**"}' }], details },
|
|
154
|
+
{ expanded: true, isPartial: false },
|
|
155
|
+
theme,
|
|
156
|
+
);
|
|
157
|
+
const rendered = visibleText(component.render(80));
|
|
158
|
+
|
|
159
|
+
expect(rendered).toContain("status");
|
|
160
|
+
expect(rendered).toContain("**ok**");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Container, Markdown } from "@oh-my-pi/pi-tui";
|
|
1
|
+
import { type Component, Container, Markdown } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { formatBytes } from "@oh-my-pi/pi-utils";
|
|
2
3
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
3
4
|
import { imageReferenceHyperlink, renderPlaceholders } from "../image-references";
|
|
4
5
|
import { highlightMagicKeywords } from "../magic-keywords";
|
|
@@ -66,3 +67,101 @@ export class UserMessageComponent extends Container {
|
|
|
66
67
|
return wrapped;
|
|
67
68
|
}
|
|
68
69
|
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Collapsed placeholder for a synthetic (agent-attributed) user input in the
|
|
73
|
+
* file/remote-backed transcript viewer — chiefly the advisor's `Session update`
|
|
74
|
+
* replay dumps, which can each be hundreds of KiB of Markdown and, on cold open,
|
|
75
|
+
* blocked the TUI for tens of seconds while every historical body was laid out
|
|
76
|
+
* before the viewport clip (issue #6308).
|
|
77
|
+
*
|
|
78
|
+
* Collapsed by default: renders one dim summary row (label · size · line count ·
|
|
79
|
+
* expand hint) and builds NO Markdown. The heavy {@link UserMessageComponent} is
|
|
80
|
+
* constructed lazily only when expanded via `ctrl+o`, so blocks above the
|
|
81
|
+
* viewport never pay layout cost until the reader asks to see them. The raw
|
|
82
|
+
* observability data stays intact in `__advisor.jsonl`.
|
|
83
|
+
*/
|
|
84
|
+
export class CollapsedSyntheticMessageComponent implements Component {
|
|
85
|
+
#expanded = false;
|
|
86
|
+
#cache?: { width: number; lines: readonly string[] };
|
|
87
|
+
#body?: UserMessageComponent;
|
|
88
|
+
readonly #summary: string;
|
|
89
|
+
|
|
90
|
+
constructor(
|
|
91
|
+
private readonly text: string,
|
|
92
|
+
private readonly imageLinks?: readonly (string | undefined)[],
|
|
93
|
+
) {
|
|
94
|
+
this.#summary = summarizeSyntheticInput(text);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** ctrl+o toggle: reveal/hide the full Markdown body. */
|
|
98
|
+
setExpanded(expanded: boolean): void {
|
|
99
|
+
if (this.#expanded === expanded) return;
|
|
100
|
+
this.#expanded = expanded;
|
|
101
|
+
this.#cache = undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
invalidate(): void {
|
|
105
|
+
this.#cache = undefined;
|
|
106
|
+
this.#body?.invalidate?.();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
dispose(): void {
|
|
110
|
+
this.#body?.dispose?.();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
render(width: number): readonly string[] {
|
|
114
|
+
width = Math.max(1, width);
|
|
115
|
+
if (this.#cache?.width === width) return this.#cache.lines;
|
|
116
|
+
const lines = this.#expanded ? this.#renderExpanded(width) : [` ${this.#summaryRow(width)}`];
|
|
117
|
+
this.#cache = { width, lines };
|
|
118
|
+
return lines;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#renderExpanded(width: number): readonly string[] {
|
|
122
|
+
if (!this.#body) this.#body = new UserMessageComponent(this.text, true, this.imageLinks);
|
|
123
|
+
return [` ${this.#summaryRow(width)}`, ...this.#body.render(width)];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#summaryRow(width: number): string {
|
|
127
|
+
const hint = `${theme.sep.dot.trim()} ctrl+o`;
|
|
128
|
+
return theme.fg("dim", truncateSummary(`${this.#summary} ${hint}`, Math.max(10, width - 1)));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Truncate a plain summary label to `maxWidth` display columns, appending `…`. */
|
|
133
|
+
function truncateSummary(text: string, maxWidth: number): string {
|
|
134
|
+
if (Bun.stringWidth(text, { countAnsiEscapeCodes: false }) <= maxWidth) return text;
|
|
135
|
+
let out = "";
|
|
136
|
+
let w = 0;
|
|
137
|
+
for (const ch of text) {
|
|
138
|
+
const cw = Bun.stringWidth(ch, { countAnsiEscapeCodes: false });
|
|
139
|
+
if (w + cw > maxWidth - 1) break;
|
|
140
|
+
out += ch;
|
|
141
|
+
w += cw;
|
|
142
|
+
}
|
|
143
|
+
return `${out}…`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* One-line summary for a collapsed synthetic input: `<label> · <size> · <n>
|
|
148
|
+
* lines`. The label is the first Markdown heading's text (e.g. `Session
|
|
149
|
+
* update`), falling back to `Synthetic input` when the body opens with none.
|
|
150
|
+
*/
|
|
151
|
+
function summarizeSyntheticInput(text: string): string {
|
|
152
|
+
const size = formatBytes(Buffer.byteLength(text, "utf-8"));
|
|
153
|
+
const lineCount = text === "" ? 0 : text.split("\n").length;
|
|
154
|
+
const dot = theme.sep.dot.trim();
|
|
155
|
+
return `${syntheticInputLabel(text)} ${dot} ${size} ${dot} ${lineCount} line${lineCount === 1 ? "" : "s"}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** First Markdown heading text in `text`, else `Synthetic input`. */
|
|
159
|
+
function syntheticInputLabel(text: string): string {
|
|
160
|
+
for (const raw of text.split("\n")) {
|
|
161
|
+
const line = raw.trim();
|
|
162
|
+
if (!line) continue;
|
|
163
|
+
const heading = /^#{1,6}\s+(.*)$/.exec(line);
|
|
164
|
+
return heading ? heading[1]!.trim() || "Synthetic input" : "Synthetic input";
|
|
165
|
+
}
|
|
166
|
+
return "Synthetic input";
|
|
167
|
+
}
|
|
@@ -1311,7 +1311,7 @@ export class CommandController {
|
|
|
1311
1311
|
|
|
1312
1312
|
compactingLoader.stop();
|
|
1313
1313
|
this.ctx.statusContainer.disposeChildren();
|
|
1314
|
-
this.ctx.rebuildChatFromMessages();
|
|
1314
|
+
this.ctx.rebuildChatFromMessages({ reuseSettledComponents: true });
|
|
1315
1315
|
|
|
1316
1316
|
this.ctx.statusLine.invalidate();
|
|
1317
1317
|
// Same as the auto-compaction rebuild: a collapsed transcript is an
|
|
@@ -1636,17 +1636,24 @@ function formatAggregateAmount(limits: UsageLimit[]): string {
|
|
|
1636
1636
|
}
|
|
1637
1637
|
|
|
1638
1638
|
function resolveResetRange(limits: UsageLimit[], nowMs: number): string | null {
|
|
1639
|
-
const
|
|
1640
|
-
.map(limit => limit.window
|
|
1641
|
-
.filter(
|
|
1642
|
-
|
|
1643
|
-
|
|
1639
|
+
const windows = limits
|
|
1640
|
+
.map(limit => limit.window)
|
|
1641
|
+
.filter(
|
|
1642
|
+
(window): window is NonNullable<UsageLimit["window"]> =>
|
|
1643
|
+
window?.resetsAt !== undefined && Number.isFinite(window.resetsAt) && window.resetsAt > nowMs,
|
|
1644
|
+
);
|
|
1645
|
+
if (windows.length === 0) return null;
|
|
1646
|
+
// Use the shared verb when every contributing window agrees (e.g. all "tick");
|
|
1647
|
+
// mixed or absent labels fall back to the generic "resets".
|
|
1648
|
+
const labels = new Set(windows.map(window => window.resetLabel ?? "resets"));
|
|
1649
|
+
const verb = labels.size === 1 ? [...labels][0]! : "resets";
|
|
1650
|
+
const offsets = windows.map(window => window.resetsAt! - nowMs);
|
|
1644
1651
|
const minReset = Math.min(...offsets);
|
|
1645
1652
|
const maxReset = Math.max(...offsets);
|
|
1646
1653
|
if (maxReset - minReset > 60_000) {
|
|
1647
|
-
return
|
|
1654
|
+
return `${verb} in ${formatDuration(minReset)}–${formatDuration(maxReset)}`;
|
|
1648
1655
|
}
|
|
1649
|
-
return
|
|
1656
|
+
return `${verb} in ${formatDuration(minReset)}`;
|
|
1650
1657
|
}
|
|
1651
1658
|
/**
|
|
1652
1659
|
* Compact one-line quota summary for a single advisor's provider.
|
|
@@ -1857,17 +1864,34 @@ export function renderUsageReports(
|
|
|
1857
1864
|
for (const line of resetAccountLines) lines.push(uiTheme.fg("dim", line));
|
|
1858
1865
|
}
|
|
1859
1866
|
|
|
1867
|
+
// Order account columns ONCE per provider (worst-first), then apply that
|
|
1868
|
+
// same order to every window group. Sorting each group independently by
|
|
1869
|
+
// its own used fraction (issue #6067) desynchronized the columns: an
|
|
1870
|
+
// account exhausted on its 5h window but light on the weekly window would
|
|
1871
|
+
// land in different column positions on each row, so the positional
|
|
1872
|
+
// `account N` labels denoted different credentials per row and an
|
|
1873
|
+
// exhausted limit appeared under a sibling that still had quota.
|
|
1874
|
+
const accountRank = new Map<UsageReport, number>();
|
|
1875
|
+
providerReports.forEach((report, position) => {
|
|
1876
|
+
const worst = report.limits.reduce((max, limit) => {
|
|
1877
|
+
const fraction = resolveUsedFraction(limit) ?? -1;
|
|
1878
|
+
return fraction > max ? fraction : max;
|
|
1879
|
+
}, -1);
|
|
1880
|
+
// Encode worst-first primary key with the stable position as tiebreak
|
|
1881
|
+
// so accounts tied on pressure keep their discovery order.
|
|
1882
|
+
accountRank.set(report, -worst * 1000 + position);
|
|
1883
|
+
});
|
|
1884
|
+
|
|
1860
1885
|
const renderableGroups = Array.from(limitGroups.values()).map(group => {
|
|
1861
1886
|
const entries = group.limits.map((limit, index) => ({
|
|
1862
1887
|
limit,
|
|
1863
1888
|
report: group.reports[index],
|
|
1864
|
-
fraction: resolveUsedFraction(limit),
|
|
1865
1889
|
index,
|
|
1866
1890
|
}));
|
|
1867
1891
|
entries.sort((a, b) => {
|
|
1868
|
-
const
|
|
1869
|
-
const
|
|
1870
|
-
if (
|
|
1892
|
+
const aRank = accountRank.get(a.report) ?? a.index;
|
|
1893
|
+
const bRank = accountRank.get(b.report) ?? b.index;
|
|
1894
|
+
if (aRank !== bRank) return aRank - bRank;
|
|
1871
1895
|
return a.index - b.index;
|
|
1872
1896
|
});
|
|
1873
1897
|
const sortedLimits = entries.map(entry => entry.limit);
|
|
@@ -920,6 +920,9 @@ export class EventController {
|
|
|
920
920
|
),
|
|
921
921
|
);
|
|
922
922
|
}
|
|
923
|
+
if (displayMessage === event.message) {
|
|
924
|
+
this.ctx.transcriptMessageComponents.set(event.message, this.ctx.streamingComponent);
|
|
925
|
+
}
|
|
923
926
|
this.ctx.streamingComponent = undefined;
|
|
924
927
|
this.ctx.streamingMessage = undefined;
|
|
925
928
|
// Pin a turn-ending provider error (e.g. Anthropic content-filter block)
|
|
@@ -1316,7 +1319,7 @@ export class EventController {
|
|
|
1316
1319
|
}
|
|
1317
1320
|
} else if (event.result) {
|
|
1318
1321
|
this.ctx.lastAssistantUsage = undefined;
|
|
1319
|
-
this.ctx.rebuildChatFromMessages();
|
|
1322
|
+
this.ctx.rebuildChatFromMessages({ reuseSettledComponents: true });
|
|
1320
1323
|
this.ctx.statusLine.invalidate();
|
|
1321
1324
|
// When history collapses behind the summary divider, the frame
|
|
1322
1325
|
// shrinks far below the committed row count; without clearing, the
|
|
@@ -70,6 +70,12 @@ export class ExtensionUiController {
|
|
|
70
70
|
// the rest queue. See `#presentDialog`.
|
|
71
71
|
#dialogActive = false;
|
|
72
72
|
#dialogQueue: Array<() => void> = [];
|
|
73
|
+
/**
|
|
74
|
+
* Built once in `initHooksAndCustomTools()`. Reused directly by `/tree`
|
|
75
|
+
* `ask` re-answer (issue #5642) to drive a standalone `AskTool.execute()`
|
|
76
|
+
* call with the same picker/dialog primitives a live tool call would get.
|
|
77
|
+
*/
|
|
78
|
+
#toolUIContext: ExtensionUIContext | undefined;
|
|
73
79
|
constructor(private ctx: InteractiveModeContext) {}
|
|
74
80
|
|
|
75
81
|
/**
|
|
@@ -121,6 +127,7 @@ export class ExtensionUiController {
|
|
|
121
127
|
setToolsExpanded: expanded => this.ctx.setToolsExpanded(expanded),
|
|
122
128
|
};
|
|
123
129
|
this.ctx.setToolUIContext(uiContext, true);
|
|
130
|
+
this.#toolUIContext = uiContext;
|
|
124
131
|
|
|
125
132
|
const extensionRunner = this.ctx.session.extensionRunner;
|
|
126
133
|
if (!extensionRunner) {
|
|
@@ -275,6 +282,17 @@ export class ExtensionUiController {
|
|
|
275
282
|
});
|
|
276
283
|
}
|
|
277
284
|
|
|
285
|
+
/**
|
|
286
|
+
* The `ExtensionUIContext` built in `initHooksAndCustomTools()` — the same
|
|
287
|
+
* picker/dialog primitives passed as `context.ui` for every live tool
|
|
288
|
+
* call. `/tree` `ask` re-answer (issue #5642) reuses this to drive a
|
|
289
|
+
* standalone `AskTool.execute()` call outside a normal agent turn.
|
|
290
|
+
* `undefined` before hooks have initialized.
|
|
291
|
+
*/
|
|
292
|
+
getToolUIContext(): ExtensionUIContext | undefined {
|
|
293
|
+
return this.#toolUIContext;
|
|
294
|
+
}
|
|
295
|
+
|
|
278
296
|
setHookWidget(key: string, content: ExtensionWidgetContent, options?: ExtensionWidgetOptions): void {
|
|
279
297
|
const placement = options?.placement ?? "aboveEditor";
|
|
280
298
|
this.#removeHookWidget(this.#hookWidgetsAbove, key);
|