Bookmarks panel - design

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.

1. Overview

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.

  • Frontend: one plugin, 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 fakes
  • Server: GET terminals and POST favicon under <base_url>/jupyterlab-bookmarks-manager-extension/, both authenticated
  • Storage: the setting bookmarks 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-SECTION
  • Recent Proxy: every run of the command server-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-TTL
  • Marks: a main-area tab whose page, file or terminal a bookmark records carries a ribbon on its top edge, its panel row a dot, and the current tab's row is tinted; the colour is the setting markColour ACC-MARK-96-TAB-MARK ACC-MARK-97-OPEN-ROW ACC-MARK-98-MARK-COLOUR
  • Top bar: the title with Filter and Refresh; Refresh checks every file bookmark and reads the running terminals again, Filter narrows the sections to a typed text ACC-PANEL-86-REFRESH ACC-PANEL-87-FILTER
  • Drag: a main-area tab dropped on the panel is added as a bookmark of its kind, inside the folder row under the pointer ACC-PANEL-99-DRAG-TAB
  • Side: the panel starts in the left side bar; JupyterLab's own command sidebar: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-RIGHT
Figure 1. Components The frontend modules in the browser, grouped into UI modules and logic modules, and the five server parts they call: the settings registry, the contents API, the two routes of this extension, the terminals API and the state database that holds the Recent Proxy list. Browser: JupyterLab frontend, src/ UI LOGIC AND SERVER ACCESS index.ts the plugin; wires the parts panel/ bookmarks_panel.ts section_view.ts row_view.ts fold_state.ts tab_drop.ts tab_menu.ts tab_add.ts Add or Remove Tab commands.ts dialogs/ form.ts, folder_select.ts edit_link.ts, edit_file.ts edit_session.ts icons.ts bookmarksIcon, the panel icon model/ types.ts, tree.ts store.ts, names.ts BookmarkStore: queued writes classify/ tab_kind.ts bookmark_match.ts tab kind to its bookmark file/ file_status.ts file_icon.ts link/ link_url.ts favicon.ts open_link.ts request.ts requestAPI session/ shell_quote.ts env_filter.ts terminal_snapshot.ts open_session.ts mark/ marks.ts recent/ recent.ts open_recent.ts Jupyter server Settings registry /lab/api/settings/<plugin id> setting bookmarks, validated on save Contents API /api/contents/<path> This extension, routes.py /jupyterlab-bookmarks-manager-extension/ GET terminals terminals_handler.py process_reader.py reads /proc for the shell and its foreground job POST favicon favicon_handler.py favicon_fetch.py fetches the page and its icon reads the terminal list Terminals API, jupyter_server_terminals POST /api/terminals terminals/websocket/<name> terminal_manager.terminals State database, per workspace IStateDB, the workspace state the Recent Proxy list, outside the settings file reload, set get, content: false GET, POST GET terminals fetch, save
Figure 1. Components. The UI modules on the left column call the logic modules on the right column; the arrows are the extension's calls to the server's APIs. The Recent Proxy list goes to the application state database, which JupyterLab's workspace plugin writes to the workspace file. terminal: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.

2. Decisions taken

The questions the design had to answer, the decision as it is built, and the reason for it.

QuestionDecisionReason
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

Further decisions

  • A shell in the foreground gives an empty command; such a bookmark reopens a shell in its cwd, and any idle shell in that cwd matches it ACC-SESS-58-SHELL ACC-SESS-64-MATCH-SHELL
  • Variables that login scripts set appear in the diff and are kept; the edit dialog lets the user delete their lines
  • Terminal tabs are recognised by duck typing (widget.content.session.name is a string), not through ITerminalTracker, so @jupyterlab/terminal is not a dependency
  • Every terminal is bookmarkable; a terminal whose foreground process cannot be read (sudo, an exited group leader) is recorded as a plain shell ACC-SESS-59-UNREADABLE
  • Processes that rewrite their argv (npm, npx) stay out of scope: the Star Colonel rejected DEF-SESS-6-ARGV-TITLE on 2026-10-28, because a process title cannot give back the true command line
  • The terminal tab-menu label also refreshes when the context menu opens on a terminal tab, so the label right after Add reads Remove within 2 seconds ACC-SESS-61-MENU-LABEL
  • The server part imports on Windows: process_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)

