@herbertgao/pi-extensions 2026.8.1 → 2026.8.2
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/README.md +5 -2
- package/node_modules/@czottmann/pi-automode/README.md +16 -2
- package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +35 -0
- package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +18 -9
- package/node_modules/@czottmann/pi-automode/docs/defaults.md +6 -2
- package/node_modules/@czottmann/pi-automode/examples/automode.local.json +6 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +11 -7
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +100 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +32 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +78 -3
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +16 -4
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +23 -1
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +22 -1
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +18 -2
- package/node_modules/@czottmann/pi-automode/package.json +4 -4
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-help.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail-frontmatter.cjs +23 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail.mjs +99 -0
- package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +30 -0
- package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +16 -0
- package/node_modules/@dietrichgebert/ponytail/AGENTS.md +32 -0
- package/node_modules/@dietrichgebert/ponytail/LICENSE +21 -0
- package/node_modules/@dietrichgebert/ponytail/README.es.md +298 -0
- package/node_modules/@dietrichgebert/ponytail/README.ko.md +315 -0
- package/node_modules/@dietrichgebert/ponytail/README.md +351 -0
- package/node_modules/@dietrichgebert/ponytail/assets/benchmark-3model.svg +21 -0
- package/node_modules/@dietrichgebert/ponytail/assets/benchmark-agentic.svg +62 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.svg +115 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt-dark.svg +27 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt.svg +27 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/social-preview.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-es.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-ko.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +44 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/copilot-hooks.json +21 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +96 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-config.js +169 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +98 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +130 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +85 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-statusline.ps1 +24 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-statusline.sh +18 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-subagent.js +77 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/qoder-hooks.json +26 -0
- package/node_modules/@dietrichgebert/ponytail/package.json +47 -0
- package/node_modules/@dietrichgebert/ponytail/pi-extension/index.js +211 -0
- package/node_modules/@dietrichgebert/ponytail/pi-extension/package.json +8 -0
- package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +60 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +120 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +41 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +44 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +50 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +71 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +57 -0
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -3
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +10 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary.ts +6 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +24 -83
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/markdown-enhance.ts +119 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +73 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-hover.ts +130 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-interaction.ts +65 -542
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-layout.ts +160 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-packets.ts +143 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-scroll.ts +184 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool-diff/diff-renderer.ts +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool-grouping.ts +9 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/transcript-refresh.ts +91 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/agent-summary.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-dark.json +1 -0
- package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-light.json +2 -2
- package/node_modules/@herbertgao/pi-stash/README.md +9 -9
- package/node_modules/@herbertgao/pi-stash/package.json +1 -1
- package/node_modules/@herbertgao/pi-stash/src/index.ts +2 -1
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +14 -0
- package/node_modules/@herbertgao/pi-subagents/README.md +6 -2
- package/node_modules/@herbertgao/pi-subagents/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +85 -11
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +28 -4
- package/node_modules/@herbertgao/pi-subagents/src/settings.ts +12 -0
- package/node_modules/@herbertgao/pi-subagents/src/types.ts +2 -0
- package/node_modules/@narumitw/pi-btw/LICENSE +21 -0
- package/node_modules/@narumitw/pi-btw/README.md +210 -0
- package/node_modules/@narumitw/pi-btw/package.json +53 -0
- package/node_modules/@narumitw/pi-btw/src/bring-to-main.ts +632 -0
- package/node_modules/@narumitw/pi-btw/src/btw.ts +996 -0
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +339 -0
- package/node_modules/@narumitw/pi-btw/src/index.ts +1 -0
- package/node_modules/@narumitw/pi-btw/src/menu.ts +296 -0
- package/node_modules/@narumitw/pi-btw/src/settings.ts +237 -0
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +238 -0
- package/node_modules/@narumitw/pi-btw/src/text.ts +10 -0
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +620 -0
- package/node_modules/@pi-plugins/fast-mode/LICENSE +21 -0
- package/node_modules/@pi-plugins/fast-mode/README.md +92 -0
- package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts +6 -0
- package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -0
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +2026 -0
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -0
- package/node_modules/@pi-plugins/fast-mode/package.json +73 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +59 -0
- package/node_modules/pi-mcp-adapter/README.md +62 -13
- package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +377 -0
- package/node_modules/pi-mcp-adapter/commands.ts +9 -7
- package/node_modules/pi-mcp-adapter/config.ts +110 -49
- package/node_modules/pi-mcp-adapter/direct-tools.ts +34 -19
- package/node_modules/pi-mcp-adapter/elicitation-handler.ts +37 -36
- package/node_modules/pi-mcp-adapter/errors.ts +21 -17
- package/node_modules/pi-mcp-adapter/host-html-template.ts +13 -7
- package/node_modules/pi-mcp-adapter/index.ts +37 -14
- package/node_modules/pi-mcp-adapter/init.ts +9 -7
- package/node_modules/pi-mcp-adapter/json-schema-validator.ts +2 -2
- package/node_modules/pi-mcp-adapter/lifecycle.ts +8 -8
- package/node_modules/pi-mcp-adapter/logger.ts +1 -1
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +36 -22
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +120 -2
- package/node_modules/pi-mcp-adapter/mcp-code.ts +13 -11
- package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +19 -13
- package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +15 -11
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +34 -15
- package/node_modules/pi-mcp-adapter/mcp-probe.ts +131 -34
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +2 -2
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +7 -4
- package/node_modules/pi-mcp-adapter/mcp-trace.ts +59 -64
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +34 -26
- package/node_modules/pi-mcp-adapter/npx-resolver.ts +6 -4
- package/node_modules/pi-mcp-adapter/oauth-handler.ts +1 -1
- package/node_modules/pi-mcp-adapter/onboarding-state.ts +19 -11
- package/node_modules/pi-mcp-adapter/package.json +13 -8
- package/node_modules/pi-mcp-adapter/panel-keys.ts +20 -4
- package/node_modules/pi-mcp-adapter/prompts.ts +2 -2
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +48 -11
- package/node_modules/pi-mcp-adapter/sampling-handler.ts +22 -15
- package/node_modules/pi-mcp-adapter/search-ranking.ts +2 -1
- package/node_modules/pi-mcp-adapter/server-manager.ts +165 -113
- package/node_modules/pi-mcp-adapter/session-recovery.ts +11 -12
- package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +3 -3
- package/node_modules/pi-mcp-adapter/state.ts +3 -1
- package/node_modules/pi-mcp-adapter/tool-approval.ts +67 -4
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +6 -5
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +14 -2
- package/node_modules/pi-mcp-adapter/ts-shape.ts +9 -2
- package/node_modules/pi-mcp-adapter/types.ts +109 -22
- package/node_modules/pi-mcp-adapter/ui-app-bridge-helpers.ts +40 -0
- package/node_modules/pi-mcp-adapter/ui-resource-handler.ts +28 -19
- package/node_modules/pi-mcp-adapter/ui-server.ts +90 -58
- package/node_modules/pi-mcp-adapter/ui-session.ts +16 -14
- package/node_modules/pi-mcp-adapter/unix-socket-transport.ts +4 -4
- package/node_modules/pi-mcp-adapter/utils.ts +23 -9
- package/package.json +22 -7
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import type {
|
|
3
|
+
ExtensionCommandContext,
|
|
4
|
+
KeybindingsManager,
|
|
5
|
+
Theme,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import {
|
|
8
|
+
type Component,
|
|
9
|
+
type OverlayHandle,
|
|
10
|
+
type TUI,
|
|
11
|
+
TuiAltScreen,
|
|
12
|
+
truncateToWidth,
|
|
13
|
+
} from "@earendil-works/pi-tui";
|
|
14
|
+
import { sanitizeSingleLine } from "./text.js";
|
|
15
|
+
|
|
16
|
+
type BtwCustomOptions = Parameters<ExtensionCommandContext["ui"]["custom"]>[1];
|
|
17
|
+
type BtwCustomFactory<T> = (
|
|
18
|
+
tui: TUI,
|
|
19
|
+
theme: Theme,
|
|
20
|
+
keybindings: KeybindingsManager,
|
|
21
|
+
done: (result: T) => void,
|
|
22
|
+
) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>;
|
|
23
|
+
|
|
24
|
+
type BtwFullscreenTui = TUI & { flash?: (message: string, durationMs?: number) => void };
|
|
25
|
+
|
|
26
|
+
export type BtwFullscreenTuiFactory = (parent: TUI) => BtwFullscreenTui;
|
|
27
|
+
|
|
28
|
+
export interface BtwFullscreenDependencies {
|
|
29
|
+
createTui?: BtwFullscreenTuiFactory;
|
|
30
|
+
openUrl?: (url: string) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type RunBtwFullscreen = <T>(
|
|
34
|
+
ctx: ExtensionCommandContext,
|
|
35
|
+
run: (ctx: ExtensionCommandContext) => Promise<T>,
|
|
36
|
+
) => Promise<T>;
|
|
37
|
+
|
|
38
|
+
type FullscreenOutcome<T> = { kind: "completed"; value: T } | { kind: "failed"; error: unknown };
|
|
39
|
+
|
|
40
|
+
class FullscreenUiDisposedError extends Error {
|
|
41
|
+
constructor() {
|
|
42
|
+
super("The dedicated pi-btw UI was disposed.");
|
|
43
|
+
this.name = "FullscreenUiDisposedError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function runBtwFullscreen<T>(
|
|
48
|
+
ctx: ExtensionCommandContext,
|
|
49
|
+
run: (ctx: ExtensionCommandContext) => Promise<T>,
|
|
50
|
+
dependencies: BtwFullscreenDependencies = {},
|
|
51
|
+
): Promise<T> {
|
|
52
|
+
const createTui =
|
|
53
|
+
dependencies.createTui ??
|
|
54
|
+
((parent: TUI) => createBtwFullscreenTui(parent, dependencies.openUrl ?? openUrlInBrowser));
|
|
55
|
+
let liveEditorText = ctx.ui.getEditorText();
|
|
56
|
+
let restoreEditor = false;
|
|
57
|
+
const outcome = await ctx.ui.custom<FullscreenOutcome<T>>(
|
|
58
|
+
(parent, theme, keybindings, done) =>
|
|
59
|
+
new BtwFullscreenHost(
|
|
60
|
+
parent,
|
|
61
|
+
theme,
|
|
62
|
+
keybindings,
|
|
63
|
+
ctx,
|
|
64
|
+
run,
|
|
65
|
+
(value) => {
|
|
66
|
+
try {
|
|
67
|
+
liveEditorText = ctx.ui.getEditorText();
|
|
68
|
+
restoreEditor = true;
|
|
69
|
+
} catch {
|
|
70
|
+
// A replaced session owns a different editor and must not receive stale text.
|
|
71
|
+
}
|
|
72
|
+
done(value);
|
|
73
|
+
},
|
|
74
|
+
createTui,
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
if (restoreEditor) {
|
|
78
|
+
try {
|
|
79
|
+
if (ctx.ui.getEditorText() !== liveEditorText) ctx.ui.setEditorText(liveEditorText);
|
|
80
|
+
} catch {
|
|
81
|
+
// A replaced session owns a different editor and must not receive stale restoration.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (outcome.kind === "failed") throw outcome.error;
|
|
85
|
+
return outcome.value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function createBtwFullscreenTui(parent: TUI, openUrl: (url: string) => void): BtwFullscreenTui {
|
|
89
|
+
return new TuiAltScreen(parent.terminal, parent.getShowHardwareCursor(), undefined, {
|
|
90
|
+
mouse: true,
|
|
91
|
+
openUrl,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Pi does not export its browser opener, so mirror its shell-free launcher for this isolated TUI.
|
|
96
|
+
function openUrlInBrowser(target: string): void {
|
|
97
|
+
const [command, args] =
|
|
98
|
+
process.platform === "darwin"
|
|
99
|
+
? ["open", [target]]
|
|
100
|
+
: process.platform === "win32"
|
|
101
|
+
? ["rundll32", ["url.dll,FileProtocolHandler", target]]
|
|
102
|
+
: ["xdg-open", [target]];
|
|
103
|
+
spawn(command, args, { stdio: "ignore", detached: true })
|
|
104
|
+
.on("error", () => {})
|
|
105
|
+
.unref();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
class BtwFullscreenHost<T> implements Component {
|
|
109
|
+
private fullscreen: BtwFullscreenTui | undefined;
|
|
110
|
+
private cancelActiveCustom: (() => void) | undefined;
|
|
111
|
+
private started = false;
|
|
112
|
+
private disposed = false;
|
|
113
|
+
private finished = false;
|
|
114
|
+
|
|
115
|
+
constructor(
|
|
116
|
+
private readonly parent: TUI,
|
|
117
|
+
private readonly theme: Theme,
|
|
118
|
+
private readonly keybindings: KeybindingsManager,
|
|
119
|
+
private readonly ctx: ExtensionCommandContext,
|
|
120
|
+
private readonly run: (ctx: ExtensionCommandContext) => Promise<T>,
|
|
121
|
+
private readonly done: (outcome: FullscreenOutcome<T>) => void,
|
|
122
|
+
private readonly createTui: BtwFullscreenTuiFactory,
|
|
123
|
+
) {
|
|
124
|
+
queueMicrotask(() => void this.start());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
render(width: number): string[] {
|
|
128
|
+
return [truncateToWidth(this.theme.fg("muted", "Opening btw side thread…"), width)];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
invalidate(): void {}
|
|
132
|
+
|
|
133
|
+
dispose(): void {
|
|
134
|
+
if (this.disposed || this.finished) return;
|
|
135
|
+
this.disposed = true;
|
|
136
|
+
this.cancelActiveCustom?.();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private async start(): Promise<void> {
|
|
140
|
+
if (this.started || this.finished) return;
|
|
141
|
+
this.started = true;
|
|
142
|
+
let outcome: FullscreenOutcome<T>;
|
|
143
|
+
let parentStopped = false;
|
|
144
|
+
let fullscreenCreated = false;
|
|
145
|
+
try {
|
|
146
|
+
if (this.disposed) throw new FullscreenUiDisposedError();
|
|
147
|
+
this.parent.stop({ preserveScreen: true });
|
|
148
|
+
parentStopped = true;
|
|
149
|
+
if (this.disposed) throw new FullscreenUiDisposedError();
|
|
150
|
+
this.fullscreen = this.createTui(this.parent);
|
|
151
|
+
fullscreenCreated = true;
|
|
152
|
+
this.fullscreen.start();
|
|
153
|
+
outcome = { kind: "completed", value: await this.run(this.createContext()) };
|
|
154
|
+
} catch (error) {
|
|
155
|
+
outcome = { kind: "failed", error };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let cleanupError: unknown;
|
|
159
|
+
try {
|
|
160
|
+
this.cancelActiveCustom?.();
|
|
161
|
+
} catch (error) {
|
|
162
|
+
cleanupError = error;
|
|
163
|
+
}
|
|
164
|
+
if (fullscreenCreated) {
|
|
165
|
+
try {
|
|
166
|
+
this.fullscreen?.stop({ preserveScreen: true });
|
|
167
|
+
} catch (error) {
|
|
168
|
+
cleanupError ??= error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (parentStopped) {
|
|
172
|
+
try {
|
|
173
|
+
this.parent.start();
|
|
174
|
+
this.parent.renderNow(false);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
cleanupError ??= error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (cleanupError !== undefined) outcome = { kind: "failed", error: cleanupError };
|
|
180
|
+
this.finished = true;
|
|
181
|
+
this.done(outcome);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private createContext(): ExtensionCommandContext {
|
|
185
|
+
const ui = new Proxy(this.ctx.ui, {
|
|
186
|
+
get: (target, property) => {
|
|
187
|
+
if (property === "custom") {
|
|
188
|
+
return <Value>(factory: BtwCustomFactory<Value>, options?: BtwCustomOptions) =>
|
|
189
|
+
this.showCustom(factory, options);
|
|
190
|
+
}
|
|
191
|
+
if (property === "notify") {
|
|
192
|
+
return (
|
|
193
|
+
message: string,
|
|
194
|
+
level?: Parameters<ExtensionCommandContext["ui"]["notify"]>[1],
|
|
195
|
+
) => {
|
|
196
|
+
target.notify(message, level);
|
|
197
|
+
const display = sanitizeSingleLine(message);
|
|
198
|
+
if (display) this.fullscreen?.flash?.(display);
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const value = Reflect.get(target, property, target) as unknown;
|
|
202
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
return new Proxy(this.ctx, {
|
|
206
|
+
get: (target, property) => (property === "ui" ? ui : Reflect.get(target, property, target)),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private showCustom<Value>(
|
|
211
|
+
factory: BtwCustomFactory<Value>,
|
|
212
|
+
options?: BtwCustomOptions,
|
|
213
|
+
): Promise<Value> {
|
|
214
|
+
const fullscreen = this.fullscreen;
|
|
215
|
+
if (!fullscreen || this.disposed || this.finished) {
|
|
216
|
+
return Promise.reject(new FullscreenUiDisposedError());
|
|
217
|
+
}
|
|
218
|
+
if (this.cancelActiveCustom) {
|
|
219
|
+
return Promise.reject(new Error("pi-btw attempted to open overlapping custom UI."));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return new Promise<Value>((resolve, reject) => {
|
|
223
|
+
let component: (Component & { dispose?(): void }) | undefined;
|
|
224
|
+
let overlay: OverlayHandle | undefined;
|
|
225
|
+
let mounted = false;
|
|
226
|
+
let factorySettled = false;
|
|
227
|
+
let closed = false;
|
|
228
|
+
let promiseSettled = false;
|
|
229
|
+
let componentDisposed = false;
|
|
230
|
+
let pendingValue: Value | undefined;
|
|
231
|
+
let hasPendingValue = false;
|
|
232
|
+
|
|
233
|
+
const disposeComponent = () => {
|
|
234
|
+
if (!component || componentDisposed) return;
|
|
235
|
+
componentDisposed = true;
|
|
236
|
+
try {
|
|
237
|
+
component.dispose?.();
|
|
238
|
+
} catch {
|
|
239
|
+
// Cleanup must continue so terminal ownership is restored.
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
const unmount = () => {
|
|
243
|
+
let cleanupError: unknown;
|
|
244
|
+
try {
|
|
245
|
+
if (overlay) overlay.hide();
|
|
246
|
+
else if (mounted && component) fullscreen.removeChild(component);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
cleanupError = error;
|
|
249
|
+
}
|
|
250
|
+
if (overlay || mounted) {
|
|
251
|
+
try {
|
|
252
|
+
fullscreen.setFocus(null);
|
|
253
|
+
fullscreen.requestRender();
|
|
254
|
+
} catch (error) {
|
|
255
|
+
cleanupError ??= error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
disposeComponent();
|
|
259
|
+
if (cleanupError !== undefined) throw cleanupError;
|
|
260
|
+
};
|
|
261
|
+
const complete = () => {
|
|
262
|
+
if (promiseSettled || !hasPendingValue) return;
|
|
263
|
+
promiseSettled = true;
|
|
264
|
+
this.cancelActiveCustom = undefined;
|
|
265
|
+
if (!factorySettled) {
|
|
266
|
+
resolve(pendingValue as Value);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
unmount();
|
|
271
|
+
resolve(pendingValue as Value);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
reject(error);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
const close = (value: Value) => {
|
|
277
|
+
if (closed || promiseSettled) return;
|
|
278
|
+
closed = true;
|
|
279
|
+
pendingValue = value;
|
|
280
|
+
hasPendingValue = true;
|
|
281
|
+
complete();
|
|
282
|
+
};
|
|
283
|
+
const fail = (error: unknown) => {
|
|
284
|
+
if (promiseSettled) return;
|
|
285
|
+
closed = true;
|
|
286
|
+
promiseSettled = true;
|
|
287
|
+
this.cancelActiveCustom = undefined;
|
|
288
|
+
try {
|
|
289
|
+
unmount();
|
|
290
|
+
reject(error);
|
|
291
|
+
} catch (cleanupError) {
|
|
292
|
+
reject(cleanupError);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
this.cancelActiveCustom = () => {
|
|
296
|
+
if (promiseSettled) return;
|
|
297
|
+
disposeComponent();
|
|
298
|
+
if (!promiseSettled) fail(new FullscreenUiDisposedError());
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
let created: ReturnType<BtwCustomFactory<Value>>;
|
|
302
|
+
try {
|
|
303
|
+
created = factory(fullscreen, this.theme, this.keybindings, close);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
factorySettled = true;
|
|
306
|
+
fail(error);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
Promise.resolve(created)
|
|
310
|
+
.then((value) => {
|
|
311
|
+
component = value;
|
|
312
|
+
factorySettled = true;
|
|
313
|
+
if (promiseSettled) {
|
|
314
|
+
disposeComponent();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (closed) {
|
|
318
|
+
complete();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (options?.overlay) {
|
|
322
|
+
const overlayOptions =
|
|
323
|
+
typeof options.overlayOptions === "function"
|
|
324
|
+
? options.overlayOptions()
|
|
325
|
+
: options.overlayOptions;
|
|
326
|
+
overlay = fullscreen.showOverlay(component, overlayOptions);
|
|
327
|
+
options.onHandle?.(overlay);
|
|
328
|
+
} else {
|
|
329
|
+
fullscreen.clear();
|
|
330
|
+
fullscreen.addChild(component);
|
|
331
|
+
mounted = true;
|
|
332
|
+
fullscreen.setFocus(component);
|
|
333
|
+
fullscreen.requestRender();
|
|
334
|
+
}
|
|
335
|
+
})
|
|
336
|
+
.catch(fail);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./btw.js";
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionCommandContext,
|
|
3
|
+
KeybindingsManager,
|
|
4
|
+
Theme,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
7
|
+
import type { MenuContext, RunMenuResult } from "@narumitw/pi-tui-kit";
|
|
8
|
+
import {
|
|
9
|
+
type BtwSettings,
|
|
10
|
+
btwSettingsPath,
|
|
11
|
+
effectiveRememberThinkingLevelChanges,
|
|
12
|
+
readBtwSettings,
|
|
13
|
+
type UpdateBtwSettingsOptions,
|
|
14
|
+
updateBtwSettings,
|
|
15
|
+
} from "./settings.js";
|
|
16
|
+
import { BTW_THINKING_LEVELS, type BtwThinkingLevel } from "./side-thread.js";
|
|
17
|
+
import { sanitizeSingleLine } from "./text.js";
|
|
18
|
+
|
|
19
|
+
interface BtwMenuState {
|
|
20
|
+
kind: "valid" | "invalid";
|
|
21
|
+
settings: BtwSettings;
|
|
22
|
+
reason?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface BtwResumeThreadSummary {
|
|
26
|
+
id: string;
|
|
27
|
+
title: string;
|
|
28
|
+
questionCount: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ShowBtwCommandMenuOptions {
|
|
32
|
+
currentThinkingLevel: BtwThinkingLevel;
|
|
33
|
+
availableThinkingLevels: readonly BtwThinkingLevel[];
|
|
34
|
+
resumeThreads?: readonly BtwResumeThreadSummary[];
|
|
35
|
+
settingsPath?: string;
|
|
36
|
+
readSettings?: typeof readBtwSettings;
|
|
37
|
+
updateSettings?: (
|
|
38
|
+
patch: Partial<Pick<BtwSettings, "thinkingLevel" | "rememberThinkingLevelChanges">>,
|
|
39
|
+
options: UpdateBtwSettingsOptions,
|
|
40
|
+
) => Promise<BtwSettings>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type BtwCommandMenuResult = "start" | "closed" | { kind: "resume"; threadId: string };
|
|
44
|
+
|
|
45
|
+
type BtwMenuScreen = "main" | "resume" | "settings" | "invalid";
|
|
46
|
+
type BtwMenuAction = "start" | "resume" | "set-thinking" | "set-remember";
|
|
47
|
+
type BtwCustomOptions = Parameters<ExtensionCommandContext["ui"]["custom"]>[1];
|
|
48
|
+
|
|
49
|
+
type BtwCustomFactory<T> = (
|
|
50
|
+
tui: TUI,
|
|
51
|
+
theme: Theme,
|
|
52
|
+
keybindings: KeybindingsManager,
|
|
53
|
+
done: (result: T) => void,
|
|
54
|
+
) => Component;
|
|
55
|
+
|
|
56
|
+
export async function showBtwCommandMenu(
|
|
57
|
+
ctx: ExtensionCommandContext,
|
|
58
|
+
options: ShowBtwCommandMenuOptions,
|
|
59
|
+
): Promise<BtwCommandMenuResult> {
|
|
60
|
+
if (ctx.mode !== "tui") return "closed";
|
|
61
|
+
const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
|
|
62
|
+
if (ctx.signal?.aborted) return "closed";
|
|
63
|
+
const settingsPath = options.settingsPath ?? btwSettingsPath();
|
|
64
|
+
const readSettings = options.readSettings ?? readBtwSettings;
|
|
65
|
+
const updateSettings = options.updateSettings ?? updateBtwSettings;
|
|
66
|
+
const levels =
|
|
67
|
+
options.availableThinkingLevels.length > 0
|
|
68
|
+
? [...options.availableThinkingLevels]
|
|
69
|
+
: (["off"] satisfies BtwThinkingLevel[]);
|
|
70
|
+
const displaySettingsPath = sanitizeSingleLine(settingsPath);
|
|
71
|
+
const resumeThreads = options.resumeThreads ?? [];
|
|
72
|
+
let startSelected = false;
|
|
73
|
+
let resumedThreadId: string | undefined;
|
|
74
|
+
|
|
75
|
+
const loadState = async (): Promise<BtwMenuState> => {
|
|
76
|
+
const loaded = await readSettings(settingsPath);
|
|
77
|
+
if (loaded.kind === "invalid") {
|
|
78
|
+
return { kind: "invalid", settings: {}, reason: loaded.reason };
|
|
79
|
+
}
|
|
80
|
+
return { kind: "valid", settings: loaded.kind === "loaded" ? loaded.settings : {} };
|
|
81
|
+
};
|
|
82
|
+
const displayThinkingLevel = (settings: BtwSettings): BtwThinkingLevel =>
|
|
83
|
+
clampToAvailableThinkingLevel(settings.thinkingLevel ?? options.currentThinkingLevel, levels);
|
|
84
|
+
|
|
85
|
+
const menu = defineMenu<BtwMenuState, BtwMenuScreen, BtwMenuAction, MenuContext>({
|
|
86
|
+
start: "main",
|
|
87
|
+
screens: {
|
|
88
|
+
main: ({ state }) => ({
|
|
89
|
+
kind: "actions",
|
|
90
|
+
title: "Pi BTW",
|
|
91
|
+
lines: [
|
|
92
|
+
`Thinking: ${displayThinkingLevel(state.settings)} · Remember changes: ${effectiveRememberThinkingLevelChanges(state.settings) ? "On" : "Off"}`,
|
|
93
|
+
],
|
|
94
|
+
items: [
|
|
95
|
+
{
|
|
96
|
+
id: "start",
|
|
97
|
+
label: "Start side thread",
|
|
98
|
+
description: "Open an empty side thread",
|
|
99
|
+
action: "start",
|
|
100
|
+
},
|
|
101
|
+
...(resumeThreads.length > 0
|
|
102
|
+
? [
|
|
103
|
+
{
|
|
104
|
+
id: "resume" as const,
|
|
105
|
+
label: "Resume side thread",
|
|
106
|
+
description: "Continue an in-memory side thread",
|
|
107
|
+
to: "resume" as const,
|
|
108
|
+
},
|
|
109
|
+
]
|
|
110
|
+
: []),
|
|
111
|
+
{
|
|
112
|
+
id: "settings",
|
|
113
|
+
label: "Settings",
|
|
114
|
+
description: "Choose pi-btw thinking and whether shortcut changes are remembered",
|
|
115
|
+
to: state.kind === "invalid" ? "invalid" : "settings",
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
hint: "close",
|
|
119
|
+
}),
|
|
120
|
+
resume: () => ({
|
|
121
|
+
kind: "choice",
|
|
122
|
+
title: "Resume BTW side thread",
|
|
123
|
+
items: resumeThreads.map((thread) => ({
|
|
124
|
+
id: thread.id,
|
|
125
|
+
label: thread.title,
|
|
126
|
+
description: `${thread.questionCount} ${thread.questionCount === 1 ? "question" : "questions"}`,
|
|
127
|
+
})),
|
|
128
|
+
action: "resume",
|
|
129
|
+
viewportSize: 10,
|
|
130
|
+
hint: "back",
|
|
131
|
+
}),
|
|
132
|
+
settings: ({ state }) => ({
|
|
133
|
+
kind: "settings",
|
|
134
|
+
title: "Pi BTW Settings",
|
|
135
|
+
lines: [`User settings · ${displaySettingsPath}`],
|
|
136
|
+
items: [
|
|
137
|
+
{
|
|
138
|
+
id: "thinkingLevel",
|
|
139
|
+
label: "Thinking level",
|
|
140
|
+
description: "Set the starting level for future pi-btw side threads.",
|
|
141
|
+
currentValue: displayThinkingLevel(state.settings),
|
|
142
|
+
values: levels,
|
|
143
|
+
action: "set-thinking",
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "rememberThinkingLevelChanges",
|
|
147
|
+
label: "Remember thinking level changes",
|
|
148
|
+
description: "Save side-thread shortcut changes to pi-btw.json for next time.",
|
|
149
|
+
currentValue: effectiveRememberThinkingLevelChanges(state.settings) ? "On" : "Off",
|
|
150
|
+
values: ["On", "Off"],
|
|
151
|
+
action: "set-remember",
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
}),
|
|
155
|
+
invalid: ({ state }) => ({
|
|
156
|
+
kind: "detail",
|
|
157
|
+
title: "Pi BTW Settings · Read only",
|
|
158
|
+
lines: [
|
|
159
|
+
`Invalid settings file. Fix ${displaySettingsPath} before saving.`,
|
|
160
|
+
sanitizeSingleLine(state.reason ?? "The settings file is invalid."),
|
|
161
|
+
],
|
|
162
|
+
hint: "back",
|
|
163
|
+
}),
|
|
164
|
+
},
|
|
165
|
+
actions: {
|
|
166
|
+
start: async () => {
|
|
167
|
+
startSelected = true;
|
|
168
|
+
return { kind: "close" };
|
|
169
|
+
},
|
|
170
|
+
resume: async ({ itemId }: { itemId: string }) => {
|
|
171
|
+
if (!resumeThreads.some((thread) => thread.id === itemId)) {
|
|
172
|
+
return { kind: "rejected" } as const;
|
|
173
|
+
}
|
|
174
|
+
resumedThreadId = itemId;
|
|
175
|
+
return { kind: "close" } as const;
|
|
176
|
+
},
|
|
177
|
+
"set-thinking": async ({ value, signal }) => {
|
|
178
|
+
if (!value || !levels.includes(value as BtwThinkingLevel)) return { kind: "rejected" };
|
|
179
|
+
try {
|
|
180
|
+
await updateSettings(
|
|
181
|
+
{ thinkingLevel: value as BtwThinkingLevel },
|
|
182
|
+
{ settingsPath, signal },
|
|
183
|
+
);
|
|
184
|
+
if (signal.aborted) return { kind: "rejected" };
|
|
185
|
+
notifySafely(ctx, `Pi BTW thinking level: ${value}.`, "info");
|
|
186
|
+
return { kind: "stay" };
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (!signal.aborted) notifySaveFailure(ctx, error);
|
|
189
|
+
return { kind: "rejected" };
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
"set-remember": async ({ value, signal }) => {
|
|
193
|
+
if (value !== "On" && value !== "Off") return { kind: "rejected" };
|
|
194
|
+
try {
|
|
195
|
+
await updateSettings(
|
|
196
|
+
{ rememberThinkingLevelChanges: value === "On" },
|
|
197
|
+
{ settingsPath, signal },
|
|
198
|
+
);
|
|
199
|
+
if (signal.aborted) return { kind: "rejected" };
|
|
200
|
+
notifySafely(ctx, `Remember thinking level changes: ${value}.`, "info");
|
|
201
|
+
return { kind: "stay" };
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (!signal.aborted) notifySaveFailure(ctx, error);
|
|
204
|
+
return { kind: "rejected" };
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const result = await runBtwMenuPreservingEditor(ctx, (menuContext) =>
|
|
211
|
+
runMenu(menuContext, menu, { getState: loadState }),
|
|
212
|
+
);
|
|
213
|
+
if (result.kind !== "closed" || result.reason !== "close") return "closed";
|
|
214
|
+
if (resumedThreadId) return { kind: "resume", threadId: resumedThreadId };
|
|
215
|
+
return startSelected ? "start" : "closed";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function runBtwMenuPreservingEditor(
|
|
219
|
+
ctx: ExtensionCommandContext,
|
|
220
|
+
run: (menuContext: MenuContext) => Promise<RunMenuResult>,
|
|
221
|
+
): Promise<RunMenuResult> {
|
|
222
|
+
let liveEditorText = ctx.ui.getEditorText();
|
|
223
|
+
let completed = false;
|
|
224
|
+
const ui = new Proxy(ctx.ui, {
|
|
225
|
+
get(target, property) {
|
|
226
|
+
if (property === "custom") {
|
|
227
|
+
return <Value>(factory: BtwCustomFactory<Value>, customOptions?: BtwCustomOptions) =>
|
|
228
|
+
target.custom<Value>(
|
|
229
|
+
(tui, theme, keybindings, done) =>
|
|
230
|
+
factory(tui, theme, keybindings, (value) => {
|
|
231
|
+
try {
|
|
232
|
+
liveEditorText = target.getEditorText();
|
|
233
|
+
} catch {
|
|
234
|
+
// Keep completion finite if session replacement invalidates the editor context.
|
|
235
|
+
}
|
|
236
|
+
completed = true;
|
|
237
|
+
done(value);
|
|
238
|
+
}),
|
|
239
|
+
customOptions,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
const value = Reflect.get(target, property, target) as unknown;
|
|
243
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
const result = await run({ mode: ctx.mode, hasUI: ctx.hasUI, ui });
|
|
247
|
+
if (result.kind !== "stale" && completed) {
|
|
248
|
+
try {
|
|
249
|
+
if (ctx.ui.getEditorText() !== liveEditorText) ctx.ui.setEditorText(liveEditorText);
|
|
250
|
+
} catch {
|
|
251
|
+
// A replaced context owns a different editor and must not receive stale restoration.
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function clampToAvailableThinkingLevel(
|
|
258
|
+
requested: BtwThinkingLevel,
|
|
259
|
+
available: readonly BtwThinkingLevel[],
|
|
260
|
+
): BtwThinkingLevel {
|
|
261
|
+
if (available.includes(requested)) return requested;
|
|
262
|
+
const requestedIndex = BTW_THINKING_LEVELS.indexOf(requested);
|
|
263
|
+
for (let index = requestedIndex; index < BTW_THINKING_LEVELS.length; index += 1) {
|
|
264
|
+
const candidate = BTW_THINKING_LEVELS[index];
|
|
265
|
+
if (candidate && available.includes(candidate)) return candidate;
|
|
266
|
+
}
|
|
267
|
+
for (let index = requestedIndex - 1; index >= 0; index -= 1) {
|
|
268
|
+
const candidate = BTW_THINKING_LEVELS[index];
|
|
269
|
+
if (candidate && available.includes(candidate)) return candidate;
|
|
270
|
+
}
|
|
271
|
+
return available[0] ?? "off";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function notifySaveFailure(ctx: ExtensionCommandContext, error: unknown): void {
|
|
275
|
+
notifySafely(
|
|
276
|
+
ctx,
|
|
277
|
+
`Pi BTW settings were not saved; the previous value remains active: ${formatError(error)}`,
|
|
278
|
+
"error",
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function notifySafely(
|
|
283
|
+
ctx: ExtensionCommandContext,
|
|
284
|
+
message: string,
|
|
285
|
+
level: Parameters<ExtensionCommandContext["ui"]["notify"]>[1],
|
|
286
|
+
): void {
|
|
287
|
+
try {
|
|
288
|
+
ctx.ui.notify(sanitizeSingleLine(message), level);
|
|
289
|
+
} catch {
|
|
290
|
+
// A completed save remains valid if its command context was replaced before notification.
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function formatError(error: unknown): string {
|
|
295
|
+
return error instanceof Error ? error.message : String(error);
|
|
296
|
+
}
|