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 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") {
@@ -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;
@@ -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");
@@ -16,6 +16,8 @@ declare const NERD_FONT_ICONS: {
16
16
  readonly plus: "󰐕";
17
17
  readonly record: "󰑊";
18
18
  readonly refresh: "󰑐";
19
+ readonly volumeHigh: "󰕾";
20
+ readonly volumeOff: "󰖁";
19
21
  readonly user: "󰀄";
20
22
  readonly compactTools: "󰍜";
21
23
  readonly thinkingExpanded: "󰌵";
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 { type AgentSessionRuntime } from "@earendil-works/pi-coding-agent";
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;
@@ -1,11 +1,12 @@
1
1
  import { homedir } from "node:os";
2
2
  import { basename, isAbsolute, relative, sep } from "node:path";
3
- import { VERSION } from "@earendil-works/pi-coding-agent";
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
- `pix · pi-sdk v${VERSION}`,
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.info);
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
+ }
@@ -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;
@@ -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>;
@@ -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 canRingTerminal(): boolean {
54
- if (process.env.PI_TERMINAL_BELL === "0") return false;
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 writeBell(): void {
60
- const stream = process.stdout.isTTY || !process.stderr.isTTY ? process.stdout : process.stderr;
61
- stream.write(BELL);
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 soundEnabled(ctx: ExtensionContext): boolean {
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 notificationsEnabled(ctx: ExtensionContext): boolean {
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 (!soundEnabled(ctx)) return;
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 (!notificationsEnabled(ctx)) return;
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 canRingTerminal(): boolean {
53
- if (process.env.PI_TERMINAL_BELL === "0") return false;
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 writeBell(): void {
59
- const stream = process.stdout.isTTY || !process.stderr.isTTY ? process.stdout : process.stderr;
60
- stream.write(BELL);
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 soundEnabled(ctx: ExtensionContext): boolean {
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 notificationsEnabled(ctx: ExtensionContext): boolean {
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 (!soundEnabled(ctx)) return;
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 (!notificationsEnabled(ctx)) return;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ui-extend",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {