xtralab 0.8.0 → 0.10.0
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 +30 -6
- package/lib/fileBrowser/commands.d.ts +7 -0
- package/lib/fileBrowser/commands.js +74 -1
- package/lib/fileBrowser/fileBrowser.js +4 -1
- package/lib/fileBrowser/icons.d.ts +5 -9
- package/lib/fileBrowser/icons.js +97 -13
- package/lib/fileBrowser/index.js +5 -3
- package/lib/fileTypeIcons/index.d.ts +6 -0
- package/lib/fileTypeIcons/index.js +144 -0
- package/lib/git/commands.d.ts +34 -33
- package/lib/git/commands.js +132 -10
- package/lib/git/diffModel.d.ts +14 -0
- package/lib/git/diffModel.js +75 -0
- package/lib/git/diffProvider.d.ts +0 -5
- package/lib/git/diffProvider.js +6 -376
- package/lib/git/diffSurface.d.ts +36 -26
- package/lib/git/diffSurface.js +13 -20
- package/lib/git/diffTheme.d.ts +9 -0
- package/lib/git/diffTheme.js +31 -0
- package/lib/git/diffWidget.d.ts +54 -91
- package/lib/git/diffWidget.js +184 -361
- package/lib/git/index.d.ts +1 -6
- package/lib/git/index.js +7 -23
- package/lib/git/notebookDiff.d.ts +6 -1
- package/lib/git/notebookDiff.js +4 -3
- package/lib/index.js +4 -0
- package/lib/terminalNotifications/index.d.ts +15 -0
- package/lib/terminalNotifications/index.js +213 -0
- package/lib/terminals/model.d.ts +1 -1
- package/lib/terminals/model.js +1 -1
- package/lib/terminals/widget.d.ts +1 -1
- package/lib/terminals/widget.js +1 -1
- package/package.json +3 -3
- package/schema/plugin.json +6 -0
- package/schema/terminal-notifications.json +21 -0
- package/src/fileBrowser/commands.ts +91 -2
- package/src/fileBrowser/fileBrowser.tsx +4 -1
- package/src/fileBrowser/icons.ts +103 -13
- package/src/fileBrowser/index.ts +9 -3
- package/src/fileTypeIcons/index.ts +174 -0
- package/src/git/commands.ts +177 -43
- package/src/git/diffModel.ts +102 -0
- package/src/git/diffProvider.tsx +8 -540
- package/src/git/diffSurface.tsx +44 -28
- package/src/git/diffTheme.ts +40 -0
- package/src/git/diffWidget.tsx +256 -448
- package/src/git/index.ts +14 -28
- package/src/git/notebookDiff.tsx +10 -3
- package/src/index.ts +4 -0
- package/src/terminalNotifications/index.ts +295 -0
- package/src/terminals/model.ts +1 -1
- package/src/terminals/widget.tsx +1 -1
- package/style/git.css +12 -9
- package/style/index.css +1 -0
- package/style/index.js +1 -0
- package/style/launcher.css +46 -18
- package/style/sidebar.css +14 -0
package/src/git/index.ts
CHANGED
|
@@ -5,14 +5,15 @@ import {
|
|
|
5
5
|
import { IThemeManager, WidgetTracker } from '@jupyterlab/apputils';
|
|
6
6
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
7
7
|
|
|
8
|
-
import { CommandArguments, CommandIDs, registerGitCommands } from './commands';
|
|
9
8
|
import {
|
|
10
|
-
|
|
9
|
+
CommandArguments,
|
|
10
|
+
CommandIDs,
|
|
11
|
+
DiffMainAreaWidget,
|
|
11
12
|
PREVIEW_DIFF_WIDGET_ID,
|
|
12
|
-
pinnedDiffWidgetId
|
|
13
|
-
|
|
13
|
+
pinnedDiffWidgetId,
|
|
14
|
+
registerGitCommands
|
|
15
|
+
} from './commands';
|
|
14
16
|
import diffProviderPlugin from './diffProvider';
|
|
15
|
-
import type { MainAreaWidget } from '@jupyterlab/apputils';
|
|
16
17
|
import { IFileChange } from './tokens';
|
|
17
18
|
|
|
18
19
|
const GIT_DIFF_COMMAND_PLUGIN_ID = 'xtralab:git-diff-command';
|
|
@@ -21,12 +22,8 @@ const GIT_DIFF_TRACKER_NAMESPACE = 'xtralab-git-diff';
|
|
|
21
22
|
/**
|
|
22
23
|
* The launcher's git diff command plugin.
|
|
23
24
|
*
|
|
24
|
-
* The
|
|
25
|
-
*
|
|
26
|
-
* user sees in the sidebar. This plugin is the launcher dashboard's
|
|
27
|
-
* independent diff path: the `xtralab:git:open-diff` command the
|
|
28
|
-
* dashboard's "Changes" section calls, plus a tracker so reopening a diff
|
|
29
|
-
* reveals the existing tab and the preview/pin behavior keeps working.
|
|
25
|
+
* The upstream git panel stays enabled; this plugin only gives the launcher
|
|
26
|
+
* its own `xtralab:git:open-diff` command and preview/pinned-tab tracking.
|
|
30
27
|
*/
|
|
31
28
|
const diffCommandPlugin: JupyterFrontEndPlugin<void> = {
|
|
32
29
|
id: GIT_DIFF_COMMAND_PLUGIN_ID,
|
|
@@ -39,17 +36,15 @@ const diffCommandPlugin: JupyterFrontEndPlugin<void> = {
|
|
|
39
36
|
themeManager: IThemeManager | null,
|
|
40
37
|
rendermime: IRenderMimeRegistry | null
|
|
41
38
|
): void => {
|
|
42
|
-
// Track open
|
|
43
|
-
|
|
44
|
-
// (and so a promoted preview can find its pinned twin).
|
|
45
|
-
const tracker = new WidgetTracker<MainAreaWidget<DiffContentWidget>>({
|
|
39
|
+
// Track open diffs so repeated opens reveal the existing tab.
|
|
40
|
+
const tracker = new WidgetTracker<DiffMainAreaWidget>({
|
|
46
41
|
namespace: GIT_DIFF_TRACKER_NAMESPACE
|
|
47
42
|
});
|
|
48
43
|
|
|
49
44
|
const findDiff = (
|
|
50
45
|
change: IFileChange,
|
|
51
46
|
pin = false
|
|
52
|
-
):
|
|
47
|
+
): DiffMainAreaWidget | undefined => {
|
|
53
48
|
const id = pin ? pinnedDiffWidgetId(change) : PREVIEW_DIFF_WIDGET_ID;
|
|
54
49
|
const existing = tracker.find(
|
|
55
50
|
widget => !widget.isDisposed && widget.id === id
|
|
@@ -62,13 +57,9 @@ const diffCommandPlugin: JupyterFrontEndPlugin<void> = {
|
|
|
62
57
|
themeManager,
|
|
63
58
|
contentsManager: app.serviceManager.contents,
|
|
64
59
|
rendermime,
|
|
65
|
-
// The launcher polls git status on its own cadence and the
|
|
66
|
-
// jupyterlab-git panel auto-refreshes on the contents `fileChanged`
|
|
67
|
-
// signal a hunk-discard save emits, so no extra refresh is needed.
|
|
68
|
-
onChanged: () => undefined,
|
|
69
60
|
trackDiff: widget => tracker.add(widget),
|
|
70
61
|
onPinned: current => {
|
|
71
|
-
const existing = findDiff(current.
|
|
62
|
+
const existing = findDiff(current.change, true);
|
|
72
63
|
if (
|
|
73
64
|
existing !== undefined &&
|
|
74
65
|
existing !== current &&
|
|
@@ -78,7 +69,7 @@ const diffCommandPlugin: JupyterFrontEndPlugin<void> = {
|
|
|
78
69
|
current.close();
|
|
79
70
|
return;
|
|
80
71
|
}
|
|
81
|
-
current.id = pinnedDiffWidgetId(current.
|
|
72
|
+
current.id = pinnedDiffWidgetId(current.change);
|
|
82
73
|
current.title.className = '';
|
|
83
74
|
app.shell.activateById(current.id);
|
|
84
75
|
},
|
|
@@ -88,12 +79,7 @@ const diffCommandPlugin: JupyterFrontEndPlugin<void> = {
|
|
|
88
79
|
};
|
|
89
80
|
|
|
90
81
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* 1. {@link diffCommandPlugin} — the launcher's `xtralab:git:open-diff`.
|
|
94
|
-
* 2. `diffProviderPlugin` — registers the `@pierre/diffs` rendering as
|
|
95
|
-
* jupyterlab-git's diff providers (replacing its disabled
|
|
96
|
-
* notebook/text diff plugins).
|
|
82
|
+
* Plugins contributed by xtralab's git integration.
|
|
97
83
|
*/
|
|
98
84
|
const plugins: JupyterFrontEndPlugin<unknown>[] = [
|
|
99
85
|
diffCommandPlugin,
|
package/src/git/notebookDiff.tsx
CHANGED
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
type FileDiffMetadata
|
|
14
14
|
} from '@pierre/diffs';
|
|
15
15
|
|
|
16
|
+
import { resolveDiffTheme } from './diffTheme';
|
|
17
|
+
|
|
16
18
|
/**
|
|
17
19
|
* The CSS class added to the root of the notebook diff. Selectors that
|
|
18
20
|
* style cell sections, headers, and the per-cell diff blocks hang off this
|
|
@@ -151,7 +153,7 @@ function emptyNotebook(): INotebook {
|
|
|
151
153
|
* nbformat allows multiline string fields (`source`, stream `text`,
|
|
152
154
|
* `text/plain` outputs, …) to be either a single string or an array of
|
|
153
155
|
* strings. Normalize to one string so equality checks and diffs see the
|
|
154
|
-
* same shape regardless of how the writer chose to
|
|
156
|
+
* same shape regardless of how the writer chose to serialize it.
|
|
155
157
|
*/
|
|
156
158
|
export function joinMultiline(value: string | string[] | undefined): string {
|
|
157
159
|
if (value === undefined) {
|
|
@@ -836,6 +838,11 @@ function MarkdownPreviewSection(props: {
|
|
|
836
838
|
interface INotebookDiffViewProps {
|
|
837
839
|
diff: INotebookDiffResult;
|
|
838
840
|
dark: boolean;
|
|
841
|
+
/**
|
|
842
|
+
* Whether a Pierre JupyterLab theme is active; selects Pierre's rich
|
|
843
|
+
* highlighting vs the CSS-variable theme (see {@link resolveDiffTheme}).
|
|
844
|
+
*/
|
|
845
|
+
pierreTheme: boolean;
|
|
839
846
|
rendermime: IRenderMimeRegistry | null;
|
|
840
847
|
}
|
|
841
848
|
|
|
@@ -852,8 +859,8 @@ interface INotebookDiffViewProps {
|
|
|
852
859
|
export function NotebookDiffView(
|
|
853
860
|
props: INotebookDiffViewProps
|
|
854
861
|
): React.ReactElement {
|
|
855
|
-
const { diff, dark, rendermime } = props;
|
|
856
|
-
const theme: DiffsThemeNames = dark
|
|
862
|
+
const { diff, dark, pierreTheme, rendermime } = props;
|
|
863
|
+
const theme: DiffsThemeNames = resolveDiffTheme(dark, pierreTheme);
|
|
857
864
|
const summary = React.useMemo(() => summarize(diff.cells), [diff.cells]);
|
|
858
865
|
return (
|
|
859
866
|
<div className={NOTEBOOK_DIFF_CSS_CLASS}>
|
package/src/index.ts
CHANGED
|
@@ -2,12 +2,14 @@ import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
|
2
2
|
|
|
3
3
|
import agentSessionsPlugin from './agentSessions';
|
|
4
4
|
import fileBrowserPlugin from './fileBrowser';
|
|
5
|
+
import fileTypeIconsPlugin from './fileTypeIcons';
|
|
5
6
|
import editorBreadcrumbsPlugin from './editorBreadcrumbs';
|
|
6
7
|
import editorIndentPlugin from './editorIndent';
|
|
7
8
|
import gitPlugins from './git';
|
|
8
9
|
import launcherPlugins from './launcher';
|
|
9
10
|
import menusPlugin from './menus';
|
|
10
11
|
import sidebarPlugin from './sidebar';
|
|
12
|
+
import terminalNotificationsPlugin from './terminalNotifications';
|
|
11
13
|
import terminalsPlugin from './terminals';
|
|
12
14
|
import topBarPlugin from './topBar';
|
|
13
15
|
|
|
@@ -25,10 +27,12 @@ const plugins: JupyterFrontEndPlugin<unknown>[] = [
|
|
|
25
27
|
editorBreadcrumbsPlugin,
|
|
26
28
|
editorIndentPlugin,
|
|
27
29
|
fileBrowserPlugin,
|
|
30
|
+
fileTypeIconsPlugin,
|
|
28
31
|
...gitPlugins,
|
|
29
32
|
...launcherPlugins,
|
|
30
33
|
menusPlugin,
|
|
31
34
|
sidebarPlugin,
|
|
35
|
+
terminalNotificationsPlugin,
|
|
32
36
|
terminalsPlugin,
|
|
33
37
|
topBarPlugin
|
|
34
38
|
];
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JupyterFrontEnd,
|
|
3
|
+
JupyterFrontEndPlugin
|
|
4
|
+
} from '@jupyterlab/application';
|
|
5
|
+
import type { MainAreaWidget } from '@jupyterlab/apputils';
|
|
6
|
+
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
7
|
+
import { ITerminal, ITerminalTracker } from '@jupyterlab/terminal';
|
|
8
|
+
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
9
|
+
|
|
10
|
+
const PLUGIN_ID = 'xtralab:terminal-notifications';
|
|
11
|
+
|
|
12
|
+
// Per-terminal gap between notifications, so the OSC-9-plus-bell agents emit
|
|
13
|
+
// together collapses into one and a chatty program cannot flood the OS.
|
|
14
|
+
const NOTIFY_THROTTLE_MS = 3000;
|
|
15
|
+
|
|
16
|
+
// iTerm2 growl notification: `ESC ] 9 ; <message> BEL`. What Claude Code and
|
|
17
|
+
// most agents emit on their default `auto` setting once TERM_PROGRAM is set.
|
|
18
|
+
const OSC_ITERM2_GROWL = 9;
|
|
19
|
+
|
|
20
|
+
// rxvt/Ghostty notification: `ESC ] 777 ; notify ; <title> ; <body> BEL`.
|
|
21
|
+
const OSC_RXVT_NOTIFY = 777;
|
|
22
|
+
|
|
23
|
+
const MAX_TEXT_LENGTH = 256;
|
|
24
|
+
|
|
25
|
+
// The bits of an xterm.js `Terminal` we reach into. JupyterLab's terminal
|
|
26
|
+
// widget keeps its xterm in a private `_term` field and does not expose these
|
|
27
|
+
// hooks, so a structural type uses them without depending on `@xterm/xterm`. If
|
|
28
|
+
// the field is ever renamed, `_term` is `undefined` and notifications stop, with
|
|
29
|
+
// a console warning per terminal.
|
|
30
|
+
interface IXtermDisposable {
|
|
31
|
+
dispose(): void;
|
|
32
|
+
}
|
|
33
|
+
interface IXtermTerminal {
|
|
34
|
+
onBell(handler: () => void): IXtermDisposable;
|
|
35
|
+
parser: {
|
|
36
|
+
registerOscHandler(
|
|
37
|
+
ident: number,
|
|
38
|
+
callback: (data: string) => boolean
|
|
39
|
+
): IXtermDisposable;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
interface ITerminalContentInternals extends ITerminal.ITerminal {
|
|
43
|
+
ready: Promise<void>;
|
|
44
|
+
_term?: IXtermTerminal;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type TerminalWidget = MainAreaWidget<ITerminal.ITerminal>;
|
|
48
|
+
|
|
49
|
+
// The renderer→main bridge the desktop shell injects on the lab window. Absent
|
|
50
|
+
// for pip-install users in a browser, who fall back to web Notifications.
|
|
51
|
+
interface IDesktopBridge {
|
|
52
|
+
notify?: (
|
|
53
|
+
title: string,
|
|
54
|
+
body: string,
|
|
55
|
+
session?: string
|
|
56
|
+
) => Promise<void> | void;
|
|
57
|
+
onFocusTerminal?: (callback: (session: string) => void) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Turns the notifications coding agents already emit (OSC 9, OSC 777, the bell)
|
|
62
|
+
* into desktop notifications. JupyterLab's xterm renders these sequences but
|
|
63
|
+
* never forwards them to the OS, so this plugin hooks each terminal and bridges
|
|
64
|
+
* them through `window.xtralab.notify` (desktop) or the web Notifications API.
|
|
65
|
+
*
|
|
66
|
+
* A notification is suppressed while its terminal is the focused, active tab,
|
|
67
|
+
* and throttled per terminal. The desktop shell advertises
|
|
68
|
+
* `TERM_PROGRAM=iTerm.app` so agents emit OSC 9 in the first place, and the
|
|
69
|
+
* session name is forwarded so clicking a notification focuses the terminal
|
|
70
|
+
* that fired it.
|
|
71
|
+
*/
|
|
72
|
+
const plugin: JupyterFrontEndPlugin<void> = {
|
|
73
|
+
id: PLUGIN_ID,
|
|
74
|
+
description:
|
|
75
|
+
'Forwards desktop notifications emitted by agents in xtralab terminals (OSC 9, OSC 777, bell) to the operating system.',
|
|
76
|
+
autoStart: true,
|
|
77
|
+
requires: [ITerminalTracker],
|
|
78
|
+
optional: [ISettingRegistry, ITranslator],
|
|
79
|
+
activate: async (
|
|
80
|
+
app: JupyterFrontEnd,
|
|
81
|
+
tracker: ITerminalTracker,
|
|
82
|
+
settingRegistry: ISettingRegistry | null,
|
|
83
|
+
translator: ITranslator | null
|
|
84
|
+
): Promise<void> => {
|
|
85
|
+
const trans = (translator ?? nullTranslator).load('jupyterlab');
|
|
86
|
+
|
|
87
|
+
let enabled = true;
|
|
88
|
+
let notifyOnBell = true;
|
|
89
|
+
|
|
90
|
+
// Per-widget throttle timestamps and xterm hooks. WeakMaps let disposed
|
|
91
|
+
// widgets be collected; hooks are torn down when a tab closes.
|
|
92
|
+
const lastNotified = new WeakMap<TerminalWidget, number>();
|
|
93
|
+
const hooks = new WeakMap<TerminalWidget, IXtermDisposable[]>();
|
|
94
|
+
|
|
95
|
+
// True when the user is already looking at this terminal, so a banner would
|
|
96
|
+
// just be noise.
|
|
97
|
+
const isActivelyViewing = (widget: TerminalWidget): boolean =>
|
|
98
|
+
document.hasFocus() && app.shell.currentWidget === widget;
|
|
99
|
+
|
|
100
|
+
const deliver = (title: string, body: string, session?: string): void => {
|
|
101
|
+
const bridge = (window as Window & { xtralab?: IDesktopBridge }).xtralab;
|
|
102
|
+
if (bridge && typeof bridge.notify === 'function') {
|
|
103
|
+
void Promise.resolve(bridge.notify(title, body, session)).catch(
|
|
104
|
+
reason => {
|
|
105
|
+
console.warn('xtralab: desktop notification failed', reason);
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Plain-browser fallback (pip install): the Web Notifications API.
|
|
111
|
+
if (typeof Notification === 'undefined') {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (Notification.permission === 'granted') {
|
|
115
|
+
new Notification(title, { body });
|
|
116
|
+
} else if (Notification.permission !== 'denied') {
|
|
117
|
+
void Notification.requestPermission().then(permission => {
|
|
118
|
+
if (permission === 'granted') {
|
|
119
|
+
new Notification(title, { body });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const notify = (
|
|
126
|
+
widget: TerminalWidget,
|
|
127
|
+
title: string,
|
|
128
|
+
body: string
|
|
129
|
+
): void => {
|
|
130
|
+
if (!enabled || isActivelyViewing(widget)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const now = Date.now();
|
|
134
|
+
const previous = lastNotified.get(widget);
|
|
135
|
+
if (previous !== undefined && now - previous < NOTIFY_THROTTLE_MS) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
lastNotified.set(widget, now);
|
|
139
|
+
const session = widget.content.session?.name;
|
|
140
|
+
deliver(sanitize(title) || 'xtralab', sanitize(body), session);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// The tab label is the agent's name once the launcher or an xterm title sets
|
|
144
|
+
// it, so it reads well as the title for body-only sequences (OSC 9, bell).
|
|
145
|
+
const labelOf = (widget: TerminalWidget): string =>
|
|
146
|
+
widget.title.label || trans.__('Terminal');
|
|
147
|
+
|
|
148
|
+
const onOsc9 = (widget: TerminalWidget, data: string): boolean => {
|
|
149
|
+
// OSC 9 is overloaded: ConEmu/Windows Terminal use `9 ; <n> ; …`
|
|
150
|
+
// subcommands (e.g. `9 ; 4` progress), so skip a numeric-subcommand
|
|
151
|
+
// payload and leave it unconsumed rather than treat it as a notification.
|
|
152
|
+
if (/^\d+(;|$)/.test(data)) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
const message = sanitize(data);
|
|
156
|
+
if (message) {
|
|
157
|
+
notify(widget, labelOf(widget), message);
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const onOsc777 = (widget: TerminalWidget, data: string): boolean => {
|
|
163
|
+
// `notify ; <title> ; <body>`; a body may contain ';'. Other OSC 777
|
|
164
|
+
// subcommands are left unconsumed.
|
|
165
|
+
const parts = data.split(';');
|
|
166
|
+
if (parts.shift() !== 'notify') {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
const title = sanitize(parts.shift() ?? '') || labelOf(widget);
|
|
170
|
+
const body = sanitize(parts.join(';'));
|
|
171
|
+
if (body || title) {
|
|
172
|
+
notify(widget, title, body);
|
|
173
|
+
}
|
|
174
|
+
return true;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const onBell = (widget: TerminalWidget): void => {
|
|
178
|
+
if (notifyOnBell) {
|
|
179
|
+
notify(widget, labelOf(widget), trans.__('Activity in terminal'));
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const hookWidget = (widget: TerminalWidget): void => {
|
|
184
|
+
if (hooks.has(widget)) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const content = widget.content as ITerminalContentInternals;
|
|
188
|
+
void content.ready
|
|
189
|
+
.then(() => {
|
|
190
|
+
const term = content._term;
|
|
191
|
+
if (widget.isDisposed) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (!term) {
|
|
195
|
+
console.warn(
|
|
196
|
+
'xtralab: xterm internals not found; terminal notifications are disabled for this terminal'
|
|
197
|
+
);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// Handlers are wrapped so a parse slip cannot break xterm's parser; an
|
|
201
|
+
// OSC handler returns whether it consumed the sequence.
|
|
202
|
+
const disposables: IXtermDisposable[] = [
|
|
203
|
+
term.onBell(() => guard(() => onBell(widget))),
|
|
204
|
+
term.parser.registerOscHandler(OSC_ITERM2_GROWL, data =>
|
|
205
|
+
guardOsc(() => onOsc9(widget, data))
|
|
206
|
+
),
|
|
207
|
+
term.parser.registerOscHandler(OSC_RXVT_NOTIFY, data =>
|
|
208
|
+
guardOsc(() => onOsc777(widget, data))
|
|
209
|
+
)
|
|
210
|
+
];
|
|
211
|
+
hooks.set(widget, disposables);
|
|
212
|
+
widget.disposed.connect(() => {
|
|
213
|
+
for (const disposable of disposables) {
|
|
214
|
+
disposable.dispose();
|
|
215
|
+
}
|
|
216
|
+
hooks.delete(widget);
|
|
217
|
+
});
|
|
218
|
+
})
|
|
219
|
+
.catch(reason => {
|
|
220
|
+
console.warn(
|
|
221
|
+
'xtralab: could not hook terminal notifications',
|
|
222
|
+
reason
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
tracker.forEach(hookWidget);
|
|
228
|
+
tracker.widgetAdded.connect((_, widget) => hookWidget(widget));
|
|
229
|
+
|
|
230
|
+
// Focus the terminal that fired a notification when the desktop shell
|
|
231
|
+
// reports a click. `terminal:open` activates its tab or reopens the session.
|
|
232
|
+
const desktopBridge = (window as Window & { xtralab?: IDesktopBridge })
|
|
233
|
+
.xtralab;
|
|
234
|
+
desktopBridge?.onFocusTerminal?.(session => {
|
|
235
|
+
if (session) {
|
|
236
|
+
void app.commands.execute('terminal:open', { name: session });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (settingRegistry) {
|
|
241
|
+
try {
|
|
242
|
+
const settings = await settingRegistry.load(PLUGIN_ID);
|
|
243
|
+
const readSettings = (): void => {
|
|
244
|
+
enabled = boolOption(settings.composite.enabled, true);
|
|
245
|
+
notifyOnBell = boolOption(settings.composite.notifyOnBell, true);
|
|
246
|
+
};
|
|
247
|
+
readSettings();
|
|
248
|
+
settings.changed.connect(readSettings);
|
|
249
|
+
} catch (reason) {
|
|
250
|
+
console.error(
|
|
251
|
+
'xtralab: failed to load terminal-notifications settings',
|
|
252
|
+
reason
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// Run `fn`, swallowing errors so a malformed escape sequence cannot take the
|
|
260
|
+
// terminal's parser or bell handler down with it.
|
|
261
|
+
function guard(fn: () => void): void {
|
|
262
|
+
try {
|
|
263
|
+
fn();
|
|
264
|
+
} catch (reason) {
|
|
265
|
+
console.warn('xtralab: terminal notification handler failed', reason);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// `guard` for an OSC handler that reports whether it consumed the sequence.
|
|
270
|
+
function guardOsc(fn: () => boolean): boolean {
|
|
271
|
+
try {
|
|
272
|
+
return fn();
|
|
273
|
+
} catch (reason) {
|
|
274
|
+
console.warn('xtralab: terminal notification handler failed', reason);
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Replace control characters with spaces (so a notification cannot carry escape
|
|
280
|
+
// sequences), collapse whitespace, and clamp the length.
|
|
281
|
+
function sanitize(value: string): string {
|
|
282
|
+
let result = '';
|
|
283
|
+
for (const char of value) {
|
|
284
|
+
const code = char.codePointAt(0) ?? 0;
|
|
285
|
+
const isControl = code < 0x20 || (code >= 0x7f && code <= 0x9f);
|
|
286
|
+
result += isControl ? ' ' : char;
|
|
287
|
+
}
|
|
288
|
+
return result.replace(/\s+/g, ' ').trim().slice(0, MAX_TEXT_LENGTH);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function boolOption(value: unknown, fallback: boolean): boolean {
|
|
292
|
+
return typeof value === 'boolean' ? value : fallback;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export default plugin;
|
package/src/terminals/model.ts
CHANGED
|
@@ -194,7 +194,7 @@ export class SessionRegistry implements IDisposable {
|
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
196
|
* Return the open widget for a session, if any. The panel uses this to
|
|
197
|
-
* switch
|
|
197
|
+
* switch behavior between "activate existing tab" and "open a new tab
|
|
198
198
|
* connected to the live session".
|
|
199
199
|
*/
|
|
200
200
|
widgetFor(name: string): TerminalWidget | null {
|
package/src/terminals/widget.tsx
CHANGED
|
@@ -32,7 +32,7 @@ export const RUNNING_TERMINALS_ID = 'xtralab-running-terminals';
|
|
|
32
32
|
* Backed by {@link SessionRegistry}: a `UseSignal` re-renders the list
|
|
33
33
|
* whenever the registry emits `stateChanged`, so it tracks
|
|
34
34
|
* `runningChanged` and the agent-title cache without any local state.
|
|
35
|
-
* Click
|
|
35
|
+
* Click behavior (activate vs. reopen, shutdown, new terminal) is
|
|
36
36
|
* delegated to plugin-supplied callbacks so the widget never imports
|
|
37
37
|
* `app`.
|
|
38
38
|
*/
|
package/style/git.css
CHANGED
|
@@ -40,12 +40,7 @@
|
|
|
40
40
|
color: var(--jp-ui-font-color1);
|
|
41
41
|
|
|
42
42
|
/*
|
|
43
|
-
*
|
|
44
|
-
* properties the library exposes. The `*-override` variants take
|
|
45
|
-
* precedence over the library's defaults, so these values track the
|
|
46
|
-
* JupyterLab theme automatically — no JS needed when the theme
|
|
47
|
-
* switches. Theme-dependent colors (insertion green, deletion red, …)
|
|
48
|
-
* still come from the active Shiki theme passed via `options.theme`.
|
|
43
|
+
* Feed JupyterLab theme colors into @pierre/diffs.
|
|
49
44
|
*/
|
|
50
45
|
--diffs-bg-override: var(--jp-layout-color1);
|
|
51
46
|
--diffs-bg-buffer-override: var(--jp-layout-color1);
|
|
@@ -58,6 +53,8 @@
|
|
|
58
53
|
--diffs-fg-number-override: var(--jp-ui-font-color3);
|
|
59
54
|
--diffs-fg-conflict-marker-override: var(--jp-warn-color1);
|
|
60
55
|
--diffs-modified-color-override: var(--jp-warn-color1);
|
|
56
|
+
--diffs-addition-color-override: var(--jp-success-color1);
|
|
57
|
+
--diffs-deletion-color-override: var(--jp-error-color1);
|
|
61
58
|
}
|
|
62
59
|
|
|
63
60
|
/*
|
|
@@ -91,7 +88,9 @@
|
|
|
91
88
|
font-size: var(--jp-ui-font-size0);
|
|
92
89
|
}
|
|
93
90
|
|
|
94
|
-
/*
|
|
91
|
+
/*
|
|
92
|
+
* Breathing room when both toggles appear (notebook diff in JSON view).
|
|
93
|
+
*/
|
|
95
94
|
.jp-xtralab-DiffWidget-diffStyleToolbarItem {
|
|
96
95
|
margin-left: 4px;
|
|
97
96
|
}
|
|
@@ -589,7 +588,9 @@
|
|
|
589
588
|
|
|
590
589
|
.jp-xtralab-ImageDiff-frame,
|
|
591
590
|
.jp-xtralab-ImageDiff-stack {
|
|
592
|
-
/*
|
|
591
|
+
/*
|
|
592
|
+
* Checkerboard so transparent regions are obvious; theme-neutral.
|
|
593
|
+
*/
|
|
593
594
|
background-color: #fff;
|
|
594
595
|
background-image:
|
|
595
596
|
linear-gradient(45deg, #c8c8c8 25%, transparent 25%),
|
|
@@ -766,7 +767,9 @@
|
|
|
766
767
|
bottom: -14px;
|
|
767
768
|
}
|
|
768
769
|
|
|
769
|
-
/*
|
|
770
|
+
/*
|
|
771
|
+
* Chevron triangles drawn with borders so no asset is needed.
|
|
772
|
+
*/
|
|
770
773
|
.jp-xtralab-ImageDiff-swipeDividerArrow {
|
|
771
774
|
width: 0;
|
|
772
775
|
height: 0;
|
package/style/index.css
CHANGED
package/style/index.js
CHANGED
package/style/launcher.css
CHANGED
|
@@ -166,8 +166,8 @@
|
|
|
166
166
|
|
|
167
167
|
/*
|
|
168
168
|
* Changed files list. Each row is a button so click + keyboard activate
|
|
169
|
-
* both work; the
|
|
170
|
-
*
|
|
169
|
+
* both work; the letter on the left is the file's single-letter git status,
|
|
170
|
+
* colored to match the @pierre/trees file tree's decorations.
|
|
171
171
|
*/
|
|
172
172
|
.jp-xtralab-Launcher-changes {
|
|
173
173
|
list-style: none;
|
|
@@ -178,6 +178,27 @@
|
|
|
178
178
|
border: 1px solid var(--jp-border-color2);
|
|
179
179
|
border-radius: 4px;
|
|
180
180
|
overflow: hidden;
|
|
181
|
+
|
|
182
|
+
/*
|
|
183
|
+
* Git status letter palette, mirroring the @pierre/trees `--trees-status-*`
|
|
184
|
+
* defaults. The file tree resolves those through `light-dark()` under
|
|
185
|
+
* `color-scheme: light dark` — i.e. by OS appearance — so the
|
|
186
|
+
* `prefers-color-scheme` override below reproduces that exact resolution
|
|
187
|
+
* and keeps these letters in lockstep with the tree's.
|
|
188
|
+
*/
|
|
189
|
+
--xtralab-git-status-added: #16a994;
|
|
190
|
+
--xtralab-git-status-modified: #1ca1c7;
|
|
191
|
+
--xtralab-git-status-renamed: #d5a910;
|
|
192
|
+
--xtralab-git-status-deleted: #ff2e3f;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@media (prefers-color-scheme: dark) {
|
|
196
|
+
.jp-xtralab-Launcher-changes {
|
|
197
|
+
--xtralab-git-status-added: #00cab1;
|
|
198
|
+
--xtralab-git-status-modified: #08c0ef;
|
|
199
|
+
--xtralab-git-status-renamed: #ffd452;
|
|
200
|
+
--xtralab-git-status-deleted: #ff6762;
|
|
201
|
+
}
|
|
181
202
|
}
|
|
182
203
|
|
|
183
204
|
.jp-xtralab-Launcher-changes > li + li {
|
|
@@ -207,35 +228,42 @@
|
|
|
207
228
|
outline-offset: -1px;
|
|
208
229
|
}
|
|
209
230
|
|
|
231
|
+
/*
|
|
232
|
+
* Bare colored letter, not a filled badge — the same treatment the
|
|
233
|
+
* @pierre/trees file tree gives its git status decorations. The fixed width
|
|
234
|
+
* keeps the letters in a tidy gutter so the paths stay column-aligned.
|
|
235
|
+
*/
|
|
210
236
|
.jp-xtralab-Launcher-change-badge {
|
|
211
237
|
display: inline-flex;
|
|
212
238
|
align-items: center;
|
|
213
239
|
justify-content: center;
|
|
214
|
-
width:
|
|
215
|
-
|
|
216
|
-
font-size: 10px;
|
|
217
|
-
font-weight: 700;
|
|
218
|
-
border-radius: 3px;
|
|
240
|
+
width: 14px;
|
|
241
|
+
flex: 0 0 auto;
|
|
219
242
|
font-family: var(--jp-code-font-family);
|
|
220
|
-
|
|
243
|
+
font-size: var(--jp-code-font-size);
|
|
244
|
+
font-weight: 600;
|
|
221
245
|
color: var(--jp-ui-font-color2);
|
|
222
|
-
flex: 0 0 auto;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
.jp-xtralab-Launcher-change-modified {
|
|
226
|
-
background-color: var(--jp-warn-color3, var(--jp-layout-color2));
|
|
227
|
-
color: var(--jp-warn-color1, var(--jp-ui-font-color1));
|
|
228
246
|
}
|
|
229
247
|
|
|
230
248
|
.jp-xtralab-Launcher-change-added,
|
|
231
249
|
.jp-xtralab-Launcher-change-untracked {
|
|
232
|
-
|
|
233
|
-
|
|
250
|
+
color: var(--xtralab-git-status-added);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* The file tree folds unmerged/typechange/unknown onto modified; match it. */
|
|
254
|
+
.jp-xtralab-Launcher-change-modified,
|
|
255
|
+
.jp-xtralab-Launcher-change-unmerged,
|
|
256
|
+
.jp-xtralab-Launcher-change-typechange,
|
|
257
|
+
.jp-xtralab-Launcher-change-unknown {
|
|
258
|
+
color: var(--xtralab-git-status-modified);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.jp-xtralab-Launcher-change-renamed {
|
|
262
|
+
color: var(--xtralab-git-status-renamed);
|
|
234
263
|
}
|
|
235
264
|
|
|
236
265
|
.jp-xtralab-Launcher-change-deleted {
|
|
237
|
-
|
|
238
|
-
color: var(--jp-error-color1, var(--jp-ui-font-color1));
|
|
266
|
+
color: var(--xtralab-git-status-deleted);
|
|
239
267
|
}
|
|
240
268
|
|
|
241
269
|
.jp-xtralab-Launcher-change-path {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The side activity bar is shipped horizontally (activityBarPosition: "top",
|
|
3
|
+
* see jupyter-config/labconfig). In that mode the TabBar content node spans
|
|
4
|
+
* the full width (@jupyterlab/application/style/sidepanel.css) with no
|
|
5
|
+
* justify-content, so the icons sit at the leading edge. Center them under
|
|
6
|
+
* the menu bar instead.
|
|
7
|
+
*
|
|
8
|
+
* `safe center` keeps the row start-aligned should the icons ever exceed the
|
|
9
|
+
* bar width, so none are pushed past the leading edge out of reach of the
|
|
10
|
+
* content node's horizontal overflow scroll.
|
|
11
|
+
*/
|
|
12
|
+
.jp-SideBar[data-orientation='horizontal'] .lm-TabBar-content {
|
|
13
|
+
justify-content: safe center;
|
|
14
|
+
}
|