xtralab 0.11.0 → 0.12.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 (162) hide show
  1. package/README.md +37 -229
  2. package/lib/agentSessions.d.ts +6 -2
  3. package/lib/askAgent/editorSelection.d.ts +53 -0
  4. package/lib/askAgent/editorSelection.js +95 -0
  5. package/lib/askAgent/icons.d.ts +7 -0
  6. package/lib/askAgent/icons.js +15 -0
  7. package/lib/askAgent/index.d.ts +32 -0
  8. package/lib/askAgent/index.js +741 -0
  9. package/lib/askAgent/popup.d.ts +83 -0
  10. package/lib/askAgent/popup.js +221 -0
  11. package/lib/askAgent/prompt.d.ts +24 -0
  12. package/lib/askAgent/prompt.js +116 -0
  13. package/lib/askAgent/queue.d.ts +93 -0
  14. package/lib/askAgent/queue.js +207 -0
  15. package/lib/askAgent/queuePanel.d.ts +76 -0
  16. package/lib/askAgent/queuePanel.js +188 -0
  17. package/lib/askAgent/targetPicker.d.ts +58 -0
  18. package/lib/askAgent/targetPicker.js +74 -0
  19. package/lib/askAgent/tokens.d.ts +124 -0
  20. package/lib/askAgent/tokens.js +13 -0
  21. package/lib/commandBar/index.d.ts +12 -8
  22. package/lib/commandBar/index.js +58 -41
  23. package/lib/editorBreadcrumbs/index.js +9 -4
  24. package/lib/editorBreadcrumbs/widget.d.ts +7 -7
  25. package/lib/editorBreadcrumbs/widget.js +6 -8
  26. package/lib/fileBrowser/commands.d.ts +2 -8
  27. package/lib/fileBrowser/commands.js +39 -30
  28. package/lib/fileBrowser/contents.d.ts +8 -3
  29. package/lib/fileBrowser/fileBrowser.d.ts +2 -1
  30. package/lib/fileBrowser/widget.d.ts +5 -9
  31. package/lib/fileBrowser/widget.js +1 -1
  32. package/lib/fileTypeIcons/index.js +5 -2
  33. package/lib/git/api.d.ts +0 -26
  34. package/lib/git/api.js +2 -44
  35. package/lib/git/askRequest.d.ts +23 -0
  36. package/lib/git/askRequest.js +90 -0
  37. package/lib/git/commands.d.ts +14 -2
  38. package/lib/git/commands.js +16 -13
  39. package/lib/git/diffModel.d.ts +1 -9
  40. package/lib/git/diffModel.js +1 -1
  41. package/lib/git/diffProvider.js +8 -3
  42. package/lib/git/diffSurface.d.ts +19 -2
  43. package/lib/git/diffSurface.js +64 -18
  44. package/lib/git/diffTheme.d.ts +0 -4
  45. package/lib/git/diffTheme.js +1 -1
  46. package/lib/git/diffWidget.d.ts +9 -0
  47. package/lib/git/diffWidget.js +38 -10
  48. package/lib/git/imageDiff.d.ts +17 -4
  49. package/lib/git/imageDiff.js +90 -30
  50. package/lib/git/index.js +7 -2
  51. package/lib/git/notebookDiff.d.ts +10 -67
  52. package/lib/git/notebookDiff.js +62 -69
  53. package/lib/git/tokens.d.ts +5 -2
  54. package/lib/highlight/index.js +12 -8
  55. package/lib/index.js +2 -0
  56. package/lib/launcher/agents.d.ts +11 -1
  57. package/lib/launcher/agents.js +26 -8
  58. package/lib/launcher/commands.d.ts +6 -2
  59. package/lib/launcher/dashboard.d.ts +8 -1
  60. package/lib/launcher/dashboard.js +39 -33
  61. package/lib/launcher/editorRegistry.js +5 -3
  62. package/lib/launcher/editors.d.ts +24 -7
  63. package/lib/launcher/editors.js +14 -5
  64. package/lib/launcher/icons.d.ts +0 -37
  65. package/lib/launcher/icons.js +11 -12
  66. package/lib/launcher/index.js +5 -2
  67. package/lib/launcher/invocation.d.ts +0 -15
  68. package/lib/launcher/invocation.js +1 -1
  69. package/lib/launcher/schemaDefaults.d.ts +3 -1
  70. package/lib/launcher/schemaDefaults.js +3 -1
  71. package/lib/mimeData.d.ts +7 -0
  72. package/lib/mimeData.js +16 -0
  73. package/lib/omnibox/index.d.ts +4 -0
  74. package/lib/omnibox/index.js +43 -2
  75. package/lib/omnibox/model.d.ts +43 -14
  76. package/lib/omnibox/model.js +117 -14
  77. package/lib/omnibox/recents.d.ts +65 -0
  78. package/lib/omnibox/recents.js +109 -0
  79. package/lib/omnibox/tokens.d.ts +6 -2
  80. package/lib/omnibox/widget.d.ts +23 -6
  81. package/lib/omnibox/widget.js +26 -7
  82. package/lib/showOutput/index.js +15 -18
  83. package/lib/terminals/agentTerminals.d.ts +63 -0
  84. package/lib/terminals/agentTerminals.js +157 -0
  85. package/lib/terminals/index.d.ts +7 -1
  86. package/lib/terminals/index.js +50 -30
  87. package/lib/terminals/model.d.ts +11 -2
  88. package/lib/terminals/model.js +27 -8
  89. package/lib/terminals/tokens.d.ts +66 -0
  90. package/lib/terminals/tokens.js +7 -0
  91. package/lib/terminals/widget.d.ts +3 -6
  92. package/lib/terminals/widget.js +1 -1
  93. package/lib/topBar/index.js +3 -3
  94. package/lib/walkthrough/index.js +14 -3
  95. package/lib/walkthrough/panel.d.ts +12 -4
  96. package/lib/walkthrough/panel.js +9 -3
  97. package/package.json +49 -51
  98. package/schema/command-bar.json +10 -0
  99. package/schema/launcher.json +4 -4
  100. package/schema/omnibox.json +15 -0
  101. package/src/agentSessions.ts +6 -2
  102. package/src/askAgent/editorSelection.ts +138 -0
  103. package/src/askAgent/icons.ts +16 -0
  104. package/src/askAgent/index.ts +915 -0
  105. package/src/askAgent/popup.tsx +407 -0
  106. package/src/askAgent/prompt.ts +144 -0
  107. package/src/askAgent/queue.ts +269 -0
  108. package/src/askAgent/queuePanel.tsx +418 -0
  109. package/src/askAgent/targetPicker.tsx +209 -0
  110. package/src/askAgent/tokens.ts +139 -0
  111. package/src/commandBar/index.ts +75 -49
  112. package/src/editorBreadcrumbs/index.ts +14 -4
  113. package/src/editorBreadcrumbs/widget.ts +11 -9
  114. package/src/fileBrowser/commands.ts +45 -31
  115. package/src/fileBrowser/contents.ts +7 -3
  116. package/src/fileBrowser/fileBrowser.tsx +1 -1
  117. package/src/fileBrowser/widget.tsx +5 -3
  118. package/src/fileTypeIcons/index.ts +6 -3
  119. package/src/git/api.ts +2 -61
  120. package/src/git/askRequest.ts +113 -0
  121. package/src/git/commands.ts +41 -14
  122. package/src/git/diffModel.ts +1 -1
  123. package/src/git/diffProvider.tsx +11 -3
  124. package/src/git/diffSurface.tsx +114 -22
  125. package/src/git/diffTheme.ts +1 -1
  126. package/src/git/diffWidget.tsx +54 -7
  127. package/src/git/imageDiff.tsx +169 -58
  128. package/src/git/index.ts +10 -2
  129. package/src/git/notebookDiff.tsx +91 -57
  130. package/src/git/tokens.ts +4 -2
  131. package/src/highlight/index.ts +12 -8
  132. package/src/index.ts +2 -0
  133. package/src/launcher/agents.ts +31 -7
  134. package/src/launcher/commands.ts +6 -2
  135. package/src/launcher/dashboard.tsx +86 -38
  136. package/src/launcher/editorRegistry.ts +10 -4
  137. package/src/launcher/editors.ts +34 -9
  138. package/src/launcher/icons.ts +11 -12
  139. package/src/launcher/index.ts +5 -2
  140. package/src/launcher/invocation.ts +1 -1
  141. package/src/launcher/schemaDefaults.ts +3 -1
  142. package/src/mimeData.ts +20 -0
  143. package/src/omnibox/index.ts +48 -1
  144. package/src/omnibox/model.ts +173 -33
  145. package/src/omnibox/recents.ts +155 -0
  146. package/src/omnibox/tokens.ts +6 -2
  147. package/src/omnibox/widget.tsx +54 -13
  148. package/src/showOutput/index.ts +20 -23
  149. package/src/terminals/agentTerminals.ts +238 -0
  150. package/src/terminals/index.ts +54 -30
  151. package/src/terminals/model.ts +28 -7
  152. package/src/terminals/tokens.ts +77 -0
  153. package/src/terminals/widget.tsx +4 -2
  154. package/src/topBar/index.ts +12 -6
  155. package/src/walkthrough/index.ts +19 -4
  156. package/src/walkthrough/panel.ts +15 -5
  157. package/style/askAgent.css +544 -0
  158. package/style/commandBar.css +12 -33
  159. package/style/git.css +72 -31
  160. package/style/index.css +1 -0
  161. package/style/index.js +2 -0
  162. package/style/topBar.css +4 -4
