Workspace manager - design proposal

Design B selected by the Star Colonel on 2026-10-23 and refined to his notes. No code of the panel exists yet. The functional requirements are acceptance criteria in docs/acc-crit.md.

The extension replaces the built-in panel "Running Terminals and Kernels" with a panel built on workspaces. Each JupyterLab workspace is a card: its colour, purpose, age, counts and actions in the header, then its tabs, terminals and kernels when the card is open. The dark models use the token values of the live lab's theme, Galaxa Dark Theme - Steel; the light models use stock JupyterLab Light.

1. The panel it replaces

Built-in Running panel of the live lab, 2026-10-23

Screenshot of the live lab on 2026-10-23 (JupyterLab 4.6.4), taken in a scratch workspace that was deleted afterwards.

  • Seven sections in one column: Open Tabs, Kernels, Language servers, Recently Closed, Workspaces, Terminals, Launcher application
  • The 16 terminals of this server read terminals/1 to terminals/16; none shows the program that runs in it or when it was last used
  • Workspaces are a flat list of names; nothing shows which terminal or kernel belongs to which workspace
  • Sidebar icon today: current icon (a circle with a square); the extension brings its own, section 8

2. Decided by the Star Colonel

3. Where the data comes from

The five open questions of the brief, each answered from the JupyterLab 4.6.4 source and from probes on this server.

QuestionAnswerEvidence
How is a terminal or a kernel assigned to a workspace? Where does one show that belongs to no workspace, or to two?
  • A terminal belongs to a workspace when the workspace's saved layout holds the key terminal:<name>
  • A kernel belongs to a workspace when a notebook or console tab of that workspace has a session on that kernel (GET /api/sessions: session path → kernel id)
  • For the workspace of this window the panel reads the live tabs of the window, not the saved file
  • In two workspaces: the row appears in both sections with the mark "also in <name>", and each count includes it
  • In no workspace: the section "In no workspace" after the last workspace, which lists the terminals, kernels and language servers that belong to none
16 terminals on this server: workspace default holds 5 (7, 11, 13, 14, 15), auto-6 holds 1 (1), 10 are in no workspace. The culler sibling reads the same terminal:<name> keys to protect terminals.
Where does the "last accessed" time of a workspace come from?
  • metadata.last_modified of the workspace, which jupyterlab_server reads from the file's modification time
  • JupyterLab writes the workspace 500 ms after any state change of a window that shows it, and once when a window opens it
  • The age in the header is therefore the time since a window last opened or changed the workspace
  • Limit: a window left open with no change does not move the time
Probe 2026-10-23: a workspace with its time set 2 days back was opened in a headless browser; with no click, its last_modified became the load time. Source: apputils-extension state plugin, a 500 ms debouncer around workspaces.save. The culler deletes a workspace when this same time is older than its idle timeout (7 days by default; never default), so the age is also the culler's clock.
Where do the last activity time and the process name of a terminal come from?
  • Last activity: last_activity of GET /api/terminals; jupyter_server_terminals sets it on each output read and each input message
  • Process name: a new endpoint of the server extension reads the foreground process group of the terminal's pty (os.tcgetpgrp on the pty, then /proc/<pgid>/comm)
  • The tab name is the title the program sets with the OSC 0 or 2 escape; JupyterLab copies it into the tab label and never stores it, so only a window that shows the terminal knows it
  • The row shows the process name for every terminal, and the tab title as the row's tooltip when this window shows that terminal
The REST model of a terminal is {name, last_activity} only. Probe 2026-10-23: the foreground process of each terminal shell of this server reads claude or fish from /proc. Source: terminal/src/widget.ts, onTitleChange sets title.label.
How are the tabs of a workspace that is not the current one read and closed?
  • Read: GET /lab/api/workspaces/<id>; data["layout-restorer:data"].main.dock lists the tab keys (terminal:7, notebook:<path>, editor:<path>)
  • Close all: the extension removes those keys from the layout and from data, and writes the workspace back with PUT; a terminal or kernel keeps running
  • A browser window that has that workspace open keeps its own copy and writes it back on its next change. Each window reports its workspace to the server extension every 30 s, so the panel knows which workspaces are open elsewhere and refuses Close all tabs, Delete workspace, Open workspace (the header click, the header icon and the menu item), Open in new browser tab and a row click for them, naming the reason
  • Open: this window navigates to /lab/workspaces/<id> and JupyterLab loads that workspace with its tabs; the menu also offers "Open in new browser tab"