3. Data model

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[];
}
Figure 2. The stored node format The setting bookmarks holds version 1 and three arrays of nodes. A node is one of four kinds: link, file bookmark, session bookmark or folder; a folder's children are nodes again. Settings registry → plugin jupyterlab_bookmarks_manager_extension:plugin → setting bookmarks bookmarks IBookmarks version: 1 links: Node[] files: Node[] sessions: Node[] Node #/definitions/node oneOf the four kinds below ILink kind: 'link' id: string name: string url: string icon?: string url: relative under the lab base URL, else absolute http(s) icon: data URL, at most 32768 characters IFileBookmark kind: 'file' id: string name: string path: string path: contents API path, relative to the server root, forward slashes, no leading / ISessionBookmark kind: 'session' id: string name: string cwd: string command: string env: {string: string} command: shell-quoted, empty for a plain shell; env: the added or changed variables IFolder kind: 'folder' id: string name: string children: Node[] children: any depth in the format; the UI makes folders at the section root only children → Node, recursive
Figure 2. The stored node format. Every node object has 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.
  • A stored URL is relative when it does not match ^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-RELATIVE
  • id is UUID.uuid4() from @lumino/coreutils
  • The UI adds only bookmarks of a section's kind to that section and its folders. It creates folders at the section root only, and the Folder field lists root folders only; the type and the tree functions accept any depth ACC-TREE-13-ONE-LEVEL ACC-TREE-14-DEPTH
  • The file-exists state is computed at run time and never stored
  • Default name: a link takes the tab title; a file takes the file name; a session takes <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-NAME

An 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;
}
  • One entry per URL: a new open of a known page moves its entry to the top and replaces its fields ACC-RECENT-93-SECTION
  • 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-TTL
  • recentWithoutBookmarks leaves out a page whose URL, in the stored form, a link of the Links section bookmarks, at the root or in a folder
  • The workspace plugin saves the state database to the workspace file, so the list survives a page reload

Setting schema, schema/plugin.json

  • Property bookmarks: 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-SETTING
  • definitions/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-NESTED
  • A link's icon carries maxLength: 32768
  • Property sessionEnvExclude: 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-SETTING
  • Property recentProxyTtlDays: a number, minimum 0, default 3, title Recent proxy days ACC-RECENT-95-TTL
  • Property markColour: a string, enum ["brand", "red"] with the names Brand and Red, default brand, title Marker colour ACC-MARK-98-MARK-COLOUR
  • jupyter.lab.setting-icon names the extension's own LabIcon, jupyterlab_bookmarks_manager_extension:bookmarks, with the label Bookmarks
  • The registry validates every save against the schema
  • Unverified: how the Settings Editor form renders the recursive $ref
  • Load cost: the registry sends the whole setting to the browser at lab start; 100 links at the cap add up to 3.2 MB (calculated, not measured)

4. Server API

Base: <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.

RouteVerbRequestResponse
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

terminals

  • Reads a copy of self.settings["terminal_manager"].terminals; an absent manager gives {"terminals": []}
  • Each entry is read by read_terminal(pid, fd) of process_reader.py in an executor, because /proc reads block
  • bookmark: 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 ready
  • The foreground group leader is the shell, or cannot be read: command is "", cwd is the shell's cwd, env is {} ACC-SESS-58-SHELL ACC-SESS-59-UNREADABLE
  • Otherwise command is shlex.join(argv), cwd is the leader's cwd, and env is the added-or-changed diff without the excluded names of section 2
  • The excluded names of process_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-FILTER
  • The package imports where fcntl 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)
  • Everything else reads /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 error