@@ -0,0 +1,209 @@
1
+ import type { TranslationBundle } from '@jupyterlab/translation';
2
+ import { addIcon, LabIcon } from '@jupyterlab/ui-components';
3
+ import * as React from 'react';
4
+
5
+ import type { IAgent } from '../launcher/agents';
6
+
7
+ /**
8
+ * A running agent terminal offered as a prompt target: the session data
9
+ * from `IAgentTerminals` plus the matching agent's icon, resolved by the
10
+ * plugin so these components stay presentation-only.
11
+ */
12
+ export interface ISessionTarget {
13
+ /**
14
+ * The terminal session name.
15
+ */
16
+ name: string;
17
+
18
+ /**
19
+ * The session's display label (usually the agent's name).
20
+ */
21
+ label: string;
22
+
23
+ /**
24
+ * The agent's latest activity line, when one is available.
25
+ */
26
+ activity: string | null;
27
+
28
+ /**
29
+ * The running agent's icon.
30
+ */
31
+ icon: LabIcon;
32
+ }
33
+
34
+ /**
35
+ * Keyboard support shared by the two radiogroups below, following the ARIA
36
+ * radio-group pattern: the group is one tab stop (only the checked radio is
37
+ * tabbable) and the arrow keys move the selection to the neighbouring
38
+ * radio, wrapping around. Selection follows focus, so the handler clicks
39
+ * the neighbour and the re-render moves the roving tabindex along.
40
+ */
41
+ function onRadioGroupKeyDown(event: React.KeyboardEvent<HTMLElement>): void {
42
+ let delta: number;
43
+ switch (event.key) {
44
+ case 'ArrowRight':
45
+ case 'ArrowDown':
46
+ delta = 1;
47
+ break;
48
+ case 'ArrowLeft':
49
+ case 'ArrowUp':
50
+ delta = -1;
51
+ break;
52
+ default:
53
+ return;
54
+ }
55
+ const radios = Array.from(
56
+ event.currentTarget.querySelectorAll<HTMLButtonElement>('[role="radio"]')
57
+ );
58
+ const index = radios.findIndex(radio => radio === event.target);
59
+ if (index === -1) {
60
+ return;
61
+ }
62
+ event.preventDefault();
63
+ const next = radios[(index + delta + radios.length) % radios.length];
64
+ next.focus();
65
+ next.click();
66
+ }
67
+
68
+ /**
69
+ * The chip row picking where a prompt goes: "New terminal" plus one chip
70
+ * per running agent session. Renders nothing while no agent session is
71
+ * running (a new terminal is then the only possibility and the row would
72
+ * be noise).
73
+ */
74
+ export function TargetChips(props: {
75
+ /**
76
+ * Agents that could start in a new terminal; gates the "New terminal" chip.
77
+ */
78
+ agents: IAgent[];
79
+ /**
80
+ * The running agent sessions.
81
+ */
82
+ targets: ISessionTarget[];
83
+ /**
84
+ * The selected session name, or `null` for a new terminal.
85
+ */
86
+ targetName: string | null;
87
+ trans: TranslationBundle;
88
+ onSelect: (targetName: string | null) => void;
89
+ }): JSX.Element | null {
90
+ const { agents, targets, targetName, trans, onSelect } = props;
91
+ if (targets.length === 0) {
92
+ return null;
93
+ }
94
+ return (
95
+ <div
96
+ className="jp-xtralab-AskAgent-targets"
97
+ role="radiogroup"
98
+ aria-label={trans.__('Send to')}
99
+ onKeyDown={onRadioGroupKeyDown}
100
+ >
101
+ {agents.length > 0 && (
102
+ <button
103
+ type="button"
104
+ role="radio"
105
+ aria-checked={targetName === null}
106
+ tabIndex={targetName === null ? 0 : -1}
107
+ title={trans.__('Start the agent in a new terminal')}
108
+ className={
109
+ 'jp-xtralab-AskAgent-targetButton' +
110
+ (targetName === null ? ' jp-mod-selected' : '')
111
+ }
112
+ // Keep focus where the user is typing while picking a target.
113
+ onMouseDown={event => event.preventDefault()}
114
+ onClick={() => onSelect(null)}
115
+ >
116
+ <addIcon.react
117
+ tag="span"
118
+ className="jp-xtralab-AskAgent-targetIcon"
119
+ />
120
+ <span className="jp-xtralab-AskAgent-targetLabel">
121
+ {trans.__('New terminal')}
122
+ </span>
123
+ </button>
124
+ )}
125
+ {targets.map(target => (
126
+ <button
127
+ key={target.name}
128
+ type="button"
129
+ role="radio"
130
+ aria-checked={target.name === targetName}
131
+ tabIndex={target.name === targetName ? 0 : -1}
132
+ title={
133
+ target.activity
134
+ ? trans.__(
135
+ '%1 · %2 — %3',
136
+ target.label,
137
+ target.name,
138
+ target.activity
139
+ )
140
+ : trans.__('%1 · %2', target.label, target.name)
141
+ }
142
+ className={
143
+ 'jp-xtralab-AskAgent-targetButton' +
144
+ (target.name === targetName ? ' jp-mod-selected' : '')
145
+ }
146
+ onMouseDown={event => event.preventDefault()}
147
+ onClick={() => onSelect(target.name)}
148
+ >
149
+ <target.icon.react
150
+ tag="span"
151
+ className="jp-xtralab-AskAgent-targetIcon"
152
+ />
153
+ <span className="jp-xtralab-AskAgent-targetLabel">
154
+ {target.label}
155
+ </span>
156
+ <span className="jp-xtralab-AskAgent-targetName">{target.name}</span>
157
+ </button>
158
+ ))}
159
+ </div>
160
+ );
161
+ }
162
+
163
+ /**
164
+ * The icon radiogroup picking which agent a new terminal starts with.
165
+ * Renders nothing when no agent accepts an initial prompt.
166
+ */
167
+ export function AgentChoices(props: {
168
+ agents: IAgent[];
169
+ agentId: string;
170
+ trans: TranslationBundle;
171
+ onSelect: (agentId: string) => void;
172
+ }): JSX.Element | null {
173
+ const { agents, agentId, trans, onSelect } = props;
174
+ if (agents.length === 0) {
175
+ return null;
176
+ }
177
+ return (
178
+ <div
179
+ className="jp-xtralab-AskAgent-agents"
180
+ role="radiogroup"
181
+ aria-label={trans.__('Agent')}
182
+ onKeyDown={onRadioGroupKeyDown}
183
+ >
184
+ {agents.map(agent => (
185
+ <button
186
+ key={agent.id}
187
+ type="button"
188
+ role="radio"
189
+ aria-checked={agent.id === agentId}
190
+ tabIndex={agent.id === agentId ? 0 : -1}
191
+ title={agent.label}
192
+ aria-label={agent.label}
193
+ className={
194
+ 'jp-xtralab-AskAgent-agentButton' +
195
+ (agent.id === agentId ? ' jp-mod-selected' : '')
196
+ }
197
+ // Keep focus where the user is typing while picking an agent.
198
+ onMouseDown={event => event.preventDefault()}
199
+ onClick={() => onSelect(agent.id)}
200
+ >
201
+ <agent.icon.react
202
+ tag="span"
203
+ className="jp-xtralab-AskAgent-agentIcon"
204
+ />
205
+ </button>
206
+ ))}
207
+ </div>
208
+ );
209
+ }
@@ -0,0 +1,139 @@
1
+ import { Token } from '@lumino/coreutils';
2
+
3
+ /**
4
+ * The command id that opens the ask-agent popup for the current text-editor
5
+ * selection. Registered by `xtralab:ask-agent`; exposed here so menus and
6
+ * other plugins can reference it without importing the plugin internals.
7
+ */
8
+ export const ASK_AGENT_COMMAND = 'xtralab:ask-agent';
9
+
10
+ /**
11
+ * A code location (and optionally the selected source text) that a prompt
12
+ * should point an agent at. The structured fields are composed into the
13
+ * final prompt by the ask-agent plugin so every entry point — editor
14
+ * selection, diff line selection — produces consistently worded prompts.
15
+ */
16
+ export interface IAskAgentContext {
17
+ /**
18
+ * Path of the file the selection belongs to, relative to {@link cwd} (or
19
+ * to the server root when no cwd is given) so the launched agent can open
20
+ * it directly.
21
+ */
22
+ path: string;
23
+
24
+ /**
25
+ * Server-relative directory the agent's terminal starts in. Omit to start
26
+ * in the server root. Diff selections pass the git repository root here so
27
+ * the agent can run git commands right away.
28
+ */
29
+ cwd?: string;
30
+
31
+ /**
32
+ * The notebook cell the selection lives in, when {@link path} is a
33
+ * notebook. `index` is the cell's 0-based position in the nbformat
34
+ * `cells` array; `type` is the nbformat cell type (`code`, `markdown`,
35
+ * `raw`). When set, {@link startLine}/{@link endLine} count within the
36
+ * cell's source rather than within a file.
37
+ */
38
+ cell?: {
39
+ index: number;
40
+ type: string;
41
+ };
42
+
43
+ /**
44
+ * First selected line, 1-indexed and inclusive. Omit when no line range is
45
+ * known (the prompt then only references the file).
46
+ */
47
+ startLine?: number;
48
+
49
+ /**
50
+ * Last selected line, 1-indexed and inclusive. Defaults to
51
+ * {@link startLine} when omitted.
52
+ */
53
+ endLine?: number;
54
+
55
+ /**
56
+ * Whether {@link startLine}/{@link endLine} index the file's current
57
+ * working-tree content. `false` for diff selections taken from another
58
+ * revision (the old side, or a challenger that is not the working tree),
59
+ * whose numbers must not be used to highlight the working file. Omitted
60
+ * means `true`.
61
+ */
62
+ linesInWorkingFile?: boolean;
63
+
64
+ /**
65
+ * The selected source text. May be empty when only a location is known;
66
+ * the prompt then relies on the path and line range alone.
67
+ */
68
+ text: string;
69
+
70
+ /**
71
+ * Extra agent-facing locator appended after the line range in the prompt,
72
+ * e.g. `the old side (INDEX) of the git diff`. Written in English because
73
+ * it is consumed by the agent, not shown in the UI.
74
+ */
75
+ location?: string;
76
+
77
+ /**
78
+ * Short user-facing tag shown in the popup header next to the file name,
79
+ * e.g. "old version". Translated, unlike {@link location}. Omit when the
80
+ * range reads naturally as the file's current content.
81
+ */
82
+ note?: string;
83
+ }
84
+
85
+ /**
86
+ * Where a submitted prompt goes: a fresh terminal started with the chosen
87
+ * agent's command, or an existing terminal session whose running agent
88
+ * receives the prompt in its input box (queued by the agent itself when it
89
+ * is busy).
90
+ */
91
+ export type AskAgentTarget =
92
+ | { kind: 'new'; agentId: string }
93
+ | { kind: 'session'; name: string };
94
+
95
+ /**
96
+ * A request to open the ask-agent popup: what code to prompt about and
97
+ * where to place the popup on screen.
98
+ */
99
+ export interface IAskAgentRequest {
100
+ /**
101
+ * The code location and selection the prompt is about.
102
+ */
103
+ context: IAskAgentContext;
104
+
105
+ /**
106
+ * Viewport rectangle the popup anchors to (typically the selection end or
107
+ * the button that was clicked). `null` positions the popup near the top
108
+ * center of the viewport.
109
+ */
110
+ anchor: DOMRect | null;
111
+ }
112
+
113
+ /**
114
+ * The ask-agent popup: a small floating prompt box that sends the given
115
+ * code selection, plus the user's typed instruction, to one of the
116
+ * configured coding agents — in a fresh terminal, or into an agent already
117
+ * running in an existing one.
118
+ */
119
+ export interface IAskAgent {
120
+ /**
121
+ * Open the popup for `request`, replacing any popup already open.
122
+ */
123
+ open(request: IAskAgentRequest): void;
124
+
125
+ /**
126
+ * Close the popup if it is open.
127
+ */
128
+ close(): void;
129
+ }
130
+
131
+ /**
132
+ * DI token for {@link IAskAgent}. Provided by `xtralab:ask-agent` and
133
+ * consumed — optionally, so diffs still render when the plugin is disabled —
134
+ * by the git diff plugins to offer "ask an agent about these lines".
135
+ */
136
+ export const IAskAgent = new Token<IAskAgent>(
137
+ 'xtralab:IAskAgent',
138
+ 'A floating prompt box that sends the current code selection to a coding agent running in a terminal.'
139
+ );
@@ -1,8 +1,8 @@
1
1
  import {
2
- ILabShell,
3
2
  JupyterFrontEnd,
4
3
  JupyterFrontEndPlugin
5
4
  } from '@jupyterlab/application';
