Fix custom buttons

This commit is contained in:
Daniel Imms
2026-02-13 10:08:06 -08:00
parent 65bfb3303c
commit 48c903255b
2 changed files with 64 additions and 21 deletions

View File

@@ -20,6 +20,7 @@ const enum Osc99PayloadType {
}
type Osc99Occasion = 'always' | 'unfocused' | 'invisible';
type Osc99CloseReason = 'button' | 'secondary' | 'auto' | 'protocol';
interface IOsc99NotificationState {
id: string | undefined;
@@ -42,6 +43,7 @@ interface IOsc99ActiveNotification {
reportOnActivate: boolean;
reportOnClose: boolean;
focusOnActivate: boolean;
closeReason: Osc99CloseReason | undefined;
}
export interface IOsc99NotificationHost {
@@ -119,8 +121,9 @@ export class TerminalNotificationHandler extends Disposable {
return true;
}
this._showOsc99Notification(state);
this._clearOsc99PendingState(id);
if (this._showOsc99Notification(state)) {
this._clearOsc99PendingState(id);
}
return true;
}
@@ -294,10 +297,10 @@ export class TerminalNotificationHandler extends Disposable {
}
}
private _showOsc99Notification(state: IOsc99NotificationState): void {
private _showOsc99Notification(state: IOsc99NotificationState): boolean {
const message = this._getOsc99NotificationMessage(state);
if (!message) {
return;
return false;
}
const severity = state.urgency === 2 ? Severity.Warning : Severity.Info;
@@ -310,6 +313,7 @@ export class TerminalNotificationHandler extends Disposable {
const actionStore = this._register(new DisposableStore());
const handleRef: { current: INotificationHandle | undefined } = { current: undefined };
const activeRef: { current: IOsc99ActiveNotification | undefined } = { current: undefined };
const reportActivation = (buttonIndex?: number, forceFocus?: boolean) => {
if (forceFocus || state.focusOnActivate) {
this._host.focusTerminal();
@@ -326,21 +330,14 @@ export class TerminalNotificationHandler extends Disposable {
continue;
}
const action = actionStore.add(new Action(`terminal.osc99.button.${i}`, label, undefined, true, () => {
if (activeRef.current) {
activeRef.current.closeReason = 'button';
}
reportActivation(i + 1);
handleRef.current?.close();
}));
primaryActions.push(action);
}
primaryActions.push(actionStore.add(new Action(
'terminal.osc99.focus',
localize('terminalNotificationFocus', 'Focus Terminal'),
undefined,
true,
() => {
reportActivation(undefined, true);
handleRef.current?.close();
}
)));
const secondaryActions: IAction[] = [];
secondaryActions.push(actionStore.add(new Action(
@@ -348,7 +345,12 @@ export class TerminalNotificationHandler extends Disposable {
localize('terminalNotificationDismiss', 'Dismiss'),
undefined,
true,
() => handleRef.current?.close()
() => {
if (activeRef.current) {
activeRef.current.closeReason = 'secondary';
}
handleRef.current?.close();
}
)));
secondaryActions.push(actionStore.add(new Action(
'terminal.osc99.disable',
@@ -357,6 +359,9 @@ export class TerminalNotificationHandler extends Disposable {
true,
async () => {
await this._host.updateEnableNotifications(false);
if (activeRef.current) {
activeRef.current.closeReason = 'secondary';
}
handleRef.current?.close();
}
)));
@@ -366,6 +371,7 @@ export class TerminalNotificationHandler extends Disposable {
if (state.id) {
const existing = this._osc99ActiveNotifications.get(state.id);
if (existing) {
activeRef.current = existing;
existing.handle.updateMessage(message);
existing.handle.updateSeverity(severity);
existing.handle.updateActions(actions);
@@ -375,8 +381,8 @@ export class TerminalNotificationHandler extends Disposable {
existing.reportOnActivate = state.reportOnActivate;
existing.reportOnClose = state.reportOnClose;
existing.autoCloseDisposable?.dispose();
existing.autoCloseDisposable = this._scheduleOsc99AutoClose(existing.handle, state.autoCloseMs);
return;
existing.autoCloseDisposable = this._scheduleOsc99AutoClose(existing, state.autoCloseMs);
return true;
}
}
@@ -397,10 +403,18 @@ export class TerminalNotificationHandler extends Disposable {
autoCloseDisposable: undefined,
reportOnActivate: state.reportOnActivate,
reportOnClose: state.reportOnClose,
focusOnActivate: state.focusOnActivate
focusOnActivate: state.focusOnActivate,
closeReason: undefined
};
active.autoCloseDisposable = this._scheduleOsc99AutoClose(handle, state.autoCloseMs);
activeRef.current = active;
active.autoCloseDisposable = this._scheduleOsc99AutoClose(active, state.autoCloseMs);
this._register(handle.onDidClose(() => {
if (active.reportOnActivate && active.closeReason === undefined) {
if (active.focusOnActivate) {
this._host.focusTerminal();
}
this._sendOsc99ActivationReport(active.id);
}
if (active.reportOnClose) {
this._sendOsc99CloseReport(active.id);
}
@@ -414,6 +428,7 @@ export class TerminalNotificationHandler extends Disposable {
if (active.id) {
this._osc99ActiveNotifications.set(active.id, active);
}
return true;
}
private _getOsc99NotificationMessage(state: IOsc99NotificationState): string | undefined {
@@ -446,11 +461,14 @@ export class TerminalNotificationHandler extends Disposable {
}
}
private _scheduleOsc99AutoClose(handle: INotificationHandle, autoCloseMs: number | undefined): IDisposable | undefined {
private _scheduleOsc99AutoClose(active: IOsc99ActiveNotification, autoCloseMs: number | undefined): IDisposable | undefined {
if (autoCloseMs === undefined || autoCloseMs <= 0) {
return undefined;
}
return disposableTimeout(() => handle.close(), autoCloseMs, this._store);
return disposableTimeout(() => {
active.closeReason = 'auto';
active.handle.close();
}, autoCloseMs, this._store);
}
private _closeOsc99Notification(id: string | undefined): void {
@@ -459,6 +477,7 @@ export class TerminalNotificationHandler extends Disposable {
}
const active = this._osc99ActiveNotifications.get(id);
if (active) {
active.closeReason = 'protocol';
active.handle.close();
}
this._osc99PendingNotifications.delete(id);

View File

@@ -181,6 +181,30 @@ suite('Terminal OSC 99 notifications', () => {
strictEqual(host.writes[0], '\x1b]99;i=btn;1\x1b\\');
});
test('supports buttons before title and reports body activation', async () => {
handler.handleSequence('i=btn:p=buttons;One\u2028Two');
handler.handleSequence('i=btn:a=report;Buttons test');
strictEqual(host.notifications.length, 1);
const actions = host.notifications[0].actions;
if (!actions?.primary || actions.primary.length !== 2) {
throw new Error('Expected two primary actions');
}
strictEqual(actions.primary[0].label, 'One');
strictEqual(actions.primary[1].label, 'Two');
await actions.primary[1].run();
strictEqual(host.writes[0], '\x1b]99;i=btn;2\x1b\\');
});
test('reports activation when notification closes without button action', () => {
handler.handleSequence('i=btn:p=buttons;One\u2028Two');
handler.handleSequence('i=btn:a=report;Buttons test');
host.notifications[0].close();
strictEqual(host.writes[0], '\x1b]99;i=btn;\x1b\\');
});
test('sends close report when requested', () => {
handler.handleSequence('i=close:c=1:p=title;Bye');
strictEqual(host.notifications.length, 1);