pi-ui-extend 0.1.4 → 0.1.6
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/dist/app/app.d.ts +3 -0
- package/dist/app/app.js +31 -0
- package/dist/app/clipboard.d.ts +1 -0
- package/dist/app/clipboard.js +16 -0
- package/dist/app/icons.d.ts +2 -0
- package/dist/app/icons.js +4 -0
- package/dist/app/mouse-controller.d.ts +4 -1
- package/dist/app/mouse-controller.js +12 -0
- package/dist/app/render-controller.js +1 -0
- package/dist/app/startup-info.d.ts +1 -1
- package/dist/app/startup-info.js +3 -2
- package/dist/app/status-line-renderer.d.ts +5 -1
- package/dist/app/status-line-renderer.js +23 -2
- package/dist/app/terminal-bell-sound-controller.d.ts +11 -0
- package/dist/app/terminal-bell-sound-controller.js +58 -0
- package/dist/app/types.d.ts +10 -0
- package/dist/app/update.d.ts +2 -0
- package/dist/app/update.js +16 -0
- package/extensions/terminal-bell/index.ts +43 -13
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +8 -0
- package/external/pi-tools-suite/src/terminal-bell/index.ts +43 -13
- package/package.json +1 -1
package/dist/app/app.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export declare class PiUiExtendApp {
|
|
|
26
26
|
private readonly subagentsWidgetController;
|
|
27
27
|
private readonly todoWidgetController;
|
|
28
28
|
private readonly terminalController;
|
|
29
|
+
private readonly terminalBellSoundController;
|
|
29
30
|
private readonly toastController;
|
|
30
31
|
private readonly nerdFontController;
|
|
31
32
|
private readonly popupActions;
|
|
@@ -56,6 +57,7 @@ export declare class PiUiExtendApp {
|
|
|
56
57
|
private resumeLoading;
|
|
57
58
|
constructor(options: AppOptions);
|
|
58
59
|
start(): Promise<void>;
|
|
60
|
+
private checkPixUpdateOnStartup;
|
|
59
61
|
private bindCurrentSession;
|
|
60
62
|
private activateRuntime;
|
|
61
63
|
private createExtensionEventBus;
|
|
@@ -85,6 +87,7 @@ export declare class PiUiExtendApp {
|
|
|
85
87
|
private toolDefaultExpanded;
|
|
86
88
|
private stopBlinking;
|
|
87
89
|
private stop;
|
|
90
|
+
private toggleTerminalBellSound;
|
|
88
91
|
private refreshModelUsageStatusFromClick;
|
|
89
92
|
private showToast;
|
|
90
93
|
private clearToastTimers;
|
package/dist/app/app.js
CHANGED
|
@@ -37,7 +37,9 @@ import { AppTodoWidgetController } from "./todo-widget-controller.js";
|
|
|
37
37
|
import { AppTabsController } from "./tabs-controller.js";
|
|
38
38
|
import { TabLineRenderer } from "./tab-line-renderer.js";
|
|
39
39
|
import { AppTerminalController } from "./terminal-controller.js";
|
|
40
|
+
import { TerminalBellSoundController } from "./terminal-bell-sound-controller.js";
|
|
40
41
|
import { AppToastController } from "./toast-controller.js";
|
|
42
|
+
import { checkPixUpdate, formatPixStartupUpdateDialog } from "./update.js";
|
|
41
43
|
import { AppVoiceController } from "./voice-controller.js";
|
|
42
44
|
import { createIsolatedExtensionEventBus } from "./extension-event-bus.js";
|
|
43
45
|
import { setAppIconTheme } from "./icons.js";
|
|
@@ -71,6 +73,7 @@ export class PiUiExtendApp {
|
|
|
71
73
|
subagentsWidgetController;
|
|
72
74
|
todoWidgetController;
|
|
73
75
|
terminalController;
|
|
76
|
+
terminalBellSoundController;
|
|
74
77
|
toastController;
|
|
75
78
|
nerdFontController;
|
|
76
79
|
popupActions;
|
|
@@ -173,6 +176,7 @@ export class PiUiExtendApp {
|
|
|
173
176
|
});
|
|
174
177
|
this.pixConfig = loadPixConfig();
|
|
175
178
|
setAppIconTheme(this.pixConfig.iconTheme.name);
|
|
179
|
+
this.terminalBellSoundController = new TerminalBellSoundController();
|
|
176
180
|
this.promptEnhancer = new AppPromptEnhancerController({
|
|
177
181
|
runtime: () => this.runtime,
|
|
178
182
|
inputEditor: () => this.inputEditor,
|
|
@@ -243,6 +247,8 @@ export class PiUiExtendApp {
|
|
|
243
247
|
promptEnhancerStatusWidgetText: () => this.promptEnhancer.statusWidgetText(),
|
|
244
248
|
promptEnhancerStatusWidgetActive: () => this.promptEnhancer.statusWidgetActive(),
|
|
245
249
|
promptEnhancerStatusWidgetEnabled: () => this.promptEnhancer.statusWidgetEnabled(),
|
|
250
|
+
terminalBellSoundStatusWidgetText: () => this.terminalBellSoundController.statusWidgetText(),
|
|
251
|
+
terminalBellSoundStatusWidgetEnabled: () => this.terminalBellSoundController.isEnabled(),
|
|
246
252
|
voiceStatusWidgetText: () => this.voiceController.statusWidgetText(),
|
|
247
253
|
voiceStatusWidgetActive: () => this.voiceController.statusWidgetActive(),
|
|
248
254
|
userMessageJumpMenuActive: () => this.popupMenus.directMenu === "user-message-jump",
|
|
@@ -501,6 +507,7 @@ export class PiUiExtendApp {
|
|
|
501
507
|
this.superCompactTools = !this.superCompactTools;
|
|
502
508
|
this.render();
|
|
503
509
|
},
|
|
510
|
+
toggleTerminalBellSound: () => this.toggleTerminalBellSound(),
|
|
504
511
|
handleExtensionInputMouse: (event) => this.extensionUiController.handleCustomUiMouse(event),
|
|
505
512
|
render: () => this.render(),
|
|
506
513
|
}, this.popupMenus, this.popupActions, this.scrollController, this.commandController);
|
|
@@ -656,6 +663,7 @@ export class PiUiExtendApp {
|
|
|
656
663
|
this.mouseController.statusUserJumpTarget = undefined;
|
|
657
664
|
this.mouseController.statusThinkingExpandTarget = undefined;
|
|
658
665
|
this.mouseController.statusCompactToolsTarget = undefined;
|
|
666
|
+
this.mouseController.statusTerminalBellSoundTarget = undefined;
|
|
659
667
|
this.mouseController.statusSessionTarget = undefined;
|
|
660
668
|
this.mouseController.statusPromptEnhancerTarget = undefined;
|
|
661
669
|
this.mouseController.statusVoiceMicTarget = undefined;
|
|
@@ -674,6 +682,18 @@ export class PiUiExtendApp {
|
|
|
674
682
|
await this.sessionLifecycle.start();
|
|
675
683
|
this.modelUsageController.startPolling();
|
|
676
684
|
this.nerdFontController.ensureInstalledOnStartup();
|
|
685
|
+
void this.checkPixUpdateOnStartup();
|
|
686
|
+
}
|
|
687
|
+
async checkPixUpdateOnStartup() {
|
|
688
|
+
try {
|
|
689
|
+
const result = await checkPixUpdate();
|
|
690
|
+
if (result.status !== "newer")
|
|
691
|
+
return;
|
|
692
|
+
this.showToast(formatPixStartupUpdateDialog(result), "warning", { variant: "dialog" });
|
|
693
|
+
}
|
|
694
|
+
catch {
|
|
695
|
+
// Startup update checks should never interrupt the TUI.
|
|
696
|
+
}
|
|
677
697
|
}
|
|
678
698
|
async bindCurrentSession() {
|
|
679
699
|
await this.sessionLifecycle.bindCurrentSession();
|
|
@@ -816,6 +836,17 @@ export class PiUiExtendApp {
|
|
|
816
836
|
async stop() {
|
|
817
837
|
await this.terminalController.stop();
|
|
818
838
|
}
|
|
839
|
+
toggleTerminalBellSound() {
|
|
840
|
+
try {
|
|
841
|
+
const enabled = this.terminalBellSoundController.toggle();
|
|
842
|
+
this.showToast(enabled ? "Terminal bell notifications enabled" : "Terminal bell notifications muted", "info");
|
|
843
|
+
this.render();
|
|
844
|
+
}
|
|
845
|
+
catch (error) {
|
|
846
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
847
|
+
this.showToast(`Could not update terminal bell notifications: ${message}`, "error");
|
|
848
|
+
}
|
|
849
|
+
}
|
|
819
850
|
refreshModelUsageStatusFromClick() {
|
|
820
851
|
const refresh = this.modelUsageController.refreshNow();
|
|
821
852
|
if (refresh.kind === "unsupported") {
|
package/dist/app/clipboard.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export declare function copyTextToClipboard(text: string): void;
|
|
2
2
|
export declare function clipboardSupportAvailable(env?: NodeJS.ProcessEnv): boolean;
|
|
3
3
|
export declare function clipboardInstallHint(): string;
|
|
4
|
+
export declare function osc52ClipboardSequence(text: string, env?: NodeJS.ProcessEnv): string;
|
package/dist/app/clipboard.js
CHANGED
|
@@ -10,6 +10,8 @@ export function copyTextToClipboard(text) {
|
|
|
10
10
|
}
|
|
11
11
|
if (copyWithNativeClipboard(text))
|
|
12
12
|
return;
|
|
13
|
+
if (copyWithOsc52(text))
|
|
14
|
+
return;
|
|
13
15
|
throw new Error(`No clipboard command found. ${clipboardInstallHint()}`);
|
|
14
16
|
}
|
|
15
17
|
export function clipboardSupportAvailable(env = process.env) {
|
|
@@ -60,6 +62,20 @@ function copyWithNativeClipboard(text) {
|
|
|
60
62
|
});
|
|
61
63
|
return !result.error && result.status === 0;
|
|
62
64
|
}
|
|
65
|
+
function copyWithOsc52(text) {
|
|
66
|
+
if (process.stdout.destroyed || (!process.stdout.isTTY && !process.env.TMUX && !process.env.STY))
|
|
67
|
+
return false;
|
|
68
|
+
process.stdout.write(osc52ClipboardSequence(text));
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
export function osc52ClipboardSequence(text, env = process.env) {
|
|
72
|
+
const sequence = `\x1b]52;c;${Buffer.from(text, "utf8").toString("base64")}\x07`;
|
|
73
|
+
if (env.TMUX)
|
|
74
|
+
return `\x1bPtmux;${sequence.replaceAll("\x1b", "\x1b\x1b")}\x1b\\`;
|
|
75
|
+
if (env.STY)
|
|
76
|
+
return `\x1bP${sequence}\x1b\\`;
|
|
77
|
+
return sequence;
|
|
78
|
+
}
|
|
63
79
|
function resolveNativeClipboardEntrypoint() {
|
|
64
80
|
try {
|
|
65
81
|
return require.resolve("@mariozechner/clipboard");
|
package/dist/app/icons.d.ts
CHANGED
package/dist/app/icons.js
CHANGED
|
@@ -22,6 +22,8 @@ const NERD_FONT_ICONS = {
|
|
|
22
22
|
plus: "\u{f0415}",
|
|
23
23
|
record: "\u{f044a}",
|
|
24
24
|
refresh: "\u{f0450}",
|
|
25
|
+
volumeHigh: "\u{f057e}",
|
|
26
|
+
volumeOff: "\u{f0581}",
|
|
25
27
|
user: "\u{f0004}",
|
|
26
28
|
compactTools: "\u{f035c}",
|
|
27
29
|
thinkingExpanded: "\u{f0335}",
|
|
@@ -45,6 +47,8 @@ const FALLBACK_ICONS = {
|
|
|
45
47
|
plus: "+",
|
|
46
48
|
record: "●",
|
|
47
49
|
refresh: "↻",
|
|
50
|
+
volumeHigh: "♪",
|
|
51
|
+
volumeOff: "ø",
|
|
48
52
|
user: "@",
|
|
49
53
|
compactTools: "≡",
|
|
50
54
|
thinkingExpanded: ">",
|
|
@@ -6,7 +6,7 @@ import type { ToastEntry, ToastVariant } from "../ui.js";
|
|
|
6
6
|
import type { AppPopupActionController } from "./popup-action-controller.js";
|
|
7
7
|
import type { AppPopupMenuController } from "./popup-menu-controller.js";
|
|
8
8
|
import type { AppScrollController } from "./scroll-controller.js";
|
|
9
|
-
import type { Entry, ImageClickTarget, MouseEvent, MouseSelection, StatusContextTarget, StatusCompactToolsTarget, StatusModelTarget, StatusModelUsageTarget, StatusPromptEnhancerTarget, StatusSessionTarget, TabLineMouseTarget, StatusThinkingExpandTarget, StatusThinkingTarget, StatusUserJumpTarget, StatusVoiceLanguageTarget, StatusVoiceMicTarget } from "./types.js";
|
|
9
|
+
import type { Entry, ImageClickTarget, MouseEvent, MouseSelection, StatusContextTarget, StatusCompactToolsTarget, StatusModelTarget, StatusModelUsageTarget, StatusPromptEnhancerTarget, StatusSessionTarget, StatusTerminalBellSoundTarget, TabLineMouseTarget, StatusThinkingExpandTarget, StatusThinkingTarget, StatusUserJumpTarget, StatusVoiceLanguageTarget, StatusVoiceMicTarget } from "./types.js";
|
|
10
10
|
import { type RenderedLink } from "./file-links.js";
|
|
11
11
|
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
type ClickFlash = {
|
|
@@ -58,6 +58,7 @@ export type AppMouseControllerHost = {
|
|
|
58
58
|
refreshModelUsageStatus(): void | Promise<void>;
|
|
59
59
|
toggleAllThinkingExpanded?(): void;
|
|
60
60
|
toggleSuperCompactTools?(): void;
|
|
61
|
+
toggleTerminalBellSound?(): void;
|
|
61
62
|
copyTextToClipboard?(text: string): void;
|
|
62
63
|
handleExtensionInputMouse(event: MouseEvent & {
|
|
63
64
|
localRow: number;
|
|
@@ -101,6 +102,7 @@ export declare class AppMouseController {
|
|
|
101
102
|
statusUserJumpTarget: StatusUserJumpTarget | undefined;
|
|
102
103
|
statusThinkingExpandTarget: StatusThinkingExpandTarget | undefined;
|
|
103
104
|
statusCompactToolsTarget: StatusCompactToolsTarget | undefined;
|
|
105
|
+
statusTerminalBellSoundTarget: StatusTerminalBellSoundTarget | undefined;
|
|
104
106
|
statusSessionTarget: StatusSessionTarget | undefined;
|
|
105
107
|
statusPromptEnhancerTarget: StatusPromptEnhancerTarget | undefined;
|
|
106
108
|
statusVoiceMicTarget: StatusVoiceMicTarget | undefined;
|
|
@@ -152,6 +154,7 @@ export declare class AppMouseController {
|
|
|
152
154
|
private handleStatusUserJumpClick;
|
|
153
155
|
private handleStatusThinkingExpandClick;
|
|
154
156
|
private handleStatusCompactToolsClick;
|
|
157
|
+
private handleStatusTerminalBellSoundClick;
|
|
155
158
|
private handleStatusSessionClick;
|
|
156
159
|
private handleStatusPromptEnhancerClick;
|
|
157
160
|
private handleStatusVoiceMicClick;
|
|
@@ -25,6 +25,7 @@ export class AppMouseController {
|
|
|
25
25
|
statusUserJumpTarget;
|
|
26
26
|
statusThinkingExpandTarget;
|
|
27
27
|
statusCompactToolsTarget;
|
|
28
|
+
statusTerminalBellSoundTarget;
|
|
28
29
|
statusSessionTarget;
|
|
29
30
|
statusPromptEnhancerTarget;
|
|
30
31
|
statusVoiceMicTarget;
|
|
@@ -78,6 +79,8 @@ export class AppMouseController {
|
|
|
78
79
|
return;
|
|
79
80
|
if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusCompactToolsClick(event)))
|
|
80
81
|
return;
|
|
82
|
+
if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusTerminalBellSoundClick(event)))
|
|
83
|
+
return;
|
|
81
84
|
if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusSessionClick(event)))
|
|
82
85
|
return;
|
|
83
86
|
if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusPromptEnhancerClick(event)))
|
|
@@ -522,6 +525,15 @@ export class AppMouseController {
|
|
|
522
525
|
this.host.toggleSuperCompactTools?.();
|
|
523
526
|
return true;
|
|
524
527
|
}
|
|
528
|
+
handleStatusTerminalBellSoundClick(event) {
|
|
529
|
+
const target = this.statusTerminalBellSoundTarget;
|
|
530
|
+
if (!target)
|
|
531
|
+
return false;
|
|
532
|
+
if (event.y !== target.row || event.x < target.startColumn || event.x >= target.endColumn)
|
|
533
|
+
return false;
|
|
534
|
+
this.host.toggleTerminalBellSound?.();
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
525
537
|
handleStatusSessionClick(event) {
|
|
526
538
|
const target = this.statusSessionTarget;
|
|
527
539
|
if (!target)
|
|
@@ -294,6 +294,7 @@ export class AppRenderController {
|
|
|
294
294
|
this.deps.mouseController.statusUserJumpTarget = this.deps.statusLineRenderer.userJumpTarget?.(statusLayout, statusRow);
|
|
295
295
|
this.deps.mouseController.statusThinkingExpandTarget = this.deps.statusLineRenderer.thinkingExpandTarget?.(statusLayout, statusRow);
|
|
296
296
|
this.deps.mouseController.statusCompactToolsTarget = this.deps.statusLineRenderer.compactToolsTarget?.(statusLayout, statusRow);
|
|
297
|
+
this.deps.mouseController.statusTerminalBellSoundTarget = this.deps.statusLineRenderer.terminalBellSoundTarget?.(statusLayout, statusRow);
|
|
297
298
|
this.deps.mouseController.statusSessionTarget = this.deps.statusLineRenderer.sessionTarget(statusLayout.text, statusRow, statusLayout.sessionLabel, statusLayout.workspaceLabel);
|
|
298
299
|
this.deps.mouseController.statusPromptEnhancerTarget = this.deps.statusLineRenderer.promptEnhancerTarget(statusLayout, statusRow);
|
|
299
300
|
this.deps.mouseController.statusVoiceMicTarget = this.deps.statusLineRenderer.voiceMicTarget(statusLayout, statusRow);
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { AgentSessionRuntime } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
export declare function createStartupInfoMessage(runtime: AgentSessionRuntime): string;
|
|
3
3
|
export declare function isEmptyStartupSession(runtime: AgentSessionRuntime): boolean;
|
package/dist/app/startup-info.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { basename, isAbsolute, relative, sep } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { getPixPackageVersion } from "./update.js";
|
|
4
4
|
export function createStartupInfoMessage(runtime) {
|
|
5
5
|
const sections = startupSections(runtime);
|
|
6
6
|
return [
|
|
7
7
|
formatModelLine(runtime),
|
|
8
|
-
|
|
8
|
+
"",
|
|
9
|
+
`pix v${getPixPackageVersion()}`,
|
|
9
10
|
"escape interrupt · ctrl+c/ctrl+d clear/exit · / commands",
|
|
10
11
|
"",
|
|
11
12
|
...sections.flatMap(formatSection),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { Theme } from "../theme.js";
|
|
3
|
-
import type { SessionActivity, StatusCompactToolsTarget, StatusContextTarget, StatusLineLayout, StatusModelTarget, StatusModelUsageTarget, StatusPromptEnhancerTarget, StatusSessionTarget, StatusThinkingExpandTarget, StatusThinkingTarget, StatusUserJumpTarget, StatusVoiceLanguageTarget, StatusVoiceMicTarget } from "./types.js";
|
|
3
|
+
import type { SessionActivity, StatusCompactToolsTarget, StatusContextTarget, StatusLineLayout, StatusModelTarget, StatusModelUsageTarget, StatusPromptEnhancerTarget, StatusSessionTarget, StatusTerminalBellSoundTarget, StatusThinkingExpandTarget, StatusThinkingTarget, StatusUserJumpTarget, StatusVoiceLanguageTarget, StatusVoiceMicTarget } from "./types.js";
|
|
4
4
|
import type { ScreenStyler } from "./screen-styler.js";
|
|
5
5
|
import { type ModelColorsConfig } from "../config.js";
|
|
6
6
|
export type StatusLineRendererHost = {
|
|
@@ -22,6 +22,8 @@ export type StatusLineRendererHost = {
|
|
|
22
22
|
promptEnhancerStatusWidgetText(): string;
|
|
23
23
|
promptEnhancerStatusWidgetActive(): boolean;
|
|
24
24
|
promptEnhancerStatusWidgetEnabled(): boolean;
|
|
25
|
+
terminalBellSoundStatusWidgetText(): string;
|
|
26
|
+
terminalBellSoundStatusWidgetEnabled(): boolean;
|
|
25
27
|
voiceStatusWidgetText(): string;
|
|
26
28
|
voiceStatusWidgetActive(): boolean;
|
|
27
29
|
userMessageJumpMenuActive?(): boolean;
|
|
@@ -43,12 +45,14 @@ export declare class StatusLineRenderer {
|
|
|
43
45
|
userJumpTarget(layout: StatusLineLayout, row: number): StatusUserJumpTarget | undefined;
|
|
44
46
|
thinkingExpandTarget(layout: StatusLineLayout, row: number): StatusThinkingExpandTarget | undefined;
|
|
45
47
|
compactToolsTarget(layout: StatusLineLayout, row: number): StatusCompactToolsTarget | undefined;
|
|
48
|
+
terminalBellSoundTarget(layout: StatusLineLayout, row: number): StatusTerminalBellSoundTarget | undefined;
|
|
46
49
|
sessionTarget(statusText: string, row: number, label: string, workspaceLabel: string): StatusSessionTarget | undefined;
|
|
47
50
|
private segments;
|
|
48
51
|
private pushPromptEnhancerWidgetSegment;
|
|
49
52
|
private pushUserJumpWidgetSegment;
|
|
50
53
|
private pushThinkingExpandWidgetSegment;
|
|
51
54
|
private pushCompactToolsWidgetSegment;
|
|
55
|
+
private pushTerminalBellSoundWidgetSegment;
|
|
52
56
|
private pushVoiceWidgetSegment;
|
|
53
57
|
private pushWorkspaceSegments;
|
|
54
58
|
private pushModelUsageSegments;
|
|
@@ -16,9 +16,10 @@ export class StatusLineRenderer {
|
|
|
16
16
|
const userJumpButton = APP_ICONS.user;
|
|
17
17
|
const thinkingExpandButton = APP_ICONS.thinkingExpanded;
|
|
18
18
|
const compactToolsButton = APP_ICONS.compactTools;
|
|
19
|
+
const terminalBellSoundWidgetText = this.host.terminalBellSoundStatusWidgetText();
|
|
19
20
|
const promptEnhancerWidgetText = this.host.promptEnhancerStatusWidgetText();
|
|
20
21
|
const voiceWidgetText = this.host.voiceStatusWidgetText();
|
|
21
|
-
const rightWidgetText = [userJumpButton, thinkingExpandButton, compactToolsButton, promptEnhancerWidgetText, voiceWidgetText].filter((text) => text.length > 0).join(" ");
|
|
22
|
+
const rightWidgetText = [userJumpButton, terminalBellSoundWidgetText, thinkingExpandButton, compactToolsButton, promptEnhancerWidgetText, voiceWidgetText].filter((text) => text.length > 0).join(" ");
|
|
22
23
|
const rightWidgetWidth = stringDisplayWidth(rightWidgetText);
|
|
23
24
|
const leftWidth = rightWidgetWidth > 0 && contentWidth > rightWidgetWidth + 1 ? contentWidth - rightWidgetWidth - 1 : contentWidth;
|
|
24
25
|
const baseStatus = this.host.currentStatus();
|
|
@@ -38,6 +39,11 @@ export class StatusLineRenderer {
|
|
|
38
39
|
: undefined;
|
|
39
40
|
if (userJumpWidget)
|
|
40
41
|
nextWidgetStartColumn = userJumpWidget.endColumn + 1;
|
|
42
|
+
const terminalBellSoundWidget = leftWidth < contentWidth && terminalBellSoundWidgetText.length > 0
|
|
43
|
+
? this.widgetLayout(nextWidgetStartColumn, terminalBellSoundWidgetText)
|
|
44
|
+
: undefined;
|
|
45
|
+
if (terminalBellSoundWidget)
|
|
46
|
+
nextWidgetStartColumn = terminalBellSoundWidget.endColumn + 1;
|
|
41
47
|
const thinkingExpandWidget = leftWidth < contentWidth
|
|
42
48
|
? this.widgetLayout(nextWidgetStartColumn, thinkingExpandButton)
|
|
43
49
|
: undefined;
|
|
@@ -62,6 +68,7 @@ export class StatusLineRenderer {
|
|
|
62
68
|
...(userJumpWidget ? { userJumpWidget } : {}),
|
|
63
69
|
...(thinkingExpandWidget ? { thinkingExpandWidget } : {}),
|
|
64
70
|
...(compactToolsWidget ? { compactToolsWidget } : {}),
|
|
71
|
+
...(terminalBellSoundWidget ? { terminalBellSoundWidget } : {}),
|
|
65
72
|
...(modelUsageLabel ? { modelUsageLabel } : {}),
|
|
66
73
|
...(contextBarLabel ? { contextBarLabel } : {}),
|
|
67
74
|
...(promptEnhancerWidget ? { promptEnhancerWidget } : {}),
|
|
@@ -162,6 +169,12 @@ export class StatusLineRenderer {
|
|
|
162
169
|
return undefined;
|
|
163
170
|
return { row, startColumn: widget.startColumn, endColumn: widget.endColumn };
|
|
164
171
|
}
|
|
172
|
+
terminalBellSoundTarget(layout, row) {
|
|
173
|
+
const widget = layout.terminalBellSoundWidget;
|
|
174
|
+
if (!widget)
|
|
175
|
+
return undefined;
|
|
176
|
+
return { row, startColumn: widget.startColumn, endColumn: widget.endColumn };
|
|
177
|
+
}
|
|
165
178
|
sessionTarget(statusText, row, label, workspaceLabel) {
|
|
166
179
|
if (!this.host.session || !label)
|
|
167
180
|
return undefined;
|
|
@@ -185,6 +198,7 @@ export class StatusLineRenderer {
|
|
|
185
198
|
this.pushUserJumpWidgetSegment(segments, statusText);
|
|
186
199
|
this.pushThinkingExpandWidgetSegment(segments, statusText);
|
|
187
200
|
this.pushCompactToolsWidgetSegment(segments, statusText);
|
|
201
|
+
this.pushTerminalBellSoundWidgetSegment(segments, statusText);
|
|
188
202
|
this.pushWorkspaceSegments(segments, statusText, layout.workspaceLabel);
|
|
189
203
|
if (layout.modelUsageLabel)
|
|
190
204
|
this.pushModelUsageSegments(segments, statusText, layout.modelUsageLabel);
|
|
@@ -248,6 +262,13 @@ export class StatusLineRenderer {
|
|
|
248
262
|
return;
|
|
249
263
|
this.pushSegment(segments, start, buttonText.length, this.host.superCompactToolsActive?.() ? this.host.theme.colors.info : this.host.theme.colors.muted);
|
|
250
264
|
}
|
|
265
|
+
pushTerminalBellSoundWidgetSegment(segments, statusText) {
|
|
266
|
+
const widgetText = this.host.terminalBellSoundStatusWidgetText();
|
|
267
|
+
const start = statusText.lastIndexOf(widgetText);
|
|
268
|
+
if (start < 0 || widgetText.length <= 0)
|
|
269
|
+
return;
|
|
270
|
+
this.pushSegment(segments, start, widgetText.length, this.host.terminalBellSoundStatusWidgetEnabled() ? this.host.theme.colors.info : this.host.theme.colors.muted);
|
|
271
|
+
}
|
|
251
272
|
pushVoiceWidgetSegment(segments, statusText) {
|
|
252
273
|
const widgetText = this.host.voiceStatusWidgetText();
|
|
253
274
|
const start = statusText.lastIndexOf(widgetText);
|
|
@@ -261,7 +282,7 @@ export class StatusLineRenderer {
|
|
|
261
282
|
}
|
|
262
283
|
const separatorIndex = widgetText.indexOf(" ");
|
|
263
284
|
const micLength = separatorIndex >= 0 ? separatorIndex : widgetText.length;
|
|
264
|
-
this.pushSegment(segments, start, micLength, this.host.theme.colors.
|
|
285
|
+
this.pushSegment(segments, start, micLength, this.host.theme.colors.muted);
|
|
265
286
|
}
|
|
266
287
|
pushWorkspaceSegments(segments, statusText, workspaceLabel) {
|
|
267
288
|
const start = statusText.lastIndexOf(workspaceLabel);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function getPiToolsSuiteUserConfigPath(homeDir?: string): string;
|
|
2
|
+
export declare function readTerminalBellSoundEnabled(configPath?: string): boolean;
|
|
3
|
+
export declare function writeTerminalBellSoundEnabled(enabled: boolean, configPath?: string): void;
|
|
4
|
+
export declare class TerminalBellSoundController {
|
|
5
|
+
private readonly configPath;
|
|
6
|
+
private enabled;
|
|
7
|
+
constructor(configPath?: string);
|
|
8
|
+
statusWidgetText(): string;
|
|
9
|
+
isEnabled(): boolean;
|
|
10
|
+
toggle(): boolean;
|
|
11
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { applyEdits, modify, parse as parseJsonc } from "jsonc-parser";
|
|
5
|
+
import { APP_ICONS } from "./icons.js";
|
|
6
|
+
const TERMINAL_BELL_CONFIG_KEY = "terminalBell";
|
|
7
|
+
const SOUND_CONFIG_KEY = "sound";
|
|
8
|
+
export function getPiToolsSuiteUserConfigPath(homeDir = homedir()) {
|
|
9
|
+
return join(homeDir, ".config", "pi", "pi-tools-suite.jsonc");
|
|
10
|
+
}
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
export function readTerminalBellSoundEnabled(configPath = getPiToolsSuiteUserConfigPath()) {
|
|
15
|
+
if (!existsSync(configPath))
|
|
16
|
+
return true;
|
|
17
|
+
try {
|
|
18
|
+
const parsed = parseJsonc(readFileSync(configPath, "utf-8"));
|
|
19
|
+
if (!isRecord(parsed))
|
|
20
|
+
return true;
|
|
21
|
+
const terminalBell = parsed[TERMINAL_BELL_CONFIG_KEY];
|
|
22
|
+
if (!isRecord(terminalBell))
|
|
23
|
+
return true;
|
|
24
|
+
return typeof terminalBell[SOUND_CONFIG_KEY] === "boolean" ? terminalBell[SOUND_CONFIG_KEY] : true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function writeTerminalBellSoundEnabled(enabled, configPath = getPiToolsSuiteUserConfigPath()) {
|
|
31
|
+
const original = existsSync(configPath) ? readFileSync(configPath, "utf-8") : "{}\n";
|
|
32
|
+
const edits = modify(original, [TERMINAL_BELL_CONFIG_KEY, SOUND_CONFIG_KEY], enabled, {
|
|
33
|
+
formattingOptions: { insertSpaces: true, tabSize: 2, eol: "\n" },
|
|
34
|
+
});
|
|
35
|
+
const updated = applyEdits(original, edits);
|
|
36
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
37
|
+
writeFileSync(configPath, updated.endsWith("\n") ? updated : `${updated}\n`, "utf-8");
|
|
38
|
+
}
|
|
39
|
+
export class TerminalBellSoundController {
|
|
40
|
+
configPath;
|
|
41
|
+
enabled;
|
|
42
|
+
constructor(configPath = getPiToolsSuiteUserConfigPath()) {
|
|
43
|
+
this.configPath = configPath;
|
|
44
|
+
this.enabled = readTerminalBellSoundEnabled(this.configPath);
|
|
45
|
+
}
|
|
46
|
+
statusWidgetText() {
|
|
47
|
+
return this.enabled ? APP_ICONS.volumeHigh : APP_ICONS.volumeOff;
|
|
48
|
+
}
|
|
49
|
+
isEnabled() {
|
|
50
|
+
return this.enabled;
|
|
51
|
+
}
|
|
52
|
+
toggle() {
|
|
53
|
+
const nextEnabled = !this.enabled;
|
|
54
|
+
writeTerminalBellSoundEnabled(nextEnabled, this.configPath);
|
|
55
|
+
this.enabled = nextEnabled;
|
|
56
|
+
return this.enabled;
|
|
57
|
+
}
|
|
58
|
+
}
|
package/dist/app/types.d.ts
CHANGED
|
@@ -266,6 +266,7 @@ export type StatusLineLayout = {
|
|
|
266
266
|
userJumpWidget?: StatusUserJumpWidgetLayout;
|
|
267
267
|
thinkingExpandWidget?: StatusThinkingExpandWidgetLayout;
|
|
268
268
|
compactToolsWidget?: StatusCompactToolsWidgetLayout;
|
|
269
|
+
terminalBellSoundWidget?: StatusTerminalBellSoundWidgetLayout;
|
|
269
270
|
promptEnhancerWidget?: StatusPromptEnhancerWidgetLayout;
|
|
270
271
|
voiceWidget?: StatusVoiceWidgetLayout;
|
|
271
272
|
};
|
|
@@ -281,10 +282,19 @@ export type StatusCompactToolsWidgetLayout = {
|
|
|
281
282
|
startColumn: number;
|
|
282
283
|
endColumn: number;
|
|
283
284
|
};
|
|
285
|
+
export type StatusTerminalBellSoundWidgetLayout = {
|
|
286
|
+
startColumn: number;
|
|
287
|
+
endColumn: number;
|
|
288
|
+
};
|
|
284
289
|
export type StatusPromptEnhancerWidgetLayout = {
|
|
285
290
|
startColumn: number;
|
|
286
291
|
endColumn: number;
|
|
287
292
|
};
|
|
293
|
+
export type StatusTerminalBellSoundTarget = {
|
|
294
|
+
row: number;
|
|
295
|
+
startColumn: number;
|
|
296
|
+
endColumn: number;
|
|
297
|
+
};
|
|
288
298
|
export type StatusVoiceWidgetLayout = {
|
|
289
299
|
startColumn: number;
|
|
290
300
|
micEndColumn: number;
|
package/dist/app/update.d.ts
CHANGED
|
@@ -30,7 +30,9 @@ export type PixUpdateCheckOptions = {
|
|
|
30
30
|
};
|
|
31
31
|
export declare function pixUpdateUsage(): string;
|
|
32
32
|
export declare function parsePixUpdateArgs(argv: readonly string[]): PixUpdateCliOptions;
|
|
33
|
+
export declare function getPixPackageVersion(packageRoot?: string): string;
|
|
33
34
|
export declare function checkPixUpdate(options?: PixUpdateCheckOptions): Promise<PixUpdateCheckResult>;
|
|
34
35
|
export declare function formatPixUpdateCheck(result: PixUpdateCheckResult): string;
|
|
36
|
+
export declare function formatPixStartupUpdateDialog(result: PixUpdateCheckResult): string;
|
|
35
37
|
export declare function getPixSelfUpdateCommand(packageName: string, latestVersion?: string, packageRoot?: string): PixSelfUpdateCommand | undefined;
|
|
36
38
|
export declare function runPixUpdateCli(argv?: readonly string[]): Promise<number>;
|
package/dist/app/update.js
CHANGED
|
@@ -40,6 +40,9 @@ export function parsePixUpdateArgs(argv) {
|
|
|
40
40
|
}
|
|
41
41
|
return { checkOnly, force, help };
|
|
42
42
|
}
|
|
43
|
+
export function getPixPackageVersion(packageRoot) {
|
|
44
|
+
return readPixPackageInfo(packageRoot).version;
|
|
45
|
+
}
|
|
43
46
|
export async function checkPixUpdate(options = {}) {
|
|
44
47
|
const packageInfo = readPixPackageInfo(options.packageRoot);
|
|
45
48
|
const base = {
|
|
@@ -107,6 +110,19 @@ export function formatPixUpdateCheck(result) {
|
|
|
107
110
|
lines.push("scope: Pix package, renderer extensions, bundled skills copied into ~/.agents/skills, and the pi-tools-suite payload linked into ~/.pi/agent/extensions");
|
|
108
111
|
return lines.join("\n");
|
|
109
112
|
}
|
|
113
|
+
export function formatPixStartupUpdateDialog(result) {
|
|
114
|
+
const lines = [
|
|
115
|
+
"A new Pix version is available.",
|
|
116
|
+
`current: ${result.packageName} v${result.currentVersion}`,
|
|
117
|
+
...(result.latestVersion ? [`latest: ${result.latestVersion}`] : []),
|
|
118
|
+
"",
|
|
119
|
+
"To update:",
|
|
120
|
+
"1. Exit Pix.",
|
|
121
|
+
"2. Run `pix update` in your shell.",
|
|
122
|
+
"3. Start Pix again after the update completes.",
|
|
123
|
+
];
|
|
124
|
+
return lines.join("\n");
|
|
125
|
+
}
|
|
110
126
|
export function getPixSelfUpdateCommand(packageName, latestVersion, packageRoot = readPixPackageInfo().packageRoot) {
|
|
111
127
|
if (!packageRootLooksPackageManaged(packageRoot))
|
|
112
128
|
return undefined;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
4
5
|
import { delimiter, isAbsolute, join } from "node:path";
|
|
6
|
+
import { parse as parseJsonc } from "jsonc-parser";
|
|
5
7
|
|
|
6
8
|
const BELL = "\x07";
|
|
7
9
|
const DEFAULT_IDLE_DELAY_MS = 250;
|
|
@@ -13,6 +15,8 @@ const DEFAULT_NOTIFICATION_TITLE = "Pi";
|
|
|
13
15
|
const DEFAULT_NOTIFICATION_MESSAGE = "Session stopped";
|
|
14
16
|
const DEFAULT_ASK_USER_NOTIFICATION_MESSAGE = "Waiting for your answer";
|
|
15
17
|
const DEFAULT_MAC_SOUND = "Glass";
|
|
18
|
+
const TERMINAL_BELL_CONFIG_KEY = "terminalBell";
|
|
19
|
+
const SOUND_CONFIG_KEY = "sound";
|
|
16
20
|
|
|
17
21
|
const TERM_PROGRAM_BUNDLE_IDS: Record<string, string> = {
|
|
18
22
|
Apple_Terminal: "com.apple.Terminal",
|
|
@@ -50,29 +54,55 @@ function extensionDisabled(): boolean {
|
|
|
50
54
|
return isTruthyEnv(process.env.HEADLESS) || isTruthyEnv(process.env.PI_TERMINAL_BELL_DISABLED);
|
|
51
55
|
}
|
|
52
56
|
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
if (process.env.PI_TERMINAL_BELL_FORCE === "1") return true;
|
|
56
|
-
return Boolean(process.stdout.isTTY || process.stderr.isTTY);
|
|
57
|
+
function getPiToolsSuiteUserConfigPath(homeDir = homedir()): string {
|
|
58
|
+
return join(homeDir, ".config", "pi", "pi-tools-suite.jsonc");
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
62
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function readTerminalBellSoundConfig(configPath = getPiToolsSuiteUserConfigPath()): boolean | undefined {
|
|
66
|
+
if (!existsSync(configPath)) return undefined;
|
|
67
|
+
try {
|
|
68
|
+
const parsed = parseJsonc(readFileSync(configPath, "utf-8")) as unknown;
|
|
69
|
+
if (!isRecord(parsed)) return undefined;
|
|
70
|
+
const terminalBell = parsed[TERMINAL_BELL_CONFIG_KEY];
|
|
71
|
+
if (!isRecord(terminalBell)) return undefined;
|
|
72
|
+
const sound = terminalBell[SOUND_CONFIG_KEY];
|
|
73
|
+
return typeof sound === "boolean" ? sound : undefined;
|
|
74
|
+
} catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
62
77
|
}
|
|
63
78
|
|
|
64
|
-
function
|
|
79
|
+
export function terminalBellSoundEnabled(ctx: Pick<ExtensionContext, "hasUI">, configPath = getPiToolsSuiteUserConfigPath()): boolean {
|
|
65
80
|
if (process.env.PI_TERMINAL_BELL_SOUND === "0") return false;
|
|
66
81
|
if (process.env.PI_TERMINAL_BELL_SOUND === "1") return true;
|
|
82
|
+
const configured = readTerminalBellSoundConfig(configPath);
|
|
83
|
+
if (configured !== undefined) return configured;
|
|
67
84
|
return ctx.hasUI === true;
|
|
68
85
|
}
|
|
69
86
|
|
|
70
|
-
function
|
|
87
|
+
export function terminalBellNotificationsEnabled(ctx: Pick<ExtensionContext, "hasUI">, configPath = getPiToolsSuiteUserConfigPath()): boolean {
|
|
88
|
+
if (!terminalBellSoundEnabled(ctx, configPath)) return false;
|
|
71
89
|
if (process.env.PI_TERMINAL_BELL_NOTIFY === "0") return false;
|
|
72
90
|
if (process.env.PI_TERMINAL_BELL_NOTIFY === "1") return true;
|
|
73
91
|
return ctx.hasUI === true;
|
|
74
92
|
}
|
|
75
93
|
|
|
94
|
+
export function canRingTerminal(ctx: Pick<ExtensionContext, "hasUI">, configPath = getPiToolsSuiteUserConfigPath()): boolean {
|
|
95
|
+
if (!terminalBellSoundEnabled(ctx, configPath)) return false;
|
|
96
|
+
if (process.env.PI_TERMINAL_BELL === "0") return false;
|
|
97
|
+
if (process.env.PI_TERMINAL_BELL_FORCE === "1") return true;
|
|
98
|
+
return Boolean(process.stdout.isTTY || process.stderr.isTTY);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function writeBell(): void {
|
|
102
|
+
const stream = process.stdout.isTTY || !process.stderr.isTTY ? process.stdout : process.stderr;
|
|
103
|
+
stream.write(BELL);
|
|
104
|
+
}
|
|
105
|
+
|
|
76
106
|
function appleScriptString(value: string): string {
|
|
77
107
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
78
108
|
}
|
|
@@ -124,7 +154,7 @@ function detectMacActivationBundleId(): string | undefined {
|
|
|
124
154
|
}
|
|
125
155
|
|
|
126
156
|
function playAttentionSound(ctx: ExtensionContext): void {
|
|
127
|
-
if (!
|
|
157
|
+
if (!terminalBellSoundEnabled(ctx)) return;
|
|
128
158
|
if (process.platform !== "darwin") return;
|
|
129
159
|
const sound = process.env.PI_TERMINAL_BELL_SOUND && process.env.PI_TERMINAL_BELL_SOUND !== "1"
|
|
130
160
|
? process.env.PI_TERMINAL_BELL_SOUND
|
|
@@ -139,7 +169,7 @@ function notifySessionStopped(
|
|
|
139
169
|
macActivationBundleId: string | undefined,
|
|
140
170
|
message = process.env.PI_TERMINAL_BELL_NOTIFY_MESSAGE ?? DEFAULT_NOTIFICATION_MESSAGE,
|
|
141
171
|
): void {
|
|
142
|
-
if (!
|
|
172
|
+
if (!terminalBellNotificationsEnabled(ctx)) return;
|
|
143
173
|
const title = process.env.PI_TERMINAL_BELL_NOTIFY_TITLE ?? DEFAULT_NOTIFICATION_TITLE;
|
|
144
174
|
|
|
145
175
|
if (process.platform === "darwin") {
|
|
@@ -221,7 +251,7 @@ export default function terminalBell(pi: ExtensionAPI) {
|
|
|
221
251
|
}
|
|
222
252
|
|
|
223
253
|
function notifyAttention(ctx: ExtensionContext, message?: string): void {
|
|
224
|
-
if (canRingTerminal()) writeBell();
|
|
254
|
+
if (canRingTerminal(ctx)) writeBell();
|
|
225
255
|
playAttentionSound(ctx);
|
|
226
256
|
notifySessionStopped(ctx, macActivationBundleId, message);
|
|
227
257
|
pi.events.emit(TERMINAL_BELL_ATTENTION_EVENT, {
|
|
@@ -16,6 +16,14 @@ export const DEFAULT_PI_TOOLS_SUITE_CONFIG_JSONC = `{
|
|
|
16
16
|
// "dcp"
|
|
17
17
|
],
|
|
18
18
|
|
|
19
|
+
// Terminal bell notification settings.
|
|
20
|
+
"terminalBell": {
|
|
21
|
+
// Toggle terminal-bell attention signals across supported OSes:
|
|
22
|
+
// terminal BEL, macOS notification sound, and system notifications
|
|
23
|
+
// (macOS terminal-notifier/osascript or Linux notify-send when available).
|
|
24
|
+
"sound": true
|
|
25
|
+
},
|
|
26
|
+
|
|
19
27
|
// Dynamic Context Pruning (DCP) module config.
|
|
20
28
|
// The DCP module owns the compress tool and /dcp commands.
|
|
21
29
|
"dcp": {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
4
5
|
import { delimiter, isAbsolute, join } from "node:path";
|
|
6
|
+
import { parse as parseJsonc } from "jsonc-parser";
|
|
5
7
|
|
|
6
8
|
const BELL = "\x07";
|
|
7
9
|
const DEFAULT_IDLE_DELAY_MS = 250;
|
|
@@ -12,6 +14,8 @@ const DEFAULT_NOTIFICATION_TITLE = "Pi";
|
|
|
12
14
|
const DEFAULT_NOTIFICATION_MESSAGE = "Session stopped";
|
|
13
15
|
const DEFAULT_ASK_USER_NOTIFICATION_MESSAGE = "Waiting for your answer";
|
|
14
16
|
const DEFAULT_MAC_SOUND = "Glass";
|
|
17
|
+
const TERMINAL_BELL_CONFIG_KEY = "terminalBell";
|
|
18
|
+
const SOUND_CONFIG_KEY = "sound";
|
|
15
19
|
|
|
16
20
|
const TERM_PROGRAM_BUNDLE_IDS: Record<string, string> = {
|
|
17
21
|
Apple_Terminal: "com.apple.Terminal",
|
|
@@ -49,29 +53,55 @@ function extensionDisabled(): boolean {
|
|
|
49
53
|
return isTruthyEnv(process.env.HEADLESS) || isTruthyEnv(process.env.PI_TERMINAL_BELL_DISABLED);
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
if (process.env.PI_TERMINAL_BELL_FORCE === "1") return true;
|
|
55
|
-
return Boolean(process.stdout.isTTY || process.stderr.isTTY);
|
|
56
|
+
function getPiToolsSuiteUserConfigPath(homeDir = homedir()): string {
|
|
57
|
+
return join(homeDir, ".config", "pi", "pi-tools-suite.jsonc");
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
61
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function readTerminalBellSoundConfig(configPath = getPiToolsSuiteUserConfigPath()): boolean | undefined {
|
|
65
|
+
if (!existsSync(configPath)) return undefined;
|
|
66
|
+
try {
|
|
67
|
+
const parsed = parseJsonc(readFileSync(configPath, "utf-8")) as unknown;
|
|
68
|
+
if (!isRecord(parsed)) return undefined;
|
|
69
|
+
const terminalBell = parsed[TERMINAL_BELL_CONFIG_KEY];
|
|
70
|
+
if (!isRecord(terminalBell)) return undefined;
|
|
71
|
+
const sound = terminalBell[SOUND_CONFIG_KEY];
|
|
72
|
+
return typeof sound === "boolean" ? sound : undefined;
|
|
73
|
+
} catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
61
76
|
}
|
|
62
77
|
|
|
63
|
-
function
|
|
78
|
+
export function terminalBellSoundEnabled(ctx: Pick<ExtensionContext, "hasUI">, configPath = getPiToolsSuiteUserConfigPath()): boolean {
|
|
64
79
|
if (process.env.PI_TERMINAL_BELL_SOUND === "0") return false;
|
|
65
80
|
if (process.env.PI_TERMINAL_BELL_SOUND === "1") return true;
|
|
81
|
+
const configured = readTerminalBellSoundConfig(configPath);
|
|
82
|
+
if (configured !== undefined) return configured;
|
|
66
83
|
return ctx.hasUI === true;
|
|
67
84
|
}
|
|
68
85
|
|
|
69
|
-
function
|
|
86
|
+
export function terminalBellNotificationsEnabled(ctx: Pick<ExtensionContext, "hasUI">, configPath = getPiToolsSuiteUserConfigPath()): boolean {
|
|
87
|
+
if (!terminalBellSoundEnabled(ctx, configPath)) return false;
|
|
70
88
|
if (process.env.PI_TERMINAL_BELL_NOTIFY === "0") return false;
|
|
71
89
|
if (process.env.PI_TERMINAL_BELL_NOTIFY === "1") return true;
|
|
72
90
|
return ctx.hasUI === true;
|
|
73
91
|
}
|
|
74
92
|
|
|
93
|
+
export function canRingTerminal(ctx: Pick<ExtensionContext, "hasUI">, configPath = getPiToolsSuiteUserConfigPath()): boolean {
|
|
94
|
+
if (!terminalBellSoundEnabled(ctx, configPath)) return false;
|
|
95
|
+
if (process.env.PI_TERMINAL_BELL === "0") return false;
|
|
96
|
+
if (process.env.PI_TERMINAL_BELL_FORCE === "1") return true;
|
|
97
|
+
return Boolean(process.stdout.isTTY || process.stderr.isTTY);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function writeBell(): void {
|
|
101
|
+
const stream = process.stdout.isTTY || !process.stderr.isTTY ? process.stdout : process.stderr;
|
|
102
|
+
stream.write(BELL);
|
|
103
|
+
}
|
|
104
|
+
|
|
75
105
|
function appleScriptString(value: string): string {
|
|
76
106
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
77
107
|
}
|
|
@@ -123,7 +153,7 @@ function detectMacActivationBundleId(): string | undefined {
|
|
|
123
153
|
}
|
|
124
154
|
|
|
125
155
|
function playAttentionSound(ctx: ExtensionContext): void {
|
|
126
|
-
if (!
|
|
156
|
+
if (!terminalBellSoundEnabled(ctx)) return;
|
|
127
157
|
if (process.platform !== "darwin") return;
|
|
128
158
|
const sound = process.env.PI_TERMINAL_BELL_SOUND && process.env.PI_TERMINAL_BELL_SOUND !== "1"
|
|
129
159
|
? process.env.PI_TERMINAL_BELL_SOUND
|
|
@@ -138,7 +168,7 @@ function notifySessionStopped(
|
|
|
138
168
|
macActivationBundleId: string | undefined,
|
|
139
169
|
message = process.env.PI_TERMINAL_BELL_NOTIFY_MESSAGE ?? DEFAULT_NOTIFICATION_MESSAGE,
|
|
140
170
|
): void {
|
|
141
|
-
if (!
|
|
171
|
+
if (!terminalBellNotificationsEnabled(ctx)) return;
|
|
142
172
|
const title = process.env.PI_TERMINAL_BELL_NOTIFY_TITLE ?? DEFAULT_NOTIFICATION_TITLE;
|
|
143
173
|
|
|
144
174
|
if (process.platform === "darwin") {
|
|
@@ -220,7 +250,7 @@ export default function terminalBell(pi: ExtensionAPI) {
|
|
|
220
250
|
}
|
|
221
251
|
|
|
222
252
|
function notifyAttention(ctx: ExtensionContext, message?: string): void {
|
|
223
|
-
if (canRingTerminal()) writeBell();
|
|
253
|
+
if (canRingTerminal(ctx)) writeBell();
|
|
224
254
|
playAttentionSound(ctx);
|
|
225
255
|
notifySessionStopped(ctx, macActivationBundleId, message);
|
|
226
256
|
}
|