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.
Files changed (57) hide show
  1. package/README.md +30 -6
  2. package/lib/fileBrowser/commands.d.ts +7 -0
  3. package/lib/fileBrowser/commands.js +74 -1
  4. package/lib/fileBrowser/fileBrowser.js +4 -1
  5. package/lib/fileBrowser/icons.d.ts +5 -9
  6. package/lib/fileBrowser/icons.js +97 -13
  7. package/lib/fileBrowser/index.js +5 -3
  8. package/lib/fileTypeIcons/index.d.ts +6 -0
  9. package/lib/fileTypeIcons/index.js +144 -0
  10. package/lib/git/commands.d.ts +34 -33
  11. package/lib/git/commands.js +132 -10
  12. package/lib/git/diffModel.d.ts +14 -0
  13. package/lib/git/diffModel.js +75 -0
  14. package/lib/git/diffProvider.d.ts +0 -5
  15. package/lib/git/diffProvider.js +6 -376
  16. package/lib/git/diffSurface.d.ts +36 -26
  17. package/lib/git/diffSurface.js +13 -20
  18. package/lib/git/diffTheme.d.ts +9 -0
  19. package/lib/git/diffTheme.js +31 -0
  20. package/lib/git/diffWidget.d.ts +54 -91
  21. package/lib/git/diffWidget.js +184 -361
  22. package/lib/git/index.d.ts +1 -6
  23. package/lib/git/index.js +7 -23
  24. package/lib/git/notebookDiff.d.ts +6 -1
  25. package/lib/git/notebookDiff.js +4 -3
  26. package/lib/index.js +4 -0
  27. package/lib/terminalNotifications/index.d.ts +15 -0
  28. package/lib/terminalNotifications/index.js +213 -0
  29. package/lib/terminals/model.d.ts +1 -1
  30. package/lib/terminals/model.js +1 -1
  31. package/lib/terminals/widget.d.ts +1 -1
  32. package/lib/terminals/widget.js +1 -1
  33. package/package.json +3 -3
  34. package/schema/plugin.json +6 -0
  35. package/schema/terminal-notifications.json +21 -0
  36. package/src/fileBrowser/commands.ts +91 -2
  37. package/src/fileBrowser/fileBrowser.tsx +4 -1
  38. package/src/fileBrowser/icons.ts +103 -13
  39. package/src/fileBrowser/index.ts +9 -3
  40. package/src/fileTypeIcons/index.ts +174 -0
  41. package/src/git/commands.ts +177 -43
  42. package/src/git/diffModel.ts +102 -0
  43. package/src/git/diffProvider.tsx +8 -540
  44. package/src/git/diffSurface.tsx +44 -28
  45. package/src/git/diffTheme.ts +40 -0
  46. package/src/git/diffWidget.tsx +256 -448
  47. package/src/git/index.ts +14 -28
  48. package/src/git/notebookDiff.tsx +10 -3
  49. package/src/index.ts +4 -0
  50. package/src/terminalNotifications/index.ts +295 -0
  51. package/src/terminals/model.ts +1 -1
  52. package/src/terminals/widget.tsx +1 -1
  53. package/style/git.css +12 -9
  54. package/style/index.css +1 -0
  55. package/style/index.js +1 -0
  56. package/style/launcher.css +46 -18
  57. 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
- DiffContentWidget,
9
+ CommandArguments,
10
+ CommandIDs,
11
+ DiffMainAreaWidget,
11
12
  PREVIEW_DIFF_WIDGET_ID,
12
- pinnedDiffWidgetId
13
- } from './diffWidget';
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 `@jupyterlab/git` frontend stays enabled (only its diff plugins are
25
- * swapped — see `diffProvider.tsx`), so the upstream git panel is what the
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 diff widgets so the openDiff handler can reveal an
43
- // already-open diff for the same file instead of creating a duplicate
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
- ): MainAreaWidget<DiffContentWidget> | undefined => {
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.content.change, true);
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.content.change);
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
- * Every plugin contributed by xtralab's git integration:
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,
@@ -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 serialise it.
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 ? 'pierre-dark' : 'pierre-light';
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;
@@ -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 behaviour between "activate existing tab" and "open a new tab
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 {
@@ -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 behaviour (activate vs. reopen, shutdown, new terminal) is
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
- * Map JupyterLab's theme variables onto the `--diffs-*-override`
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
- /* Breathing room when both toggles appear (notebook diff in JSON view). */
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
- /* Checkerboard so transparent regions are obvious; theme-neutral. */
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
- /* Chevron triangles drawn with borders so no asset is needed. */
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
@@ -2,6 +2,7 @@
2
2
  @import url('editorBreadcrumbs.css');
3
3
  @import url('git.css');
4
4
  @import url('launcher.css');
5
+ @import url('sidebar.css');
5
6
  @import url('tabs.css');
6
7
  @import url('terminals.css');
7
8
  @import url('topBar.css');
package/style/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import './base.css';
2
2
  import './git.css';
3
3
  import './launcher.css';
4
+ import './sidebar.css';
4
5
  import './tabs.css';
5
6
  import './terminals.css';
6
7
  import './topBar.css';
@@ -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 badge on the left mirrors the single-letter status
170
- * indicator the git panel uses, kept in sync via FileChangeStatus.
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: 18px;
215
- height: 18px;
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
- background-color: var(--jp-layout-color2);
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
- background-color: var(--jp-success-color3, var(--jp-layout-color2));
233
- color: var(--jp-success-color1, var(--jp-ui-font-color1));
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
- background-color: var(--jp-error-color3, var(--jp-layout-color2));
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
+ }