Saved layout of default on 2026-10-23: main.dock.widgets = terminal:14, terminal:11, terminal:13, terminal:15, terminal:7, each with a data entry {"name": "<n>"}. The built-in Workspaces section reads the tab count the same way.
Replace the built-in panel or add a panel?
  • Decided: replace
  • package.json disables only @jupyterlab/running-extension:sidebar (jupyterlab.disabledExtensions); @jupyterlab/running-extension:plugin stays, so other extensions still register their sections
  • The new panel provides IRunningSessionSidebar, keeps the id jp-running-sessions and the restorer name running-sessions (saved layouts open it in the same place) and registers running:show-panel again
  • Sections that other extensions add (here: Recently Closed, Launcher application) are listed under "Other sessions", each item with the actions the replaced panel gave it: a shutdown button, its child rows, and Shut Down All with its confirmation where the section offers it; Language servers is not one of them, its rows are listed in In no workspace
Source of running-extension 4.6.4: the sidebar is its own plugin, restorer.add(running, 'running-sessions'); jupyterlab_server reads disabledExtensions from an extension's package.json (config.py, lines 170 to 173).

4. Design B, refined

At load: no card is open

{{svg:b2-closed}}

After a click on the card of this window's workspace

{{svg:b2-open}}

Icons

{{svg:b2-legend}}

Legibility

Measured as WCAG contrast ratio of text to its background; WCAG AA asks 4.5:1 for text under 18px. Galaxa Dark Theme - Steel sets --jp-ui-font-size1 to 11px and its colour 2 (#7d8791) reads 3.3:1 even on the plain panel, so the refined cards keep colour 2 out of every text. Each value is the lowest of the six workspace colours.

TextFirst draft of BRefinedGalaxa SteelLight
Workspace name12px, colour 1, 12% tint--jp-ui-font-size2 (13.2px), semibold, colour 0, 8% tint3.7 → 5.614.8 → 19.8
Purpose11px italic, colour 2size 1 (11px), regular, colour 02.5 → 5.64.2 → 19.8
Age and counts11px, colour 2size 1, colour 0; glyphs colour 12.5 → 5.64.2 → 19.8
Header of this window's workspacecolour 2 on 22% tint (open card)colour 0 on the active shade2.0 → 6.74.0 → 9.9
Row detail (terminal name, notebook)11px, colour 2size 1, colour 1 on a neutral body3.3 → 4.84.6 → 16.1
Row detail in this window's open cardcolour 2colour 1 on the body (40% of the active shade)3.3 → 4.84.6 → 12.6
Row detail on a hovered rowcolour 2 on layout 2colour 0 under a 12% veil of the text colour2.4 → 5.24.0 → 7.7
"now"brand colour 1colour 0, semibold, plus a brand colour 2 dot3.8 → 5.64.6 → 19.8

5. Behaviour common to the panel

Panel header

New workspace

{{svg:b2-new}}

Filter

{{svg:b2-filter}}

Workspace menu

{{svg:b2-menu}}

Candidates for the Open workspace icon, each as a hover button beside close-all-tabs and enlarged. Decided by the Star Colonel: 2, the arrow in a circle. An arrow into a box read as exit, and two opposite arrows were rejected.

{{svg:open-icons}}

Hidden workspaces

{{svg:b2-hidden}}

Rules of the cards

6. Workspace colours

{{svg:palette}}

7. Working with the culler sibling

8. Sidebar icon

{{svg:icons}}

9. Choices, all decided on 2026-10-23

ChoiceDecision
1. Visual designB, tinted cards, refined
2. Filterevery card with a match is open while the filter is active (recommendation, accepted with "ok")
3. Countstabs, terminals, notebooks in each card header; the number of cards in the panel header
4. Tab listeach tab once (recommendation, accepted with "ok")
5. Recordsthe extension's own file beside the workspaces (recommendation, accepted with "ok")
6. Coloursgiven automatically from the extension's own palette, which starts from the colourful tab's active tab shades; the user changes them in the workspace menu
7. Sidebar icon4, four panes
8. Hide this window's workspacenot possible (recommendation, accepted with "ok")
9. Clicks on a cardthe triangle opens or closes it; the header of another workspace switches to that workspace

10. Acceptance criteria

The criteria live in docs/acc-crit.md, written through pm-tools as @kj: 169 in 9 categories, none open, 161 closed and 8 rejected. Importance: 14 CRITICAL, 57 HIGH, 76 MEDIUM, 22 LOW. Each names its test: 57 with a Jest or pytest test, 128 with a Galata test (a criterion with both counts twice). pm-tools report docs/acc-crit.md prints the status.

11. Added while filling the criteria

A complete replacement of the Running panel needs these; they are filed as criteria and wait for the Star Colonel's veto.

Appendix - the alternatives compared on 2026-10-23

The first drafts, kept as the record of the choice; they are not maintained.

A. Accordion with a colour strip

{{svg:alt-a}}

B. Tinted cards, first draft (selected)

{{svg:alt-b}}

C. Colour rail

{{svg:alt-c}}