xtralab 0.3.0 → 0.4.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.
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
2
 
3
+ import { Poll } from '@lumino/polling';
3
4
  import { ISignal } from '@lumino/signaling';
4
5
 
5
6
  import {
@@ -49,6 +50,14 @@ interface IPanelState {
49
50
  */
50
51
  const STATUS_POLL_INTERVAL_MS = 5000;
51
52
 
53
+ /**
54
+ * Upper bound on the poll's exponential backoff when status requests fail
55
+ * repeatedly. Matches the default JupyterLab file browser's `max`: long
56
+ * enough that a server outage stops hammering the API, short enough that
57
+ * a transient failure heals on its own.
58
+ */
59
+ const STATUS_POLL_MAX_MS = 300_000;
60
+
52
61
  /**
53
62
  * Top-level React component rendered inside the {@link GitPanel} widget.
54
63
  * Owns the polling + refresh wiring around the `/git/status` endpoint and
@@ -109,27 +118,32 @@ export function GitPanelComponent(props: {
109
118
  }
110
119
  }, [repoPath]);
111
120
 
112
- // Initial load + polling. `refresh` is stable for a given `repoPath`, so
113
- // the polling interval is only re-created when the repo path changes.
114
- React.useEffect(() => {
115
- void refresh();
116
- const interval = window.setInterval(
117
- () => void refresh(),
118
- STATUS_POLL_INTERVAL_MS
119
- );
120
- return () => {
121
- window.clearInterval(interval);
122
- };
123
- }, [refresh]);
124
-
125
- // External refresh triggers (toolbar button, plugin-level events).
121
+ // Initial load + polling. Driven by a Lumino `Poll` so the panel stops
122
+ // hitting the git endpoint while the JupyterLab tab is hidden, and so
123
+ // repeated failures back off exponentially instead of spinning at 5s.
124
+ // External refresh triggers (toolbar button, plugin-level events) call
125
+ // `poll.refresh()` to schedule an immediate tick instead of running
126
+ // `refresh()` directly — that keeps the panel from issuing two parallel
127
+ // status requests when an automatic tick happens to land at the same
128
+ // time as an external trigger.
126
129
  React.useEffect(() => {
130
+ const poll = new Poll({
131
+ name: '@xtralab/git:panelStatus',
132
+ factory: () => refresh(),
133
+ frequency: {
134
+ interval: STATUS_POLL_INTERVAL_MS,
135
+ backoff: true,
136
+ max: STATUS_POLL_MAX_MS
137
+ },
138
+ standby: 'when-hidden'
139
+ });
127
140
  const handler = (): void => {
128
- void refresh();
141
+ void poll.refresh();
129
142
  };
130
143
  refreshSignal.connect(handler);
131
144
  return () => {
132
145
  refreshSignal.disconnect(handler);
146
+ poll.dispose();
133
147
  };
134
148
  }, [refresh, refreshSignal]);
135
149
 
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
2
 
3
3
  import fileBrowserPlugin from './fileBrowser';
4
+ import editorBreadcrumbsPlugin from './editorBreadcrumbs';
5
+ import editorIndentPlugin from './editorIndent';
4
6
  import gitPlugin from './git';
5
7
  import launcherPlugin from './launcher';
6
8
  import menusPlugin from './menus';
@@ -15,6 +17,8 @@ import statusBarPlugin from './statusBar';
15
17
  * available end up running.
16
18
  */
17
19
  const plugins: JupyterFrontEndPlugin<unknown>[] = [
20
+ editorBreadcrumbsPlugin,
21
+ editorIndentPlugin,
18
22
  fileBrowserPlugin,
19
23
  gitPlugin,
20
24
  launcherPlugin,
@@ -2,6 +2,7 @@ import * as React from 'react';
2
2
 
3
3
  import type { CommandRegistry } from '@lumino/commands';
4
4
  import type { ReadonlyPartialJSONObject } from '@lumino/coreutils';
5
+ import { Poll } from '@lumino/polling';
5
6
  import {
6
7
  LabIcon,
7
8
  ReactWidget,
@@ -90,6 +91,13 @@ interface IGitState {
90
91
  */
91
92
  const CHANGES_POLL_INTERVAL_MS = 8000;
92
93
 
94
+ /**
95
+ * Upper bound on the exponential backoff when the status request fails
96
+ * repeatedly. Matches the rest of the plugin's polls so behavior is
97
+ * consistent across the dashboard, the git panel, and the file tree.
98
+ */
99
+ const CHANGES_POLL_MAX_MS = 300_000;
100
+
93
101
  function LauncherDashboardComponent(
94
102
  props: ILauncherDashboardOptions
95
103
  ): React.ReactElement {
@@ -114,12 +122,23 @@ function LauncherDashboardComponent(
114
122
  }
115
123
  }, [repoPath]);
116
124
 
125
+ // Driven by a Lumino `Poll` so the dashboard stops hitting the git
126
+ // endpoint while the JupyterLab tab is hidden, and so repeated
127
+ // failures back off exponentially instead of pegging the 8s cadence.
117
128
  React.useEffect(() => {
118
- void refresh();
119
- const handle = window.setInterval(() => {
120
- void refresh();
121
- }, CHANGES_POLL_INTERVAL_MS);
122
- return () => window.clearInterval(handle);
129
+ const poll = new Poll({
130
+ name: '@xtralab/launcher:gitChanges',
131
+ factory: () => refresh(),
132
+ frequency: {
133
+ interval: CHANGES_POLL_INTERVAL_MS,
134
+ backoff: true,
135
+ max: CHANGES_POLL_MAX_MS
136
+ },
137
+ standby: 'when-hidden'
138
+ });
139
+ return () => {
140
+ poll.dispose();
141
+ };
123
142
  }, [refresh]);
124
143
 
125
144
  const launch = React.useCallback(
@@ -320,10 +339,9 @@ function AgentSection(props: {
320
339
  const { agents, prompt, onPromptChange, onLaunch } = props;
321
340
  const trimmed = prompt.trim();
322
341
  const promptActive = trimmed.length > 0;
323
- // The "primary" agent is the first prompt-capable one in the rendered
324
- // order. We mark it visually so the keyboard shortcut target isn't a
325
- // mystery, and reuse the same agent when Cmd/Ctrl+Enter fires from the
326
- // textarea — picking by the same rule keeps the two behaviors in sync.
342
+ // The first prompt-capable agent is what Cmd/Ctrl+Enter fires; the hint
343
+ // text below the textarea names it so the keyboard shortcut target
344
+ // isn't a mystery.
327
345
  const primaryAgent = agents.find(agent => agent.promptArgs !== undefined);
328
346
  const hintId = 'jp-xtralab-Launcher-agent-hint';
329
347
 
@@ -362,19 +380,14 @@ function AgentSection(props: {
362
380
  {agents.map(agent => {
363
381
  const supportsPrompt = agent.promptArgs !== undefined;
364
382
  const disabled = promptActive && !supportsPrompt;
365
- const isPrimary = agent === primaryAgent;
366
383
  const tooltip = disabled
367
384
  ? `${agent.label} doesn't accept an initial prompt — clear the prompt to launch.`
368
385
  : agent.caption;
369
- const classes = ['jp-xtralab-Launcher-agent'];
370
- if (isPrimary) {
371
- classes.push('jp-xtralab-Launcher-agent-primary');
372
- }
373
386
  return (
374
387
  <button
375
388
  key={agent.id}
376
389
  type="button"
377
- className={classes.join(' ')}
390
+ className="jp-xtralab-Launcher-agent"
378
391
  title={tooltip}
379
392
  aria-label={agent.label}
380
393
  aria-describedby={hintId}
@@ -0,0 +1,86 @@
1
+ .jp-xtralab-EditorBreadcrumbs.jp-Toolbar-item {
2
+ flex: 1 1 auto;
3
+ display: flex;
4
+ align-items: center;
5
+ align-self: center;
6
+ box-sizing: border-box;
7
+ min-width: 0;
8
+ height: 28px;
9
+ min-height: 28px;
10
+ max-height: 28px;
11
+ margin: 0;
12
+ padding: 0 10px;
13
+ color: var(--jp-ui-font-color2);
14
+ font-family: var(--jp-ui-font-family);
15
+ font-size: var(--jp-ui-font-size1);
16
+ overflow: hidden;
17
+ }
18
+
19
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-container {
20
+ display: flex;
21
+ align-items: center;
22
+ min-width: 0;
23
+ max-width: 100%;
24
+ height: 100%;
25
+ }
26
+
27
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-content {
28
+ display: flex;
29
+ align-items: center;
30
+ min-width: 0;
31
+ max-width: 100%;
32
+ overflow: hidden;
33
+ white-space: nowrap;
34
+ }
35
+
36
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-home {
37
+ display: inline-flex;
38
+ align-items: center;
39
+ flex: 0 0 auto;
40
+ margin-right: 2px;
41
+ padding: 2px;
42
+ border-radius: var(--jp-border-radius);
43
+ cursor: pointer;
44
+ }
45
+
46
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-home:hover {
47
+ background-color: var(--jp-layout-color2);
48
+ }
49
+
50
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-home:focus-visible {
51
+ outline: 2px solid var(--jp-brand-color1);
52
+ }
53
+
54
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-item {
55
+ flex: 0 1 auto;
56
+ min-width: 0;
57
+ margin: 0 2px;
58
+ overflow: hidden;
59
+ padding: 2px;
60
+ border-radius: var(--jp-border-radius);
61
+ cursor: pointer;
62
+ line-height: 18px;
63
+ text-overflow: ellipsis;
64
+ }
65
+
66
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-item,
67
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-separator {
68
+ vertical-align: middle;
69
+ }
70
+
71
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-item:hover {
72
+ background-color: var(--jp-layout-color2);
73
+ }
74
+
75
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-item:focus-visible {
76
+ outline: 2px solid var(--jp-brand-color1);
77
+ }
78
+
79
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-item.jp-mod-current {
80
+ color: var(--jp-ui-font-color1);
81
+ }
82
+
83
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-separator {
84
+ flex: 0 0 auto;
85
+ line-height: 18px;
86
+ }
package/style/index.css CHANGED
@@ -1,4 +1,6 @@
1
1
  @import url('base.css');
2
+ @import url('editorBreadcrumbs.css');
2
3
  @import url('git.css');
3
4
  @import url('launcher.css');
4
5
  @import url('statusBar.css');
6
+ @import url('tabs.css');
package/style/index.js CHANGED
@@ -2,3 +2,4 @@ import './base.css';
2
2
  import './git.css';
3
3
  import './launcher.css';
4
4
  import './statusBar.css';
5
+ import './tabs.css';
@@ -295,11 +295,6 @@
295
295
  cursor: not-allowed;
296
296
  }
297
297
 
298
- .jp-xtralab-Launcher-agent-primary {
299
- border-color: var(--jp-brand-color2);
300
- box-shadow: 0 0 0 1px var(--jp-brand-color2);
301
- }
302
-
303
298
  .jp-xtralab-Launcher-agent:hover:not(:disabled) {
304
299
  background-color: var(--jp-layout-color2);
305
300
  border-color: var(--jp-border-color1);
package/style/tabs.css ADDED
@@ -0,0 +1,66 @@
1
+ /*
2
+ * Adaptive (VS Code-style) document tab widths. Tabs size to their filename
3
+ * instead of being pinned to --jp-private-horizontal-tab-width (216px), and
4
+ * the horizontal tab strip becomes scrollable when tabs overflow.
5
+ *
6
+ * Mostly mirrors jupyterlab/jupyterlab#18796, with one deliberate divergence:
7
+ * .lm-TabBar-content uses `flex: 0 1 auto` (not `1 1 auto`) so the +/add
8
+ * button sits next to the last tab instead of being pushed to the right
9
+ * edge of the bar. Overflow scrolling still works because `flex-shrink: 1`
10
+ * plus `min-width: 0` lets content shrink when tabs exceed the bar width.
11
+ */
12
+
13
+ .lm-DockPanel-tabBar .lm-TabBar-tab,
14
+ .lm-TabPanel-tabBar .lm-TabBar-tab {
15
+ flex: 0 0 auto;
16
+ padding: 0 8px;
17
+ cursor: pointer;
18
+ }
19
+
20
+ .lm-DockPanel-tabBar .lm-TabBar-addButton,
21
+ .lm-TabPanel-tabBar .lm-TabBar-addButton {
22
+ cursor: pointer;
23
+ }
24
+
25
+ .lm-DockPanel-tabBar .lm-TabBar-tab {
26
+ width: auto;
27
+ }
28
+
29
+ .lm-DockPanel-tabBar .lm-TabBar-content {
30
+ flex: 0 1 auto;
31
+ min-width: 0;
32
+ }
33
+
34
+ .lm-DockPanel-tabBar[data-orientation='horizontal'] {
35
+ overflow: hidden;
36
+ }
37
+
38
+ .lm-DockPanel-tabBar[data-orientation='horizontal'] > .lm-TabBar-content {
39
+ overflow: auto hidden;
40
+ position: relative;
41
+ z-index: 3;
42
+ }
43
+
44
+ .lm-TabBar-content::-webkit-scrollbar {
45
+ height: 2px;
46
+ background: var(--jp-layout-color1);
47
+ }
48
+
49
+ .lm-TabBar-content::-webkit-scrollbar-button {
50
+ display: none;
51
+ width: 0;
52
+ height: 0;
53
+ }
54
+
55
+ .lm-TabBar-content::-webkit-scrollbar-track {
56
+ background: var(--jp-layout-color1);
57
+ }
58
+
59
+ .lm-TabBar-content::-webkit-scrollbar-thumb {
60
+ background: rgb(120 120 120 / 40%);
61
+ border-radius: 999px;
62
+ }
63
+
64
+ .lm-TabBar-content:hover::-webkit-scrollbar-thumb {
65
+ background: rgb(160 160 160 / 60%);
66
+ }