5
+ import { IToolbarWidgetRegistry } from '@jupyterlab/apputils';
6
6
  import { ITranslator, nullTranslator } from '@jupyterlab/translation';
7
7
  import { searchIcon } from '@jupyterlab/ui-components';
8
8
  import { CommandRegistry } from '@lumino/commands';
@@ -13,15 +13,24 @@ import { IOmnibox, OMNIBOX_OPEN_COMMAND } from '../omnibox/tokens';
13
13
  const PLUGIN_ID = 'xtralab:command-bar';
14
14
 
15
15
  /**
16
- * A search-bar-styled launcher for the `top` area. It holds no text input of
17
- * its own: clicking the pill (or pressing Enter/Space while it is focused)
18
- * runs `onActivate`, which opens the omnibox.
19
- *
20
- * The widget's root is a transparent, full-height wrapper centered as an
21
- * overlay over the whole top panel (style/commandBar.css). The wrapper
22
- * ignores pointer events so only the inner pill is clickable and the empty
23
- * strips above and below it let clicks fall through to whatever sits beneath
24
- * the overlay, such as the menu bar.
16
+ * Factory name of JupyterLab's settings-driven top bar toolbar (`#jp-top-bar`,
17
+ * built by `@jupyterlab/application-extension:top-bar`).
18
+ */
19
+ const TOPBAR_FACTORY = 'TopBar';
20
+
21
+ /**
22
+ * Name of the pill item within that toolbar. Its placement is declared under
23
+ * `jupyter.lab.toolbars` in this plugin's settings schema
24
+ * (schema/command-bar.json): the core toolbar ships a spacer at rank 50, so
25
+ * the pill's higher rank lands it at the trailing end of the toolbar, next to
26
+ * the right-sidebar toggle button.
27
+ */
28
+ const ITEM_NAME = 'omnibox';
29
+
30
+ /**
31
+ * A search-bar-styled launcher pill. It holds no text input of its own:
32
+ * clicking it (or pressing Enter/Space while it is focused) runs `onActivate`,
33
+ * which opens the omnibox.
25
34
  */
