The design of the JupyterLab extension jupyterlab_bookmarks_manager_extension (PyPI jupyterlab-bookmarks-manager-extension) as it is built. Every statement was checked against src/, jupyterlab_bookmarks_manager_extension/, schema/plugin.json, style/base.css and ui-tests/ on 2026-10-28.
An id such as ACC-LINK-37-OPEN-TAB names an acceptance criterion in docs/acc-crit.md. Only closed criteria are cited, and each names the test that proves it. An id that starts with DEF- names a defect in docs/defects.md.
A side-bar panel with four sections. Links, Files and Sessions are each a tree of folders and bookmarks, kept in one JupyterLab setting; Recent Proxy is a flat list of the pages opened through the lab's server proxy, kept in the application state database. Two server routes read the foreground process of each terminal and fetch the favicon of a page on another origin.
jupyterlab_bookmarks_manager_extension:plugin, that adds a SidePanel, the tab-menu command, the drag of a tab onto the panel, the tab marks and the row commands; the tree functions of model/tree.ts are pure; Jest tests them with the store, the filter, the recent list, the marks and the drop target against fakesGET terminals and POST favicon under <base_url>/jupyterlab-bookmarks-manager-extension/, both authenticatedbookmarks of the plugin jupyterlab_bookmarks_manager_extension:plugin ACC-STORE-77-SETTING; the Recent Proxy list under the state key jupyterlab_bookmarks_manager_extension:recent-proxy, outside the settings file ACC-RECENT-93-SECTIONserver-proxy:open records its page; a row reopens the page the way it was first opened, and an entry leaves the list after recentProxyTtlDays days, 3 by default ACC-RECENT-94-OPEN ACC-RECENT-95-TTLmarkColour ACC-MARK-96-TAB-MARK ACC-MARK-97-OPEN-ROW ACC-MARK-98-MARK-COLOURsidebar:switch (Switch Sidebar Side in the context menu of the panel's side-bar tab) moves it to the right; the extension has no code for this ACC-PANEL-1-LEFT ACC-PANEL-2-RIGHTterminal:open and terminal:create-new are JupyterLab's own terminal commands; the tab they open connects to the terminal's websocket. On a narrow screen the figure scrolls sideways.The questions the design had to answer, the decision as it is built, and the reason for it.
| Question | Decision | Reason |
|---|---|---|
| Name of the section for web pages | Links |
The section was meant to hold lab-served pages, pages on other origins and a PNG file path; the working name Webpages did not fit the PNG path. The PNG path itself is out of scope |
| What a web tab is and how its URL is read | A tab whose widget has no document context and whose node holds exactly one <iframe> with an http or https address. The URL is iframe.contentWindow.location.href when it is readable and http(s), else iframe.src ACC-LINK-19-WEB-TAB ACC-LINK-21-URL |
All four producers of web tabs installed on the workstation - server-proxy:open, help:open, proxy-launcher:open and iframe:open - keep the URL in the iframe src, so one rule covers all of them. A frame on another origin refuses access to its location; src is then the address |
| How a link reopens | A page on the lab's origin opens in the extension's own MainAreaWidget around an IFrame with the sandbox allow-same-origin allow-scripts allow-popups allow-forms allow-downloads allow-modals and the widget id bookmarks-link-<id>; a second click activates that tab. A page on another origin opens with window.open(url, '_blank', 'noopener') ACC-LINK-37-OPEN-TAB ACC-LINK-38-OPEN-AGAIN ACC-LINK-39-OPEN-OTHER |
The frame that help:open makes has a sandbox that blocks cookies. The lab's Content-Security-Policy blocks a frame of another origin. The cost of the one code path: the tab does not come back after a lab reload |
| Where favicons are cached | In the link record, as a data URL of at most 32768 characters, fetched after the link is added and after its URL changes ACC-LINK-25-FAVICON ACC-LINK-26-FAVICON-CACHE ACC-LINK-27-FAVICON-EDIT ACC-LINK-32-FAVICON-CAP | The browser fetches a page on the lab's origin and its icon with the lab's credentials. A page on another origin cannot be fetched from the browser, so POST favicon fetches it on the server ACC-LINK-28-FAVICON-OTHER. Without an icon link the icon is favicon.ico beside the page, never at the origin root ACC-LINK-30-FAVICON-BESIDE. The Star Colonel kept the cap at 32768 characters on 2026-10-27 |
| Where bookmarks are stored | The setting bookmarks of the plugin jupyterlab_bookmarks_manager_extension:plugin in the settings registry ACC-STORE-76-RELOAD ACC-STORE-77-SETTING |
The registry keeps the value on the server and validates every save against schema/plugin.json; Galata's mockSettings gives each test its own value |
| What a file path is relative to | The Jupyter server root, as a contents API path: context.path, forward slashes, no leading slash; the file edit dialog strips leading slashes from a typed Location ACC-FILE-44-LOCATION |
docmanager:open and the contents API take paths in this form |
| How the terminal process is read and which variables are stored | os.tcgetpgrp(ptyproc.fd) gives the foreground process group of the terminal's pty. The group leader's argv comes from /proc/<pgid>/cmdline, joined by shlex.join; its cwd from readlink /proc/<pgid>/cwd; its variables are the ones added or changed against /proc/<shell pid>/environ. Never recorded: _, PWD, OLDPWD, SHLVL, LINES, COLUMNS and any name that contains TOKEN, SECRET, PASSWORD, PASSWD, API_KEY or CREDENTIAL in any case ACC-SESS-54-COMMAND ACC-SESS-55-CWD ACC-SESS-56-ENV ACC-SESS-57-SECRETS |
Measured on a live server. LINES and COLUMNS are excluded because LINES appeared in the measured diff. The handler reads manager.terminals and never calls get_terminal, which creates a terminal that does not exist |
| What "the same setup" means and how a session reopens | Figure 3 shows the click. The first match is a terminal this bookmark started whose shell has not yet read the typed recipe: GET terminals reports bookmark, the shell's JUPYTERLAB_BOOKMARK, equal to the bookmark id, and pending true. Otherwise a running terminal, with or without a tab, matches when its foreground process has the same shell-quoted command and the same cwd, read from a fresh GET terminals on the click. Of several matches, the one whose tab is open wins, otherwise the first listed. A match runs terminal:open {name}. No match: POST api/terminals {cwd, extra_env: {JUPYTERLAB_BOOKMARK: <id>}}, then terminal:create-new {name}, then session.send types export K='v' ...; <command> and Enter. A click while an earlier click of the same bookmark still runs gets that click's promise ACC-SESS-63-MATCH ACC-SESS-64-MATCH-SHELL ACC-SESS-65-OPEN-ACTIVE ACC-SESS-66-OPEN-START ACC-SESS-67-OPEN-CWD ACC-SESS-68-QUOTE ACC-SESS-79-OPEN-ATTACH ACC-SESS-84-OPEN-FRESH ACC-SESS-85-START-ONCE |
The click never uses the tab-menu snapshot: a process stopped or started in an open tab changes no terminal name, and runningChanged is meant to fire on a change of names only (@jupyterlab/services/src/terminal/manager.ts L240-251). Closing a terminal tab leaves the terminal running, because shutdownOnClose defaults to false; terminal:open looks for a tab by session name and otherwise runs terminal:create-new {name}, which connects to the running terminal. terminal:create-new {cwd} strips the leading slash of an absolute cwd, so the terminal is started with POST api/terminals. For about 3 s after a start the server reports only the shell, so without the marker a second click started a second terminal (DEF-SESS-7-START-TWICE). After Ctrl-C the marker stays but pending is false, so a click starts a new terminal |
| Does editing the environment and the command make sense | Yes. The bookmark is a launch recipe, text typed into a new terminal; an edit changes the bookmark and never a running process ACC-SESS-70-EDIT-COMMAND ACC-SESS-71-EDIT-ENV ACC-SESS-72-EDIT-CWD ACC-SESS-74-EDIT-ONLY | Recognition then uses the edited recipe, so a terminal still running the old command offers Add Tab to Bookmarks ACC-SESS-75-EDIT-MATCH |
| What the panel header holds and how large the title is | The title Bookmarks, then Filter (filterIcon) and Refresh (refreshIcon) as 24 px icon buttons, as in the header of the AI code assistants panel; Filter shows or hides a search box above the sections, Refresh re-checks every file bookmark and reads the running terminals again with its glyph spinning. The title takes the size of the section labels ACC-PANEL-86-REFRESH ACC-PANEL-87-FILTER |
The reference is the header of jupyterlab_ai_code_assistants_extension. .jp-SidePanel-header sets --jp-ui-font-size0 itself and the title set it again, so the title shrank twice to about 9 px; the header resets its font size to --jp-ui-font-size1 (DEF-PANEL-9-TITLE-SIZE) |
| How a bookmark or a folder is created in a section | The section header holds a + button that opens a menu with New Bookmark... and New Folder; New Bookmark opens the section's edit dialog with empty fields, and Save adds the bookmark in the Folder field's choice, None at the section root ACC-TREE-89-PLUS-MENU ACC-TREE-90-NEW-BOOKMARK |
One button for the two creations of a section; the dialog is the edit dialog, so the checks of an edit apply to a new bookmark too - the favicon of a link is fetched, a missing file is marked, and an excluded name disables Save |
| Which names a captured environment leaves out | The names of process_reader.py and the secret pattern, plus every name matching an expression of the setting sessionEnvExclude (* any run, ? one character, [...] one of a set, case kept; 29 expressions by default). A name typed in the edit dialog is kept; the dialog refuses only the built-in names and the secret pattern, not the expressions of the setting ACC-SESS-88-ENV-FILTER ACC-SESS-91-ENV-SETTING ACC-SESS-92-ENV-MANUAL |
The setting is read where the frontend stores a capture, so the server needs no settings access, and an edited list applies without a page reload. The default is the accepted 36-entry list merged into wildcards: PS? also matches PS3 and TERM* also matches TERMINFO |
| Which pages the Recent Proxy section lists | Every run of the command server-proxy:open, one entry per URL with the title, the arguments and the time of the run; the section shows the pages opened within recentProxyTtlDays days that no link bookmarks, newest first, and a click runs the command again with the stored arguments ACC-RECENT-93-SECTION ACC-RECENT-94-OPEN ACC-RECENT-95-TTL |
The command is the one definition of a proxy page, and its arguments are what a reopen needs: an open lab tab is reused by its id and a page first opened in a browser tab opens in a browser tab again. Filtering by the links at render takes the row away while the page is bookmarked and brings it back while the entry lives |
| How an open bookmark shows on its tab and in its row | A 7 by 11 px ribbon hangs from the bookmarked tab's top edge, 3 px from its right border, with a notch at the bottom; a 7 px dot marks the open bookmark's row at its right end, and the current tab's row takes the brand selection tint. The setting markColour picks brand (--jp-brand-color1) or red (--jp-error-color1) ACC-MARK-96-TAB-MARK ACC-MARK-97-OPEN-ROW ACC-MARK-98-MARK-COLOUR |
The ribbon is the tab label's ::after: jupyterlab_colourful_tab_extension paints a background with !important on every element of a coloured tab, and a pseudo-element is not an element. The mark stays in the marker colour on such a tab |
widget.content.session.name is a string), not through ITerminalTracker, so @jupyterlab/terminal is not a dependencysudo, an exited group leader) is recorded as a plain shell ACC-SESS-59-UNREADABLEprocess_reader.py imports fcntl and termios inside the pty read, so the package loads without them and no unread input is recorded (DEF-SESS-10-WINDOWS-IMPORT)The types of src/model/types.ts:
export type SectionKey = 'links' | 'files' | 'sessions';
export interface IFolder {
kind: 'folder';
id: string;
name: string;
children: Node[];
}
export interface ILink {
kind: 'link';
id: string;
name: string;
/** Absolute http(s) URL, or a path relative to the lab base URL for a lab-served page. */
url: string;
/** Favicon data URL, absent when the page has none or it exceeds the cap. */
icon?: string;
}
export interface IFileBookmark {
kind: 'file';
id: string;
name: string;
/** Contents API path, relative to the server root. */
path: string;
}
export interface ISessionBookmark {
kind: 'session';
id: string;
name: string;
cwd: string;
/** Shell-quoted command line; empty for a plain shell. */
command: string;
env: Record<string, string>;
}
export type Node = IFolder | ILink | IFileBookmark | ISessionBookmark;
export interface IBookmarks {
version: 1;
links: Node[];
files: Node[];
sessions: Node[];
}
additionalProperties: false in the schema, and the schema checks the kind of a node, not whether that kind belongs to the section that holds it.^https?:// (any case). toStoredUrl stores a URL under the lab base URL relative to it and any other URL absolute; the relative form survives a change of host name ACC-LINK-24-RELATIVEid is UUID.uuid4() from @lumino/coreutils<first word of the command, or "shell"> - <base name of the cwd> ACC-LINK-20-MENU-ADD ACC-FILE-40-MENU-ADD ACC-SESS-60-NAMEAn example value of the setting:
{
"version": 1,
"links": [
{
"kind": "link",
"id": "7b1f",
"name": "Resources",
"url": "proxy/7681/",
"icon": "data:image/png;base64,iVBORw0KGgo..."
},
{
"kind": "folder",
"id": "c20a",
"name": "Apps",
"children": [
{ "kind": "link", "id": "91de", "name": "MLflow", "url": "mlflow/" }
]
}
],
"files": [
{
"kind": "file",
"id": "4a77",
"name": "README.md",
"path": "private/jupyterlab/jupyterlab_bookmarks_extension/README.md"
}
],
"sessions": [
{
"kind": "session",
"id": "e5c3",
"name": "python3 - tmp",
"cwd": "/tmp",
"command": "python3 -m http.server 8000",
"env": { "BM": "a b" }
}
]
}
The Recent Proxy list is stored outside this setting, under the state key jupyterlab_bookmarks_manager_extension:recent-proxy, as an array of IRecentProxyEntry of src/recent/recent.ts, newest first:
export interface IRecentProxyEntry {
/** The absolute URL of the page. */
url: string;
/** The title the command ran with. */
title: string;
/** The page opened in a browser tab, never in a lab tab. */
newBrowserTab: boolean;
/** The widget id the command opened, for a lab tab. */
id?: string;
/** Milliseconds since the epoch of the last open. */
openedAt: number;
}
pruneRecent keeps the entries whose age is at most recentProxyTtlDays × 86400000 ms; an entry exactly at the limit stays, and a TTL of 0 keeps only an entry opened at the same millisecond ACC-RECENT-95-TTLrecentWithoutBookmarks leaves out a page whose URL, in the stored form, a link of the Links section bookmarks, at the root or in a folderbookmarks: an object with the required properties version (const: 1), links, files and sessions, each an array of #/definitions/node; additionalProperties: false; default { "version": 1, "links": [], "files": [], "sessions": [] } ACC-STORE-77-SETTINGdefinitions/node is a oneOf of folder, link, file and session; a folder's children refer back to node, so the format accepts folders inside folders ACC-STORE-78-NESTEDicon carries maxLength: 32768sessionEnvExclude: an array of strings, title Session environment filter, default the 29 expressions of ACC-SESS-88 - JUPYTER*, JPY_*, PYXTERM_*, BASH_*, TERM*, HIST*, PS?, TMUX*, SSH_*, XDG_*, DBUS_*, _CE_*, _CONDA_*, VIRTUAL_ENV_*, PATH and CUDA_VISIBLE_DEVICES among them; the Settings Editor shows it as a list of entries ACC-SESS-88-ENV-FILTER ACC-SESS-91-ENV-SETTINGrecentProxyTtlDays: a number, minimum 0, default 3, title Recent proxy days ACC-RECENT-95-TTLmarkColour: a string, enum ["brand", "red"] with the names Brand and Red, default brand, title Marker colour ACC-MARK-98-MARK-COLOURjupyter.lab.setting-icon names the extension's own LabIcon, jupyterlab_bookmarks_manager_extension:bookmarks, with the label Bookmarks$refBase: <base_url>/jupyterlab-bookmarks-manager-extension/, registered by routes.py. Every verb method carries @tornado.web.authenticated; .github/scripts/check_auth.py fails make test when a handler lacks it, and an unauthenticated GET terminals answers 403.
| Route | Verb | Request | Response |
|---|---|---|---|
terminals |
GET | none | 200 {"terminals": [{"name", "cwd", "command", "env", "bookmark", "pending"}]}, one entry per terminal in terminal_manager.terminals; env maps names to strings, bookmark is a string, pending a boolean |
favicon |
POST | {"url": str}, the URL of a page, scheme http or https |
200 {"icon": str | null}; 400 when the body is not JSON, has no url, or the url is not a string with the scheme http or https ACC-LINK-33-FAVICON-INPUT |
self.settings["terminal_manager"].terminals; an absent manager gives {"terminals": []}read_terminal(pid, fd) of process_reader.py in an executor, because /proc reads blockbookmark: the shell's JUPYTERLAB_BOOKMARK, or "". pending: FIONREAD on /proc/<shell pid>/fd/0 reports unread input, the recipe line a click sent before the shell was readycommand is "", cwd is the shell's cwd, env is {} ACC-SESS-58-SHELL ACC-SESS-59-UNREADABLEcommand is shlex.join(argv), cwd is the leader's cwd, and env is the added-or-changed diff without the excluded names of section 2process_reader.py are applied on the server; the expressions of sessionEnvExclude are not, because the setting is read in the frontend where a capture is stored ACC-SESS-88-ENV-FILTERfcntl and termios are absent: both are imported inside the pty read, and a missing one gives pending false, so the server part loads on Windows (DEF-SESS-10-WINDOWS-IMPORT)/proc. Where it is absent, or os.tcgetpgrp is, the shell gives an empty cwd, command, env and bookmark, so each terminal is listed with empty fields rather than an errorAsyncHTTPClient: 5 s timeout per request, 1 MB body limithtml.parser and takes the href of the first <link> whose rel holds the word icon, so rel="shortcut icon" counts; an href that starts with data:image/ is returned as it is, within the same cap ACC-LINK-29-FAVICON-ROUTEurljoin(<page URL after redirects>, "favicon.ico"), beside the page ACC-LINK-30-FAVICON-BESIDEimage/* content type and fit in 32768 characters as a data URL; otherwise icon is null ACC-LINK-32-FAVICON-CAP{"icon": null}, not an error, because the icon is optional ACC-LINK-31-FAVICON-NONEServer files under jupyterlab_bookmarks_manager_extension/: routes.py (registration), terminals_handler.py, process_reader.py (/proc reading, the diff, the exclusions, the marker and the unread input), favicon_handler.py and favicon_fetch.py.
Modules under src/:
| Module | Purpose |
|---|---|
index.ts | The plugin: loads the setting, builds the store, the recent list, the terminal snapshot, the marks and the panel, adds the panel to the left side bar, registers the commands, the tab menu and the drag, and records every run of server-proxy:open |
request.ts | The template helper requestAPI: calls a route under <base_url>/jupyterlab-bookmarks-manager-extension/ and parses the JSON answer |
icons.ts | bookmarksIcon, the panel's own LabIcon, named jupyterlab_bookmarks_manager_extension:bookmarks and painted with jp-icon3. Every other icon is a stock icon of @jupyterlab/ui-components: folderIcon, linkIcon, terminalIcon, fileIcon, addIcon, filterIcon, refreshIcon; the caret is a text glyph (▾ or ▸) |
commands.ts | The ids of the nine commands; registers New Bookmark, New Folder, Rename Folder, Delete Folder, Edit Bookmark, Remove Bookmark, Add to Bookmarks and Remove from Recent, the row context menus and blank(section), the empty bookmark of a section's kind |
tab_menu.ts | The toggle-tab command and its two tab-bar context-menu items; its Add runs addTabBookmark |
tab_add.ts | addTabBookmark, the one add the tab menu and the drag onto the panel run: into the kind's section or into the folder a drop landed in, kept when the section already holds the bookmark, a terminal read again, the captured env filtered and a link's favicon fetched |
model/types.ts | The types of section 3 |
model/tree.ts | Pure functions over one section: findNode, bookmarksIn, countBookmarks, updateNode, addNode, removeNodes, moveNode, and the filter's matchesFilter and filterNodes; none mutates its input, every one accepts any depth |
model/store.ts | BookmarkStore: reads settings.composite.bookmarks. update(section, change) queues the write on one promise chain; each write calls registry.reload(plugin), applies change to the fresh section and saves with set; a failed write is logged and the next one runs. changed is the setting's own signal, emitted after every save and every reload. The reload keeps a second lab window, loaded earlier, from writing its old copy over a bookmark saved since ACC-STORE-83-OTHER-WINDOW. The getters sessionEnvExclude, recentProxyTtlDays and markColour read the composite at each access, so an edit applies without a page reload |
model/names.ts | Default names: fileName(path) and sessionName(command, cwd) |
classify/tab_kind.ts | classifyTab(widget, contextForWidget) gives {kind: 'file', path}, {kind: 'terminal', name}, {kind: 'link', url} or null; terminalName(widget) by duck typing |
classify/bookmark_match.ts | tabMatches(kind, terminals, baseUrl), the bookmark that records a tab's file, page or terminal, and SECTION, the section of each tab kind; the tab menu, the drag and the marks share both |
link/link_url.ts | toStoredUrl, toAbsoluteUrl and isSameOrigin |
link/favicon.ts | fetchFavicon: the lab's origin from the browser, another origin through POST favicon; capIcon at 32768 characters; storeFavicon writes the icon only while the link still has the URL it was fetched for |
link/open_link.ts | openLink: a lab tab for the lab's origin, a browser tab for any other origin |
file/file_status.ts | isFileMissing: contents API get(path, {content: false}); only a 404 counts as missing, any other error is logged and leaves the file unmarked ACC-FILE-50-CHECK-ERROR |
file/file_icon.ts | fileTypeIcon: registry.getFileTypeForModel({path}).icon, else fileIcon |
session/shell_quote.ts | shellQuote, exportLine(env, command), parseEnvLines (refuses an invalid line and the excluded names of section 2) and formatEnvLines |
session/env_filter.ts | The setting sessionEnvExclude: matchesEnvName on *, ? and [...] with case kept, and filterCapturedEnv, applied where a captured env is stored ACC-SESS-88-ENV-FILTER ACC-SESS-91-ENV-SETTING |
session/terminal_snapshot.ts | ITerminalInfo, sameSetup, findOpenTerminal(recipe, terminals, openNames), fetchTerminals, and TerminalSnapshot: one snapshot shared by the tab menu's label and the marks, refreshed on runningChanged, when the context menu opens on a terminal tab, on Add and after a change of the setting |
session/open_session.ts | openSession: the click on a session bookmark, figure 3 |
mark/marks.ts | markValue, the setting's value as brand or red; TabMarks, which classifies every main-area tab, matches the bookmarks with tabMatches, writes data-bookmark on the tab element for the ribbon, and exposes open and current with changed for the panel ACC-MARK-96-TAB-MARK ACC-MARK-97-OPEN-ROW |
recent/recent.ts | IRecentProxyEntry, pruneRecent (TTL), recordRecent (newest first, one entry per URL), recentWithoutBookmarks, matchesRecent, formatAgo, and RecentProxyStore over IStateDB: load, record, remove, changed ACC-RECENT-93-SECTION ACC-RECENT-95-TTL |
recent/open_recent.ts | openRecent: a row click runs server-proxy:open again with the stored url, title, newBrowserTab and id ACC-RECENT-94-OPEN |
panel/bookmarks_panel.ts | BookmarksPanel, a SidePanel: the top bar (title, Filter, Refresh), the filter box, the body with the three bookmark sections and the Recent Proxy section, the + menu of the bookmark section headers, click, keyboard and focus handling, the missing-file state and the fold state |
panel/section_view.ts, panel/row_view.ts | DOM of one section (its header with the fold toggle and the + button), one folder or bookmark row with the open dot and the current-tab state; renderRecentSection and its row with the age column |
panel/tab_drop.ts | The pointer drag of a main-area tab onto the panel: dropTarget resolves the section or the folder under the pointer, and the release adds through addTabBookmark ACC-PANEL-99-DRAG-TAB |
panel/fold_state.ts | The collapsed ids in localStorage, read and written in try/catch |
dialogs/form.ts | The dialog body table, textInput, showEditDialog (which sends one input event once the dialog is ready, so a New Bookmark dialog starts with Save disabled) and the Cancel and Save buttons |
dialogs/folder_select.ts | The Folder <select> of the three dialogs, dialogTitle (New Bookmark for a bookmark the section does not hold), saveEdit and folderChoice |
dialogs/edit_link.ts, edit_file.ts, edit_session.ts | The three dialog bodies and their showEditDialog calls; the same dialog opens for New Bookmark... with empty fields |
ISettingRegistry, IDocumentManager and IStateDB; ILayoutRestorer is optionalregistry.load(PLUGIN_ID), then the BookmarkStore, the RecentProxyStore, one TerminalSnapshot, the TabMarks and the BookmarksPanel; then void recent.load() is started and not awaited, because the state database's fetch waits for the workspace load, which runs after every plugin has activated, so awaiting it inside activation would deadlock the start; then app.shell.add(panel, 'left', { rank: 600, type: 'Bookmarks' }), restorer.add(panel, panel.id), addCommands, addTabMenu, addTabDrop and the commandExecuted hook that records every run of server-proxy:openpackage.json: @jupyterlab/application, apputils, docmanager, docregistry, settingregistry, statedb and ui-components at ^4.6.0; @jupyterlab/coreutils ^6.6.0; @jupyterlab/services ^7.6.0; @lumino/coreutils ^2.2.0; @lumino/signaling ^2.1.6; @lumino/widgets ^2.10.0. @jupyterlab/terminal is not a dependencyAll in the namespace jupyterlab_bookmarks_manager_extension:.
| Id | Label | Where |
|---|---|---|
toggle-tab | Add Tab to Bookmarks or Remove Tab from Bookmarks | Tab context menu, rank 25 |
edit | Edit Bookmark... | Bookmark row context menu, first group |
remove | Remove Bookmark | Bookmark row context menu, last group below a separator, class jp-BookmarksContextMenu-danger ACC-TREE-18-REMOVE |
new-bookmark | New Bookmark... | The + menu of a bookmark section header, which runs it with {section} ACC-TREE-90-NEW-BOOKMARK |
new-folder | New Folder | The + menu of a bookmark section header, which runs it with {section} ACC-TREE-89-PLUS-MENU |
rename-folder | Rename Folder... | Folder row context menu, first group ACC-TREE-15-RENAME-FOLDER |
delete-folder | Delete Folder | Folder row context menu, last group below a separator, danger class; removes the folder and its bookmarks after a confirmation dialog that names their number ACC-TREE-16-DELETE-FOLDER ACC-TREE-17-DELETE-CANCEL |
add-recent | Add to Bookmarks | Recent Proxy row context menu, first group; adds the page at the Links root and fetches its favicon ACC-RECENT-94-OPEN |
remove-recent | Remove from Recent | Recent Proxy row context menu, second group; the entry is a history row, so it is not destructive and stands below no separator ACC-RECENT-94-OPEN |
The row menus use the selectors .jp-BookmarksPanel-row:not(.jp-BookmarksPanel-folder):not(.jp-BookmarksPanel-recent) for bookmarks, .jp-BookmarksPanel-folder for folders and .jp-BookmarksPanel-recent for Recent Proxy rows, so the three menus never mix. The bookmark and folder menus carry edit or rename-folder at rank 1, a separator at rank 2 and the destructive entry at rank 3; the Recent Proxy menu carries its two entries at rank 1 and rank 2.
#jp-main-dock-panel .lm-DockPanel-tabBar .lm-TabBar-tab and #jp-down-stack .lm-TabBar-tab[data-id], rank 25app.contextMenuHitTest(node => !!node.dataset.id) gives the tab's data-id, looked up in app.shell.widgets('main') and app.shell.widgets('down')classifyTab: a document context from IDocumentManager.contextForWidget → file, path context.path; widget.content.session.name a string → terminal; exactly one iframe with an http(s) address → link; anything else → isVisible is false, and Lumino renders the item with lm-mod-hidden ACC-PANEL-5-NO-ITEMRemove Tab from Bookmarks when a bookmark of that kind matches - a link with the same absolute URL, a file with the same path, a session with the same command and cwd as the snapshot entry of that terminal name - and Add Tab to Bookmarks otherwise ACC-LINK-22-MENU-LABEL ACC-FILE-41-MENU-LABEL ACC-SESS-61-MENU-LABELaddTabBookmark of tab_add.ts ACC-LINK-23-MENU-REMOVE ACC-FILE-42-MENU-REMOVE ACC-SESS-62-MENU-REMOVEGET terminals fresh and records that entry's cwd, command and env, so the recipe is current even when the snapshot was old; the captured env passes filterCapturedEnv with the setting sessionEnvExclude, read at that moment, so an edited list applies without a page reload ACC-SESS-53-MENU-ADD ACC-SESS-88-ENV-FILTERserviceManager.terminals.runningChanged (a failed refresh is dropped, DEF-SESS-8-ROUTE-MISSING), when the context menu opens on a terminal tab (app.contextMenu.opened), on Add of a terminal, and after every change of the setting, because the tab menu and the marks share one snapshot. After each refresh and each change of the setting the code calls commands.notifyCommandChanged and app.contextMenu.menu.update(), because a Lumino menu redraws only on update()pointerdown, pointermove and pointerup on the document, because Lumino moves a tab by pointer events, and the tab's own move between dock areas is left alone ACC-PANEL-99-DRAG-TABaddTabBookmark, so the checks are the tab menu's: a bookmark the section already holds is kept, a terminal is read again, the captured env is filtered and a link gets its faviconjp-mod-dropTarget, a 2 px --jp-brand-color1 outline inset by 2 px over an 8% brand wash; the mark goes on the release and when the pointer leaves the panel, and Escape and the context menu abort the dragelementFromPoint reads the panel because Lumino's tab drag covers the pointer with its cursor backdrop; the lookup drops the backdrop behind the page for the call, as Lumino's own drop code doesopenLink turns the stored URL into the absolute URL; another origin → window.open(url, '_blank', 'noopener')bookmarks-link-<id> is activated when it is open, its URL set again when it differs; otherwise a new MainAreaWidget around a sandboxed IFrame, titled with the link name and linkIcon, closableServerConnection.makeRequest for the page; when it answers OK with an HTML content type, DOMParser and link[rel~="icon" i], else favicon.ico resolved against the page URL; a data: href is taken as it is; the icon must answer OK with an image/* type and at most 32768 bytes, and is read as a data URL with FileReader, then capped at 32768 characterslinkIcon ACC-LINK-31-FAVICON-NONEA click or Enter on a session row runs openSession(app, bookmark). The match is made against a fresh GET terminals, never against the snapshot of the tab menu: click → match → switch, or click → no match → new terminal.
app.commands.commandExecuted; a run whose id is server-proxy:open with a string url is recorded as {url, title (the URL when absent), newBrowserTab: args.newBrowserTab === true, id (a string only)} with the time of the run, one entry per URL, and saved to the state database ACC-RECENT-93-SECTIONrecentProxyTtlDays and Date.now(), drops the pages a link bookmarks and applies the filter box's text; the panel redraws on RecentProxyStore.changed, so a record, a removal, a bookmark and a change of the setting show without a page reloadserver-proxy:open again with the stored arguments: an open lab tab whose id is id is activated, otherwise created, and newBrowserTab opens a browser tab and nothing else ACC-RECENT-94-OPENserver-proxy:open for a restored proxy tab, so that run is recorded tooTabMarks reads every main-area widget, classifies it as the tab menu does and matches the bookmarks with tabMatches; a match writes data-bookmark - brand or red, the setting markColour - on the tab element, and the stylesheet paints the ribbon from it ACC-MARK-96-TAB-MARK::after, 7 px wide and 11 px tall, hanging from the tab's top edge 3 px from its right border with a notch at the bottom; a marked tab takes 14 px of right padding where an unmarked one takes 8, so the tab is 6 px wider, and its close glyph is 15 px where the stock glyph is 16 pxjupyterlab_colourful_tab_extension colours, because that extension paints a background with !important on the elements of the tab, not on a pseudo-elementTabMarks syncs on currentChanged, on the shell's layoutModified (the shell is a DockPanel), on the snapshot's changed and after every change of the setting, so a tab shows or loses its ribbon without a page reload ACC-MARK-96-TAB-MARKopen holds the ids of the bookmarks with an open tab and current the current tab's bookmark; the panel redraws on TabMarks.changed and paints the row dot and the tint ACC-MARK-97-OPEN-ROWmarkValue reads any value but red as brand; the two map to --jp-brand-color1 and --jp-error-color1, through the tab element's data-bookmark and the panel root's data-mark ACC-MARK-98-MARK-COLOUREvery visible part follows the Extension Design System of the jupyterlab-extension skill: classes jp-BookmarksPanel-<component> with jp-mod-<state>, colours from --jp-* variables chosen by role.
jp-BookmarksPanel. The rows of a section sit in jp-BookmarksPanel-sectionBody, the section's scroller; the filter box, when its top-bar button shows it, stands between the header and the sections. The panel's tab on the side-bar rail shows bookmarksIcon. On a narrow screen the figure scrolls sideways.app.shell.add(panel, 'left', { rank: 600, type: 'Bookmarks' }); panel id jupyterlab_bookmarks_manager_extension-panel; title.icon is bookmarksIcon, title.caption is Bookmarks; ILayoutRestorer.add(panel, panel.id) when the restorer is present; the side-bar rail is not styledBookmarksPanel extends SidePanel with { content: new Panel() } and house DOM sections inside; no AccordionPaneljp-BookmarksPanel-header in the SidePanel header, on --jp-layout-color2, holds the title jp-BookmarksPanel-title with the text Bookmarks - weight 600, uppercase, 1 px letter spacing, --jp-ui-font-size0, margin-left: 14px, flex: 1 1 auto - and the Filter and Refresh buttons at its right. .jp-BookmarksPanel .jp-SidePanel-header resets the header's font size to --jp-ui-font-size1: the SidePanel header sets --jp-ui-font-size0 itself and the title set it again, so without the reset the title computed at about 9 px (DEF-PANEL-9-TITLE-SIZE). It now computes to the size of the section labelsjp-BookmarksPanel: flex column, height: 100%, min-width: 0, --jp-layout-color1, --jp-ui-font-size1jp-BookmarksPanel-body: flex column; it scrolls vertically when the four sections are taller than the panelstyle/base.css: the panel rules are scoped under .jp-BookmarksPanel, the dialog rules carry the extension's own classes (jp-BookmarksPanel-dialogBody, -form, -banner, -dialogButton), and the dialog's width and custom properties sit on .jp-Dialog-content:has(.jp-BookmarksPanel-dialogBody); the danger class of the context menu is not scoped, because a Lumino menu attaches to the page body, and the tab-mark rules are scoped to .lm-DockPanel-tabBar, because a tab lives outside the panel. Colours come from --jp-* variables, apart from the black of the recessed input surface; font sizes are --jp-ui-font-size0 and --jp-ui-font-size1; no font-family on labelsfilterIcon) then Refresh (refreshIcon), class jp-BookmarksPanel-iconButton: a 24 px box with a 14 px glyph, title and aria-label Filter bookmarks and Refresh. The header has no right padding, so the last button stands in the column of the section headers' buttons ACC-PANEL-86-REFRESH ACC-PANEL-87-FILTER--jp-layout-color3; jp-mod-active with aria-pressed="true" on Filter while its box shows, background --jp-layout-color3 and glyph --jp-brand-color1; a disabled button takes opacity 0.4, a default cursor and no hover background. The buttons never hidejp-mod-spinning, which turns the glyph (@keyframes jp-bookmarks-spin, 0.9 s). It checks every bookmarked file path again, reads the running terminals again (serviceManager.terminals.refreshRunning()) and waits at least 500 ms, then redraws the four sections and enables the button; a failed terminal read is logged and the files still redraw ACC-PANEL-86-REFRESHjp-BookmarksPanel-filter, a Lumino widget in the SidePanel content above the sections that starts hidden; showing it focuses the input, hiding it clears the text. The input jp-BookmarksPanel-filterInput is type="search", 24 px high, on the recessed surface of the dialog inputs, 1 px --jp-border-color2 and --jp-brand-color1 on focus, with the placeholder and aria-label Filter bookmarksinput event redraws. A non-empty trimmed text narrows each section to filterNodes(nodes, node => matchesFilter(node, text)): a bookmark whose name, or URL (the stored form), path or command contains the text, case ignored; a folder stays when it holds a match. The counts NAME (N) count the matches, and a section without one shows No matching bookmarks. ACC-PANEL-87-FILTERdiv.jp-BookmarksPanel-section[data-section] in the order links, files, sessions, recent, each with a 1 px --jp-border-color3 bottom border ACC-PANEL-3-SECTIONS ACC-RECENT-93-SECTIONjp-BookmarksPanel-sectionHeader: a sticky band on --jp-layout-color2 with the toggle <button class="jp-BookmarksPanel-sectionToggle" aria-expanded> - the caret and the label LINKS (N), N the number of bookmarks with folders not counted, shown also when collapsed and when 0 - and, on the three bookmark sections, a sibling + button; no button nests in a button ACC-TREE-9-SECTION-FOLDjp-BookmarksPanel-iconButton jp-BookmarksPanel-plus: the 14 px addIcon in the 24 px box, title New Bookmark or Folder, aria-label New Bookmark or Folder in <Section>, aria-haspopup="menu" and aria-expanded; it replaces the single New Folder button of ACC-TREE-10-NEW-FOLDER. The Recent Proxy header has none, because its rows are not created there ACC-TREE-89-PLUS-MENUMenuSvg of the panel, emptied and filled on each click with new-bookmark and new-folder and the args {section}, opened at the button's rect.left and rect.bottom. While the menu is open the button carries jp-mod-active, the open state of the top bar buttons, and aria-expanded="true"; aboutToClose removes both and focuses the button, so the focus returns to itjp-BookmarksPanel-sectionBody with role="tree": the section's scroller, overflow-y: auto; scrollbar-gutter: stable; max-height: clamp(5 rows, 33vh, 10 rows) at 24 px a row; not rendered while the section is collapsedjp-BookmarksPanel-empty, italic, --jp-ui-font-color2, --jp-ui-font-size0, with the text No links. Use Add Tab to Bookmarks on a tab., and the same with files and sessions; the Recent Proxy section reads No recent proxy pages. and a section without a match under the filter reads No matching bookmarks. ACC-PANEL-4-EMPTYjp-BookmarksPanel-row: role="treeitem", aria-level, data-id the node id, 24 px high. All rows of a section are siblings in its body; a row at depth d is indented by 4px + d × 16px through --jp-bookmarks-depthjp-BookmarksPanel-caret (a 14 px column, ▾ or ▸ on a folder, empty on a bookmark so that the icons line up), the icon jp-BookmarksPanel-rowIcon (16 px), the name jp-BookmarksPanel-rowName (flex: 1 1 auto; min-width: 0) with the text span jp-BookmarksPanel-rowText, the only part that ellipsises, and, on an open bookmark's row, the 7 px dot jp-BookmarksPanel-rowMark at the right end, aria-hidden; the aria-label of such a row gains , openjp-BookmarksPanel-row jp-BookmarksPanel-folder with aria-expanded and folderIcon; a click folds or unfolds it; jp-mod-collapsed marks a folded folder or section ACC-TREE-7-FOLDlocalStorage key jupyterlab_bookmarks_manager_extension:collapsed, read and written in try/catch; an unreadable value means all expanded; it stays out of the settings file ACC-TREE-8-FOLD-KEPTicon in an <img>, else linkIcon; a file row shows the icon of its file type, which falls back to the text type and then to fileIcon; a session row shows terminalIcon ACC-LINK-25-FAVICON ACC-FILE-43-ICONtitle): a link shows its absolute URL; a file its path, or File not found: <path>; a session <cwd>: <command> ACC-FILE-47-MISSING-ROW ACC-SESS-53-MENU-ADDopenSession; a file is checked first - missing: the row is marked, showErrorMessage shows File not found: <path> and no tab opens; found: the mark clears and docmanager:open {path} runs ACC-FILE-45-OPEN ACC-FILE-81-OPEN-MISSINGevent.detail > 1) is ignored, so a double-click neither starts a session twice nor folds a folder back (DEF-SESS-5-DOUBLE-CLICK).jp-BookmarksPanel-row:not(.jp-BookmarksPanel-folder):not(.jp-BookmarksPanel-recent)), and the Recent Proxy rows carry Add to Bookmarks and Remove from Recent. The destructive entry carries jp-BookmarksContextMenu-danger and stands last below a separator; its label and its icon are --jp-error-color1jp-mod-missing (the name in --jp-ui-font-color2 and struck through; the aria-label <name>, File not found and the tooltip carry the words, so colour is never the only sign), jp-mod-collapsed, jp-mod-active (the focused row, --jp-layout-color3), jp-mod-current (the current tab's bookmark row, color-mix(in srgb, var(--jp-brand-color1) 14%, var(--jp-layout-color1)); hover and the focused row paint over it), jp-mod-dropTarget (the section header or folder row a dragged tab would land in, a 2 px --jp-brand-color1 outline inset by 2 px over an 8% brand wash); hover --jp-layout-color2TabMarks.changed, so a tab opening, closing or switching adds, moves or removes them without a page reload ACC-MARK-97-OPEN-ROWjp-BookmarksPanel-row jp-BookmarksPanel-recent, data-id the page's absolute URL, aria-level 1, 24 px high; the link icon, the title in -rowName, and the age in -rowTime, a fixed right column at --jp-ui-font-size0, --jp-ui-font-color2 and tabular figures ACC-RECENT-93-SECTIONformatAgo(now - openedAt) - now, Nm ago, Nh ago, Nd ago, Nmo ago, Ny ago with whole-number floors; the row's title is the page URL and its aria-label is <title>, <age>jp-mod-recentlyActive and the title and the age paint --jp-brand-color1; over seven days it carries jp-mod-stale and the title takes opacity 0.65, never the row, which would dim the agerecent lives in the same localStorage value as the section keys and the folder idsshowEditDialog opens a JupyterLab Dialog with the title Edit Bookmark or New Bookmark, the body, the buttons and focusNodeSelector: 'input', so the dialog opens on the first field. The dialog checks its required fields only on an input event, so the helper waits for dialog.ready and sends one, and Save starts disabled while a required field is empty. The body root jp-BookmarksPanel-dialogBody holds the table jp-BookmarksPanel-form: a 42 % label column and a 58 % field column, 40 px rows, 28 px controls; the dialog is min(560px, 100vw - 32px) wide--jp-brand-color1) and stands last, so Enter resolves it; Cancel is a surface button; the focus rim shows for keyboard focus only--jp-layout-color1, never the theme's input colour; jp-mod-error or :invalid gives an --jp-error-color1 border. The JupyterLab dialog disables Save while a field is :invalid or carries jp-mod-error| Kind | Fields |
|---|---|
| Link | Name (required); URL (required; shows the absolute URL, and Save stores it again in the stored form of section 3); Folder ACC-LINK-34-EDIT-NAME ACC-LINK-35-EDIT-URL ACC-LINK-36-URL-EMPTY |
| File | Name (required) with a type="button" Reset Name beside it that writes the file name of the Location into Name - a footer button would resolve the dialog; Location (required; leading slashes are stripped on Save); Folder; the banner File not found (jp-BookmarksPanel-banner: padding: 6px 12px; background: var(--jp-layout-color2); border-left: 2px solid var(--jp-warn-color1)), checked when the dialog opens and on each change of Location ACC-FILE-48-MISSING-EDIT ACC-FILE-51-EDIT-NAME ACC-FILE-52-RESET-NAME ACC-FILE-80-LOCATION-EMPTY |
| Session | Name (required); Working directory; Command; Environment, a 3-row textarea of KEY=VALUE lines in which Enter stays a newline; Folder ACC-SESS-69-EDIT-NAME ACC-SESS-70-EDIT-COMMAND ACC-SESS-71-EDIT-ENV ACC-SESS-72-EDIT-CWD |
<select>: None plus the root folders of the section, set to the folder that holds the bookmark; a changed choice moves the bookmark, None to the section root; the Folder field is the only way a bookmark moves between folders, because a row is not draggable ACC-TREE-11-MOVE-IN ACC-TREE-12-MOVE-OUT ACC-TREE-13-ONE-LEVEL^[A-Za-z_][A-Za-z0-9_]*= and must not name a variable that the recording excludes; otherwise the textarea gets jp-mod-error and Save is disabled. Blank lines are skipped. The exclusion applies to typed variables too, so secrets never reach the settings file ACC-SESS-73-ENV-INVALID. The expressions of sessionEnvExclude do not apply to a typed name: PATH=/opt/tool/bin is kept and exported on the next start ACC-SESS-92-ENV-MANUALlinkIcon until then ACC-LINK-27-FAVICON-EDITInputDialog.getText; the name is trimmed, and an empty name changes nothing. Delete Folder asks Delete the folder "<name>" and the N bookmarks in it? (bookmark for one) with Cancel and a warn button, Deleteblank(section): a new id and empty fields - Links Name and URL, Files Name and Location, Sessions Name, Working directory, Command and Environment - with the Folder field at None. The title is New Bookmark; a Name and a URL or Location are required, so Save starts disabled. Save runs the edit's own path: the bookmark the section does not hold is added in the Folder field's choice, None at the section root, and the checks are the edit's - the link's favicon is fetched, a file path the panel has not seen is checked and marked, an excluded name keeps Save disabled ACC-TREE-90-NEW-BOOKMARKThree tiers. Every criterion in docs/acc-crit.md names its test; each closed one was closed with the passing test run as evidence. make test runs Jest, pytest and .github/scripts/check_auth.py.
test_process_reader.py: exclusions by exact name and by pattern; the kept name (PATH) and the user's variable survive a capture while the secret pattern match goes; the shipped default of sessionEnvExclude is the accepted 29 expressions; the added-or-changed diff; an empty command when the shell is in the foreground; an unreadable group leader recorded as a shell; a shell-quoted command; the marker and the unread input of a real pty; the package importing with fcntl and termios blocked, giving an empty command, an empty env and pending false ACC-SESS-88-ENV-FILTER (DEF-SESS-10-WINDOWS-IMPORT)test_terminals.py: an unauthenticated GET terminals answers 403; a real terminal started with POST api/terminals {"cwd": tmp} and ptyproc.write("export K=v; sleep 30\r") is listed with the command sleep 30, that cwd and K in env; teardown manager.terminate(name, force=True); server configuration jpserver_extensions["jupyter_server_terminals"] = True and terminado_settings = {"shell_command": ["/bin/sh"]}test_favicon.py, against a local http.server thread: a rel="shortcut icon" link and a data: href; no icon link → favicon.ico beside the page, not the one at the root; no icon at all → null; an oversize icon → null; 400 for a body that is not JSON, for {}, for an ftp: URL and for a URL that is not a stringcheck_auth.py: every verb carries the decoratortest extras of pyproject.toml hold coverage, pytest, pytest-asyncio, pytest-cov, pytest-jupyter[server] and jupyterlabtree.spec.ts: add to the section root and to a folder; add to a folder that does not exist → root; remove a bookmark, and a folder with its contents; rename; move between folders and back to the root; count bookmarks, not folders; find a node with its folder; a folder two levels deep; no mutation of the input; filterNodes keeps the matches and the folders that hold them and drops a folder without a match; matchesFilter matches the name, the URL, the path and the command, case ignored, and not the cwd ACC-PANEL-87-FILTERstore.spec.ts: with a fake registry whose reload returns a setting that holds a bookmark another window saved, a write keeps it; two writes started together both land; a failed write does not stop the next one; sessionEnvExclude is read at each access, so a replaced list applies without a reload ACC-SESS-91-ENV-SETTINGtab_kind.spec.ts: link only for one http(s) iframe, not for two iframes, a javascript: or file: iframe or a plain widget; the address the frame shows, its src when unreadable; a document widget gives file, also with an iframe inside; a terminal gives terminallink_url.spec.ts: a lab URL is stored relative, a remote one and one on another port absolute; a relative one joins back on another base URL; a path typed with a leading slash resolves on the originshell_quote.spec.ts: exportLine quotes values and leaves out an empty part; parseEnvLines accepts K=a=b and rejects 1K=x, GITHUB_TOKEN=x and a line without =; formatEnvLines reads back through parseEnvLinesenv_filter.spec.ts: * any run, ? one character and [...] one of a set, case kept, a dot literal; filterCapturedEnv keeps the names no expression matches and leaves the input unchanged for an empty list ACC-SESS-88-ENV-FILTER ACC-SESS-91-ENV-SETTINGterminal_snapshot.spec.ts: findOpenTerminal matches on command and cwd, not on name; an empty command matches any shell in the cwd; of two matches, the one with an open tab wins, otherwise the first listed; a poll answered 404 leaves no unhandled rejectionmarks.spec.ts: markValue keeps red and reads an absent or unknown value as brand ACC-MARK-98-MARK-COLOURrecent.spec.ts: at a fixed clock with a TTL of 3 an entry opened 2 or 3 days ago stays and one opened 4 days ago goes, and a TTL of 0 drops the entries opened before the render; a reopened page moves to the top and one URL keeps one row; a page a link bookmarks at the root or in a folder is left out, comparing the stored form; the age labels now, 1m ago, 2h ago, 3d ago, 1mo ago, 1y ago ACC-RECENT-93-SECTION ACC-RECENT-95-TTLtab_drop.spec.ts: dropTarget reads a folder row of the tab kind's section as that folder, any other part of the panel as the section root - a folder row of another section included - and an element outside the panel as no drop ACC-PANEL-99-DRAG-TABfavicon.spec.ts, file_status.spec.ts, fold_state.spec.ts, names.spec.ts: the 32768-character cap; no icon when favicon.ico beside the page answers 404; only 404 counts as missing; an unreadable or malformed fold state means all expanded; the default namesEverything visible; assertions are on text, attributes and counts, never on snapshots alone. The specs with mocked settings import test from helpers.ts, whose page goes offline before Galata's teardown, so a settings write still running when a test ends never reaches the shared test server.
panel.spec.ts: the panel on the left; Switch Sidebar Side moves it to the right; four section headers with counts - LINKS (0), FILES (0), SESSIONS (0), RECENT PROXY (0); the empty hints; the item on the Launcher tab is hidden (toBeHidden()) ACC-PANEL-3-SECTIONS ACC-PANEL-5-NO-ITEMpanel.spec.ts): the title's computed font size equals a section label's and a probe at --jp-ui-font-size0 inside --jp-ui-font-size1 (DEF-PANEL-9-TITLE-SIZE); Refresh on a bookmark of bookmarks-refresh/note.txt, a folder of its own in the server root because the mocked setting is fixed before the page loads - marked missing on load, the file uploaded, Refresh spins disabled, then the mark clears, and after the file is deleted the mark stays until Refresh sets it (ACC-PANEL-86-REFRESH); Filter on a folded folder and one bookmark per section - aria-pressed, a part of one name in another case leaves that row and its open folder with the counts of the matches, the URL, the path and the command each match one row, and an empty box and a hidden box bring every row and the folded folder back (ACC-PANEL-87-FILTER)panel.spec.ts): a page tab dragged onto the Links section and a file tab dragged onto a folder row, each with the pointer resting on the target while jp-mod-dropTarget is asserted on the section header or the folder row; the release adds the row at aria-level 1 in Links and inside the folder in Files (ACC-PANEL-99-DRAG-TAB); the drag is Playwright's mouse down, move and up, because Lumino moves a tab by pointer eventsfiles.spec.ts: Add on a document tab, the label turns to Remove, Remove; the file-type icon; the location relative to the server root; a click opens the file; a bookmark of a deleted file is marked missing, on Add and on load; the banner for a missing location; a location change updates the mark; Edit of the name; Reset Name; an empty Location disables Save; a click on a missing file opens no tablinks.spec.ts: web tabs opened with help:open on pages under /files/; Add, the recorded URL, the Remove label, Remove; the favicon on the row; the favicon after a reload with no request for the page or the icon (the tab closes before the reload, because a restored help:open tab requests its page again); a URL edit refetches the favicon; a page without a favicon keeps linkIcon; Edit of the name and the URL; an empty URL disables Save; a click opens a lab tab and a second click activates it. Another origin: a Node http server on a free port of 127.0.0.1 serves a page and its icon; a link edited to that page gets its favicon through POST favicon, and a click opens it in a browser tabsessions.spec.ts, the only spec that starts terminals: a terminal running cd /tmp; export K=v; sleep 300 is added as sleep - tmp with the tooltip /tmp: sleep 300; the edit dialog shows the recipe; Remove label and Remove; a bookmarked terminal tab carries the ribbon ACC-MARK-96-TAB-MARK; a click activates the running terminal; after DELETE api/terminals/<name> a click starts a terminal whose screen shows export K='v'; sleep 300; a double-click starts it once; a second click while it starts opens no second terminal; a plain shell bookmark reopens in its cwd; a capture of export JPY_ONE=x K=v stores only K=v, so the shipped default JPY_* applies, and with sessionEnvExclude set to FILTER_* a capture of export FILTER_ONE=x K=v stores only K=v ACC-SESS-88-ENV-FILTER ACC-SESS-91-ENV-SETTING; Edit to PATH=/opt/tool/bin and a click starts a terminal whose echo $PATH prints it ACC-SESS-92-ENV-MANUAL; a new bookmark starts with empty fields, GITHUB_TOKEN=x disables Save, and a click starts a terminal whose screen shows the typed recipe ACC-TREE-90-NEW-BOOKMARK; Edit of the name, the command, the environment and the cwd; an invalid environment line disables Save; an edit leaves the running terminal alone; the edited recipe is used for recognition; a click reopens a running terminal whose tab was closed; a click after Ctrl-C starts the session. Terminal waits have their own 20 s timeouttabmark.spec.ts: the ribbon on a bookmarked file tab and on a help:open web tab, removed with the bookmark; markColour set through the registry repaints the ribbon and the row dot without a page reload; with two bookmarked files open both rows carry the dot, the current tab's row is tinted, the tint moves with the tab switch and the closed file's dot is gone; a jp-colourful-tab-mint class on a marked tab leaves the ribbon in the marker colour ACC-MARK-96-TAB-MARK ACC-MARK-97-OPEN-ROW ACC-MARK-98-MARK-COLOURrecent.spec.ts: a page opened by running server-proxy:open with a proxy URL shows RECENT PROXY (1) with its title and the age now; the section folds with its count kept; a reload keeps the list; a link naming that page's URL makes a Links row and takes the Recent row away ACC-RECENT-93-SECTION; a closed page's row reopens its tab and a second click reuses it, Add to Bookmarks makes a Links row and removes the Recent row, Remove from Recent drops the entry, and a click moves the row to the top ACC-RECENT-94-OPEN; the setting Recent proxy days shows in the Settings Editor, and setting it to 0 through the registry empties the section without a page reload ACC-RECENT-95-TTLtree.spec.ts: the tree starts expanded; a folder folds and unfolds; the fold state survives a reload; a section header folds the section; in each section the + button opens a menu whose items read New Bookmark... and New Folder, with aria-expanded="true" on the button, and New Folder adds the folder at the root ACC-TREE-89-PLUS-MENU; New Bookmark opens empty fields under the title New Bookmark, a link to a same-origin page becomes a root row with the page's data: icon and opens its tab, and a file at a path that does not exist yet becomes a root row marked missing and opens after the upload ACC-TREE-90-NEW-BOOKMARK; Edit moves a bookmark into a folder and back; the Folder field lists only root folders; Rename Folder; Delete Folder after the confirmation and Cancel; Remove Bookmark; the keyboard: Tab reaches the Files rows, Down moves to the next row, Enter opens its file, Enter on a folder folds itstorage.spec.ts, on the real settings registry of the test server: the bookmarks survive a reload, they are in the plugin setting, and the setting accepts a folder inside a foldermake install first, then in ui-tests/ JUPYTER_TEST_PORT=<free port> jlpm playwright test > <log> 2>&1; make build alone uninstalls the wheel (its clean step), so make install runs again before the suiteJUPYTER_TEST_PORT moves the test server and the base URL together; the suite never adopts a server it did not start; serverFiles: 'on' keeps each test folder until the server stops (DEF-SUITE-2-ROOT-LISTING)playwright.config.js sets no workers or fullyParalleljupyter_server_test_config.py: a private user-settings folder; deleted folders are removed, not sent to the trash; the shell is /bin/bash --rcfile with PS1="$ ", and a shell that carries JUPYTERLAB_BOOKMARK waits 3 s before its prompt, as a slow login shell doesscreenReaderMode: true for @jupyterlab/terminal-extension:plugin, and the tests read .xterm-accessibility-tree; without it the screen text is not in the DOMterminals_available, which the lab server provides by defaultfile:// links and a route that serves files outside the server root, such as /tmp/paste-files/...png, because the workstation's jupyter_lab_config.py turned local-file serving off; the Star Colonel decided this on 2026-10-27jupyterlab-favorites/procITerminalTracker dependency, AccordionPanel resizing