favicon

  • Fetches the page with its own Tornado AsyncHTTPClient: 5 s timeout per request, 1 MB body limit
  • Parses the HTML with html.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-ROUTE
  • No icon link: urljoin(<page URL after redirects>, "favicon.ico"), beside the page ACC-LINK-30-FAVICON-BESIDE
  • The icon answer must have an image/* content type and fit in 32768 characters as a data URL; otherwise icon is null ACC-LINK-32-FAVICON-CAP
  • A fetch error or an HTTP error answer gives {"icon": null}, not an error, because the icon is optional ACC-LINK-31-FAVICON-NONE
  • The server fetches any http(s) URL for an authenticated user; this grants nothing that a terminal does not

Server 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.

5. Frontend

Modules under src/:

ModulePurpose
index.tsThe 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.tsThe template helper requestAPI: calls a route under <base_url>/jupyterlab-bookmarks-manager-extension/ and parses the JSON answer
icons.tsbookmarksIcon, 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.tsThe 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.tsThe toggle-tab command and its two tab-bar context-menu items; its Add runs addTabBookmark
tab_add.tsaddTabBookmark, 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.tsThe types of section 3
model/tree.tsPure 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.tsBookmarkStore: 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.tsDefault names: fileName(path) and sessionName(command, cwd)
classify/tab_kind.tsclassifyTab(widget, contextForWidget) gives {kind: 'file', path}, {kind: 'terminal', name}, {kind: 'link', url} or null; terminalName(widget) by duck typing
classify/bookmark_match.tstabMatches(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.tstoStoredUrl, toAbsoluteUrl and isSameOrigin
link/favicon.tsfetchFavicon: 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.tsopenLink: a lab tab for the lab's origin, a browser tab for any other origin
file/file_status.tsisFileMissing: 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.tsfileTypeIcon: registry.getFileTypeForModel({path}).icon, else fileIcon
session/shell_quote.tsshellQuote, exportLine(env, command), parseEnvLines (refuses an invalid line and the excluded names of section 2) and formatEnvLines
session/env_filter.tsThe 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.tsITerminalInfo, 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.tsopenSession: the click on a session bookmark, figure 3
mark/marks.tsmarkValue, 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.tsIRecentProxyEntry, 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.tsopenRecent: a row click runs server-proxy:open again with the stored url, title, newBrowserTab and id ACC-RECENT-94-OPEN
panel/bookmarks_panel.tsBookmarksPanel, 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.tsDOM 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.tsThe 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.tsThe collapsed ids in localStorage, read and written in try/catch
dialogs/form.tsThe 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.tsThe 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.tsThe three dialog bodies and their showEditDialog calls; the same dialog opens for New Bookmark... with empty fields

Plugin

  • Requires ISettingRegistry, IDocumentManager and IStateDB; ILayoutRestorer is optional
  • Activation: registry.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:open
  • Dependencies in package.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 dependency

Commands

All in the namespace jupyterlab_bookmarks_manager_extension:.

IdLabelWhere
toggle-tabAdd Tab to Bookmarks or Remove Tab from BookmarksTab context menu, rank 25
editEdit Bookmark...Bookmark row context menu, first group
removeRemove BookmarkBookmark row context menu, last group below a separator, class jp-BookmarksContextMenu-danger ACC-TREE-18-REMOVE
new-bookmarkNew Bookmark...The + menu of a bookmark section header, which runs it with {section} ACC-TREE-90-NEW-BOOKMARK
new-folderNew FolderThe + menu of a bookmark section header, which runs it with {section} ACC-TREE-89-PLUS-MENU
rename-folderRename Folder...Folder row context menu, first group ACC-TREE-15-RENAME-FOLDER
delete-folderDelete FolderFolder 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-recentAdd to BookmarksRecent Proxy row context menu, first group; adds the page at the Links root and fetches its favicon ACC-RECENT-94-OPEN
remove-recentRemove from RecentRecent 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.

Tab menu, toggle-tab

  • Items on #jp-main-dock-panel .lm-DockPanel-tabBar .lm-TabBar-tab and #jp-down-stack .lm-TabBar-tab[data-id], rank 25
  • The widget: app.contextMenuHitTest(node => !!node.dataset.id) gives the tab's data-id, looked up in app.shell.widgets('main') and app.shell.widgets('down')
  • The kind, by 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-ITEM
  • The label reads Remove 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-LABEL
  • Execute performs the verb the label showed. Add never removes, and it adds nothing when a match exists by then; Remove removes every match. The tab menu and the drag of a tab onto the panel run the same add, addTabBookmark of tab_add.ts ACC-LINK-23-MENU-REMOVE ACC-FILE-42-MENU-REMOVE ACC-SESS-62-MENU-REMOVE
  • Add of a terminal reads GET 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-FILTER
  • Add of a link stores the tab title as the name and the URL in stored form, then fetches the favicon ACC-LINK-20-MENU-ADD
  • The snapshot refreshes on serviceManager.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()

Drag a tab onto the panel

  • A press on a main-area tab that moves 5 px is a drag, the threshold of Lumino's own tab drag; the drop is followed by capture-phase 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-TAB
  • Where it lands: a folder row of the tab kind's section takes the bookmark inside that folder; every other part of the panel takes it at the tab kind's section root, including a folder row of another section, because a folder holds the bookmarks of one kind; outside the panel nothing lands
  • The release adds through addTabBookmark, 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 favicon
  • While the pointer rests over the panel the target section header or folder row carries jp-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 drag
  • elementFromPoint 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 does

Opening a link

  • openLink turns the stored URL into the absolute URL; another origin → window.open(url, '_blank', 'noopener')
  • The lab's origin: the main-area widget with the id 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, closable
  • The favicon of a page on the lab's origin: ServerConnection.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 characters
  • Any error while fetching a favicon gives a console warning and no icon; the row keeps linkIcon ACC-LINK-31-FAVICON-NONE

Opening a session bookmark

A 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.

Figure 3. The click on a session bookmark Three checks in order: an open of the same bookmark still running, a terminal with the bookmark marker and pending input, and a terminal with the same command and cwd. A match runs terminal:open; no match starts a new terminal through POST api/terminals, terminal:create-new and session.send. Click or Enter on a session row the second click of a double-click is ignored (event.detail > 1) Is an open of this bookmark still running? opening: a map of promises by bookmark id yes Return that open's promise this click starts nothing no Read GET terminals now, never the snapshot and the names of the open terminal tabs, main and down areas A terminal with bookmark = this id and pending = true? JUPYTERLAB_BOOKMARK marks the shell; pending: unread input on its pty yes no findOpenTerminal: the same command and the same cwd? an empty command matches any idle shell in that cwd of several matches: the one with an open tab, else the first listed yes no Switch terminal:open {name} activates the terminal's tab, or opens a tab on the running terminal when its tab was closed; starts no terminal POST api/terminals {cwd, extra_env: {JUPYTERLAB_BOOKMARK: id}} terminal:create-new {name} opens a tab connected to the new terminal session.send: the export line and Enter export K='v' ...; <command> nothing is sent when env and command are both empty New terminal starts in the stored cwd, also an absolute path outside the server root its shell carries JUPYTERLAB_BOOKMARK, so a click before the shell reads the line finds it through pending and starts no second one after Ctrl-C the marker stays, pending is false, and a click starts a new terminal
Figure 3. The click on a session bookmark, from the marker-and-pending match to a switch or a new terminal. The marker check exists because for about 3 s after a start the server reports only the shell, so the command check does not yet find the new terminal. On a narrow screen the figure scrolls sideways.

Recent Proxy

  • Recording: the plugin connects 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-SECTION
  • Reading: each render prunes the list with recentProxyTtlDays 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 reload
  • A click runs server-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-OPEN
  • The layout restorer re-runs server-proxy:open for a restored proxy tab, so that run is recorded too
  • Add to Bookmarks adds a link at the Links root, named with the entry's title and with its URL in stored form, then fetches its favicon; the Recent row leaves the section because the link bookmarks its URL. Remove from Recent removes the entry; the page stays open and a later run records it again

Marks

  • TabMarks 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
  • The ribbon is the tab label's ::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 px
  • The pseudo-element keeps the ribbon in the marker colour on a tab that jupyterlab_colourful_tab_extension colours, because that extension paints a background with !important on the elements of the tab, not on a pseudo-element
  • The mark follows the tabs and the bookmarks: TabMarks 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-MARK
  • open 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-ROW
  • markValue 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-COLOUR

6. Panel UI

Every 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.

Figure 4. Panel anatomy The panel beside its side-bar tab: the top bar with the title, Filter and Refresh; four sections with sticky headers and the + button on the three bookmark sections, a folder row, a bookmark row inside it, the current tab's tinted row with the open dot, a file row, a missing file row and a collapsed section; below, the section + menu and the bookmark, folder, Recent Proxy and tab context menus. BOOKMARKS ▾ LINKS (2) ▾ Apps MLflow Resources ▾ FILES (2) README.md notes.md ▸ SESSIONS (1) ▾ RECENT PROXY (2) MLflow UI now Proxy 7681 2h ago -header: the top bar with the title, Filter and Refresh -sectionHeader: sticky band with the -sectionToggle and the + button -folder row: caret ▾ or ▸, folderIcon; a click folds it -row in a folder: 24 px high, indented 16 px a level jp-mod-current: the current tab's row, tinted; -rowMark the open dot -plus: New Bookmark or Folder, 24 px button, 14 px glyph file row: the file-type icon; the tooltip is the path jp-mod-missing: muted, struck through; aria-label adds File not found jp-mod-collapsed section: the body is not rendered, the count stays RECENT PROXY: no + button; the age in -rowTime a recent row: jp-mod-recentlyActive under a minute, jp-mod-stale over seven days a row: hover --jp-layout-color2, focus jp-mod-active Section header: + menu New Bookmark... New Folder Bookmark row: context menu Edit Bookmark... Remove Bookmark Folder row: context menu Rename Folder... Delete Folder Recent Proxy row: context menu Add to Bookmarks Remove from Recent Document, web or terminal tab: context menu Add Tab to Bookmarks reads Remove Tab from Bookmarks when a bookmark matches hidden on a tab that shows no page, file or terminal
Figure 4. Panel anatomy. Class names without a prefix start with 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.

Layout

  • 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 styled
  • BookmarksPanel extends SidePanel with { content: new Panel() } and house DOM sections inside; no AccordionPanel
  • The header widget jp-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 labels
  • Root jp-BookmarksPanel: flex column, height: 100%, min-width: 0, --jp-layout-color1, --jp-ui-font-size1
  • Body jp-BookmarksPanel-body: flex column; it scrolls vertically when the four sections are taller than the panel
  • One stylesheet, style/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 labels

Top bar

  • Two icon buttons, Filter (filterIcon) 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
  • States: hover --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 hide
  • Refresh: the click disables the button and adds jp-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-REFRESH
  • Filter: the click shows or hides the box jp-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 bookmarks
  • Every input 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-FILTER
  • While a text narrows the sections the view's collapsed set holds only the section keys, so every folder shown is open; a click or Enter on a folder row does not fold it then, and the saved fold state returns when the box is empty or hidden

Sections

  • Four div.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-SECTION
  • Header jp-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-FOLD
  • The + button jp-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-MENU
  • The + menu: one MenuSvg 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 it
  • Body jp-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 collapsed
  • Empty section: jp-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-EMPTY
  • Every section and every folder starts expanded ACC-TREE-6-EXPANDED

Rows and folders

  • Row jp-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-depth
  • Parts in order: the caret jp-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 , open
  • Folder row jp-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-FOLD
  • Fold state: the collapsed section keys and folder ids in the localStorage 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-KEPT
  • Icons: a link row shows its stored icon 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-ICON
  • Tooltip (title): 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-ADD
  • Click: a link opens as in section 5; a session runs openSession; 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-MISSING
  • The second click of a double-click (event.detail > 1) is ignored, so a double-click neither starts a session twice nor folds a folder back (DEF-SESS-5-DOUBLE-CLICK)
  • Per-row actions live in the row context menu - Edit and Remove on bookmarks, Rename and Delete on folders - and never in inline buttons, because a row click navigates. The bookmark selector excludes folders and Recent Proxy rows (.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-color1
  • Keyboard: the rows of a section are one tab stop (roving tabindex; the last focused row keeps the stop); Up and Down move the focus across the visible rows of the section, folder children included; Enter acts as a click, so it opens a bookmark, folds or unfolds a folder, and reopens a Recent Proxy page ACC-TREE-82-KEYBOARD
  • States: jp-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-color2
  • The open dot and the tint follow the tabs: the panel redraws on TabMarks.changed, so a tab opening, closing or switching adds, moves or removes them without a page reload ACC-MARK-97-OPEN-ROW
  • File existence: each path is checked once when it first appears - on panel load, after Add, after a location change - and again on every click and on Refresh ACC-FILE-46-MISSING-ADD ACC-FILE-49-MISSING-SAVE

Recent Proxy rows

  • One flat list, newest first, no folders: jp-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-SECTION
  • The age is formatAgo(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>
  • Recency emphasis: under 60 s the row carries 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 age
  • The filter box narrows this section as it narrows the others, matching the title and the URL; the count is the number of rows shown
  • A click reopens the page as section 5 describes; the fold key recent lives in the same localStorage value as the section keys and the folder ids

Edit dialogs

  • showEditDialog 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
  • Footer: Cancel, then Save. Save is the only filled button (--jp-brand-color1) and stands last, so Enter resolves it; Cancel is a surface button; the focus rim shows for keyboard focus only
  • Inputs: a recessed surface, 4% black over --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
KindFields
LinkName (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
FileName (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
SessionName (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
  • Folder <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
  • An Environment line must match ^[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-MANUAL
  • Saving a changed Link URL drops the old icon and fetches the icon of the new page; a new link gets its icon after the add, and the row shows linkIcon until then ACC-LINK-27-FAVICON-EDIT
  • New Folder and Rename Folder use InputDialog.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, Delete
  • New Bookmark... opens the dialog of the section's kind on blank(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-BOOKMARK

7. Tests

Three 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.

Pytest, jupyterlab_bookmarks_manager_extension/tests/

  • 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 string
  • check_auth.py: every verb carries the decorator
  • The test extras of pyproject.toml hold coverage, pytest, pytest-asyncio, pytest-cov, pytest-jupyter[server] and jupyterlab

Jest, src/__tests__/

  • tree.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-FILTER
  • store.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-SETTING
  • tab_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 terminal
  • link_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 origin
  • shell_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 parseEnvLines
  • env_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-SETTING
  • terminal_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 rejection
  • marks.spec.ts: markValue keeps red and reads an absent or unknown value as brand ACC-MARK-98-MARK-COLOUR
  • recent.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-TTL
  • tab_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-TAB
  • favicon.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 names

Galata, ui-tests/tests/

Everything 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-ITEM
  • The top bar (panel.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)
  • Drag (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 events
  • files.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 tab
  • links.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 tab
  • sessions.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 timeout
  • tabmark.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-COLOUR
  • recent.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-TTL
  • tree.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 it
  • storage.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 folder

Galata test server and run

  • The suite tests the installed extension: make 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 suite
  • JUPYTER_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's default parallelism: the spec files run in parallel workers, and the tests of one file run in order in one worker; playwright.config.js sets no workers or fullyParallel
  • jupyter_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 does
  • Terminal screen text: the test settings set screenReaderMode: true for @jupyterlab/terminal-extension:plugin, and the tests read .xterm-accessibility-tree; without it the screen text is not in the DOM
  • The terminal tests need terminals_available, which the lab server provides by default

Out of scope

  • file:// 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-27
  • Pages on another origin inside a lab tab
  • Reopening a link tab after a lab reload
  • Folders deeper than one level in the UI; dragging a bookmark row or a folder to move it; sort and group options
  • Import and export; sync between lab instances; a filter per root folder as in jupyterlab-favorites
  • A check that a stored cwd still exists; a missing cwd opens a terminal in the server's directory
  • Pipelines (only the group leader of the foreground process group is recorded), processes that rewrite their argv, processes under another uid, and support for a platform without /proc
  • Translation strings, an ITerminalTracker dependency, AccordionPanel resizing