26
35
  class CommandBar extends Widget {
27
36
  constructor(options: CommandBar.IOptions) {
@@ -43,9 +52,9 @@ class CommandBar extends Widget {
43
52
  }
44
53
 
45
54
  protected onAfterAttach(): void {
46
- // The pill restores `pointer-events: auto`, so the click bubbles from it
47
- // up to the root node listened on here; keyboard Enter/Space on the
48
- // focused button raises the same synthetic click.
55
+ // The click bubbles from the inner pill button up to the root node
56
+ // listened on here; keyboard Enter/Space on the focused button raises the
57
+ // same synthetic click.
49
58
  this.node.addEventListener('click', this);
50
59
  }
51
60
 
@@ -58,13 +67,21 @@ class CommandBar extends Widget {
58
67
 
59
68
  namespace CommandBar {
60
69
  export interface IOptions {
61
- /** Placeholder-style text shown inside the pill. */
70
+ /**
71
+ * Placeholder-style text shown inside the pill.
72
+ */
62
73
  label: string;
63
- /** Tooltip and accessible name for the button. */
74
+ /**
75
+ * Tooltip and accessible name for the button.
76
+ */
64
77
  caption: string;
65
- /** Formatted keyboard shortcut shown as a trailing hint, if any. */
78
+ /**
79
+ * Formatted keyboard shortcut shown as a trailing hint, if any.
80
+ */
66
81
  shortcut?: string;
67
- /** Runs on click or keyboard activation. */
82
+ /**
83
+ * Runs on click or keyboard activation.
84
+ */
68
85
  onActivate: () => void;
69
86
  }
70
87
 
@@ -73,6 +90,10 @@ namespace CommandBar {
73
90
  caption: string,
74
91
  shortcut?: string
75
92
  ): HTMLElement {
93
+ // The root is a plain wrapper: as a toolbar item it receives the core
94
+ // `.jp-Toolbar > .jp-Toolbar-item` sizing (full height, centered flex),
95
+ // which would otherwise stretch the pill itself; the button inside keeps
96
+ // its own compact pill height.
76
97
  const wrapper = document.createElement('div');
77
98
 
78
99
  const button = document.createElement('button');
@@ -108,26 +129,30 @@ namespace CommandBar {
108
129
  }
109
130
 
110
131
  /**
111
- * Add a centered, search-bar-styled command bar to the top area. Clicking it
112
- * opens the omnibox — a launcher overlay that fuzzy-searches files and
113
- * commands and routes a typed prompt to an agent (the leading sidebar toggle
114
- * and the menu bar sit to its left, the trailing sidebar toggle to its right;
115
- * the bar is centered over the whole panel as an overlay, so it stays put
116
- * regardless of their widths).
132
+ * Contribute a search-bar-styled command bar to JupyterLab's top bar toolbar.
133
+ * Clicking it opens the omnibox — a launcher overlay that fuzzy-searches files
134
+ * and commands and routes a typed prompt to an agent.
117
135
  *
118
- * The bar is only added when the omnibox is available — the `IOmnibox` token
119
- * is provided by `xtralab:omnibox` — so it never appears with nothing to open.
136
+ * The pill is a regular settings-driven toolbar item: this plugin registers a
137
+ * widget factory for it on the `IToolbarWidgetRegistry` and declares its rank
138
+ * in schema/command-bar.json, so users can move or disable it from the Top
139
+ * Bar settings like any other toolbar item.
140
+ *
141
+ * The factory is only registered when the omnibox is available — the
142
+ * `IOmnibox` token is provided by `xtralab:omnibox` — so the pill never
143
+ * appears with nothing to open (without a factory, the toolbar item resolves
144
+ * to an empty command button that renders nothing).
120
145
  */
121
146
  const plugin: JupyterFrontEndPlugin<void> = {
122
147
  id: PLUGIN_ID,
123
148
  description:
124
- 'Add a centered command bar to the top area that opens the omnibox.',
149
+ 'Add a command bar to the top bar toolbar that opens the omnibox.',
125
150
  autoStart: true,
126
- requires: [ILabShell],
151
+ requires: [IToolbarWidgetRegistry],
127
152
  optional: [IOmnibox, ITranslator],
128
153
  activate: (
129
154
  app: JupyterFrontEnd,
130
- labShell: ILabShell,
155
+ toolbarRegistry: IToolbarWidgetRegistry,
131
156
  omnibox: IOmnibox | null,
132
157
  translator: ITranslator | null
133
158
  ): void => {
@@ -137,28 +162,29 @@ const plugin: JupyterFrontEndPlugin<void> = {
137
162
 
138
163
  const trans = (translator ?? nullTranslator).load('jupyterlab');
139
164
 
140
- // Show the omnibox's keyboard shortcut as a hint in the pill, derived from
141
- // the live binding (registered by xtralab:omnibox, which activates first as
142
- // the IOmnibox provider) so it stays correct and uses the platform's
143
- // modifier symbols. Empty when no binding is registered.
144
- const binding = app.commands.keyBindings.find(
145
- keyBinding => keyBinding.command === OMNIBOX_OPEN_COMMAND
146
- );
147
- const shortcut = binding
148
- ? CommandRegistry.formatKeystroke(binding.keys)
149
- : '';
150
-
151
- const widget = new CommandBar({
152
- label: trans.__('Search…'),
153
- caption: trans.__('Search files and commands, or ask an agent'),
154
- shortcut,
155
- onActivate: () => {
156
- omnibox.open();
157
- }
165
+ toolbarRegistry.addFactory(TOPBAR_FACTORY, ITEM_NAME, () => {
166
+ // Show the omnibox's keyboard shortcut as a hint in the pill, derived
167
+ // from the live binding (registered by xtralab:omnibox, which activates
168
+ // first as the IOmnibox provider) so it stays correct and uses the
169
+ // platform's modifier symbols. Empty when no binding is registered.
170
+ const binding = app.commands.keyBindings.find(
171
+ keyBinding => keyBinding.command === OMNIBOX_OPEN_COMMAND
172
+ );
173
+ const shortcut = binding
174
+ ? CommandRegistry.formatKeystroke(binding.keys)
175
+ : '';
176
+
177
+ const widget = new CommandBar({
178
+ label: trans.__('Search…'),
179
+ caption: trans.__('Search files and commands, or ask an agent'),
180
+ shortcut,
181
+ onActivate: () => {
182
+ omnibox.open();
183
+ }
184
+ });
185
+ widget.id = 'xtralab-command-bar';
186
+ return widget;
158
187
  });
159
- widget.id = 'xtralab-command-bar';
160
-
161
- labShell.add(widget, 'top', { rank: 500 });
162
188
  }
163
189
  };
164
190
 
@@ -6,6 +6,11 @@ import type {
6
6
  DocumentRegistry,
7
7
  IDocumentWidget
8
8
  } from '@jupyterlab/docregistry';
9
+ import {
10
+ ITranslator,
11
+ nullTranslator,
12
+ type TranslationBundle
13
+ } from '@jupyterlab/translation';
9
14
  import type { CommandRegistry } from '@lumino/commands';
10
15
  import { DisposableDelegate, type IDisposable } from '@lumino/disposable';
11
16
  import type { Widget } from '@lumino/widgets';
@@ -20,8 +25,9 @@ class EditorBreadcrumbsExtension implements DocumentRegistry.IWidgetExtension<
20
25
  IDocumentWidget<Widget, DocumentRegistry.IModel>,
21
26
  DocumentRegistry.IModel
22
27
  > {
23
- constructor(commands: CommandRegistry) {
28
+ constructor(commands: CommandRegistry, trans: TranslationBundle) {
24
29
  this._commands = commands;
30
+ this._trans = trans;
25
31
  }
26
32
 
27
33
  createNew(
@@ -30,7 +36,8 @@ class EditorBreadcrumbsExtension implements DocumentRegistry.IWidgetExtension<
30
36
  ): IDisposable | void {
31
37
  const breadcrumbs = new EditorBreadcrumbs({
32
38
  context,
33
- commands: this._commands
39
+ commands: this._commands,
40
+ trans: this._trans
34
41
  });
35
42
  const added = widget.toolbar.insertItem(0, TOOLBAR_ITEM_NAME, breadcrumbs);
36
43
 
@@ -45,6 +52,7 @@ class EditorBreadcrumbsExtension implements DocumentRegistry.IWidgetExtension<
45
52
  }
46
53
 
47
54
  private _commands: CommandRegistry;
55
+ private _trans: TranslationBundle;
48
56
  }
49
57
 
50
58
  /**
@@ -59,10 +67,12 @@ const plugin: JupyterFrontEndPlugin<void> = {
59
67
  id: PLUGIN_ID,
60
68
  description: 'Display the active file path in text editor toolbars.',
61
69
  autoStart: true,
62
- activate: (app: JupyterFrontEnd): void => {
70
+ optional: [ITranslator],
71
+ activate: (app: JupyterFrontEnd, translator: ITranslator | null): void => {
72
+ const trans = (translator ?? nullTranslator).load('jupyterlab');
63
73
  app.docRegistry.addWidgetExtension(
64
74
  EDITOR_FACTORY,
65
- new EditorBreadcrumbsExtension(app.commands)
75
+ new EditorBreadcrumbsExtension(app.commands, trans)
66
76
  );
67
77
  }
68
78
  };
@@ -1,5 +1,6 @@
1
1
  import { PageConfig } from '@jupyterlab/coreutils';
2
2
  import type { DocumentRegistry } from '@jupyterlab/docregistry';
3
+ import type { TranslationBundle } from '@jupyterlab/translation';
3
4
  import { homeIcon as rootIcon } from '@jupyterlab/ui-components';
4
5
  import type { CommandRegistry } from '@lumino/commands';
5
6
  import { Widget } from '@lumino/widgets';
@@ -19,26 +20,24 @@ const SEPARATOR_CLASS = 'jp-BreadCrumbs-separator';
19
20
  */
20
21
  const REVEAL_COMMAND = 'xtralab:reveal-path';
21
22
 
22
- export interface IEditorBreadcrumbsOptions {
23
+ interface IEditorBreadcrumbsOptions {
23
24
  context: DocumentRegistry.IContext<DocumentRegistry.IModel>;
24
25
  commands: CommandRegistry;
26
+ trans: TranslationBundle;
25
27
  }
26
28
 
27
29
  /**
28
- * Read-only breadcrumb path for file editor toolbars.
29
- *
30
- * The upstream `@jupyterlab/filebrowser` BreadCrumbs widget is coupled to
31
- * FileBrowserModel navigation, drag/drop, and path editing. If that code
32
- * becomes reusable upstream, replace this local display-only renderer with
33
- * the shared implementation.
30
+ * Read-only breadcrumb path for file editor toolbars: renders the open
31
+ * file's path as clickable segments, each dispatching the reveal command.
34
32
  */
35
33
  export class EditorBreadcrumbs extends Widget {
36
34
  constructor(options: IEditorBreadcrumbsOptions) {
37
35
  super({ node: document.createElement('nav') });
38
36
  this._context = options.context;
39
37
  this._commands = options.commands;
38
+ this._trans = options.trans;
40
39
  this.addClass(EDITOR_BREADCRUMBS_CLASS);
41
- this.node.setAttribute('aria-label', 'Editor file path');
40
+ this.node.setAttribute('aria-label', this._trans.__('Editor file path'));
42
41
 
43
42
  this._container = document.createElement('span');
44
43
  this._container.className = BREADCRUMBS_CONTAINER_CLASS;
@@ -105,7 +104,9 @@ export class EditorBreadcrumbs extends Widget {
105
104
  const item = rootIcon.element({
106
105
  className: ROOT_CLASS,
107
106
  tag: 'span',
108
- title: PageConfig.getOption('serverRoot') || 'Jupyter Server Root',
107
+ title:
108
+ PageConfig.getOption('serverRoot') ||
109
+ this._trans.__('Jupyter Server Root'),
109
110
  stylesheet: 'breadCrumb'
110
111
  });
111
112
  item.dataset.path = '/';
@@ -153,6 +154,7 @@ export class EditorBreadcrumbs extends Widget {
153
154
 
154
155
  private _context: DocumentRegistry.IContext<DocumentRegistry.IModel>;
155
156
  private _commands: CommandRegistry;
157
+ private _trans: TranslationBundle;
156
158
  private _container: HTMLElement;
157
159
  private _content: HTMLElement;
158
160
  }