Skip to content

2026 10 29 session uris

kubed-io[bot] edited this page Oct 29, 2026 · 1 revision

Resource URIs: session:// for what dies with the browser: Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: A browser's downloads are session://downloads[/{name}] and GET /browser/downloads. With no session open they say so instead of answering empty. workspace://files/downloads… is refused with its new spelling, and workspace://files no longer names a downloads folder. Console, network, current and everything else keep their workspace:// URIs.

Architecture: FOLDER_URI[DOWNLOADS] takes the new URI, so uri_of, describe (keep_with), show, the spec builder and every other reader of that table follow with no code of their own. parse_uri learns the one new prefix. names.retired_uri becomes an ordered table of moved prefixes. FILE_ROUTES holds whole paths, so the listing can live under /browser while staying the files surface's endpoint. Everything else is words.

Tech Stack: Python 3.11+, FastMCP 4, Starlette, pytest; Svelte 5 and vitest for one chip in ui/.

Spec: docs/superpowers/specs/2026-10-29-session-uris-design.md, the draft from #99, finished in this pull request. Rulings 1-5 and 12 are Dr K's. Rulings 6-11 and 13 are proposals awaiting him. This plan builds them as proposed. If he rules otherwise, the task that owns the ruling changes, and the Design lines below name which.

Design

The spec is the design. This plan settles the following where the spec left the call to it:

  • The refusal table in names.py, tried in order. A prefix matches the URI itself or the URI followed by /, and only the first match answers (ruling 11):

    MOVED = (
        ("workspace://files/downloads", "session://downloads"),
        ("session://files/downloads", "session://downloads"),
        ("session://downloads", None),   # served: nothing to refuse
        ("session://", "workspace://"),  # the rename's ruling 5
    )

    session:// matches by plain prefix, since it is a scheme. The refusal reads `{text}` is now `{new + rest}`, exactly as today. RETIRED_SCHEME goes.

  • The no-session sentence is NO_DOWNLOADS in http/files.py, raised as faults.NoSession(NO_DOWNLOADS) (ruling 8):

    no session is open in this workspace; a download lives only as long as the browser that took it

    It is raised by folder(…, DOWNLOADS), by read_file and by keep, never by listing_of, which the admin's sections calls with an empty id and must keep answering [].

  • Without a DATA_DIR, root and folder for Files, Screenshots and Recordings raise OFF, whether a browser is open or not: nothing they list comes from the browser any more. NOTHING stays, for sections alone.

  • folder's result for downloads loses browser, which would always be true. root's folders are [{name, uri, count}] for screenshots and recordings. downloads_count is deleted.

  • FILE_ROUTES holds each path under the mount:

    FILE_ROUTES = {
        "list": ("get", "/files"),
        "screenshots": ("get", "/files/screenshots"),
        "downloads": ("get", "/browser/downloads"),
        "recordings": ("get", "/files/recordings"),
        "keep": ("put", "/files/{folder}/{name}/kept"),
    }

    _routes registers prefix + path. The listing's route name is browser_downloads. _file_paths publishes at f"{prefix}{template}" and tags an operation browser when its path starts with /browser, files otherwise (ruling 9).

  • The copy that changes, word for word:

    • SHAPES: "workspace://files/ for a file in Files, workspace://files/screenshots/ for a screenshot, workspace://files/recordings/ for a recording, or session://downloads/ for a download".
    • Files' DESCRIPTION, first paragraph: "The files in Files: prints, and anything kept with keep_file. Also names two folders — workspace://files/screenshots and workspace://files/recordings — each with its own listing. The open browser's downloads are session://downloads." The second paragraph stays.
    • FOLDER_DESCRIPTION[DOWNLOADS] gains a last sentence: "With no session open, reading it says so."
    • keep_file's description: "uri is as workspace://files, its folders and session://downloads list it."
    • show's DESCRIPTION: "…a workspace://, session://, flow://, secret:// or skill:// URI…".
    • read_resource's binary answer, for a URI under session://downloads/: "Not returned: binary content is not text. Read session://downloads for this file's link, which any HTTP client can download."
    • upload_file's docstring (mcp/tools.py), its file line: "file, by its uri: a file in workspace://files or its folders, or a download from session://downloads — without its bytes passing through you;". The refusal at core/actions.py:995 ends "…or file for any file this workspace has, by its workspace://files or session://downloads uri". The docstring at core/actions.py:965 says the same.
    • The handshake's READING_POINTER: "…a URI — workspace://current, session://downloads, flow://flows, secret://secrets and the like, …".
    • CAPTURE_KEPT in mcp/resources.py, appended to CONSOLE_DESCRIPTION, NETWORK_DESCRIPTION and the end of DESCRIPTION's capture paragraph:

      What was captured is the workspace's, not the browser's: it stays readable after the session ends, until capture.ttl passes with nothing new or open_session is called without capture.

    • OpenAPI: listFiles is summarised "Files' own listing, and its two folders." and described "Everything kept in Files, newest first, plus a count for workspace://files/screenshots and workspace://files/recordings — each its own listing, fetched separately so a caller who only wants to know whether there is anything to look at need not pay for either. The open browser's downloads are GET /browser/downloads." listDownloads is described "Newest first, read from the browser itself. With no session open it is a 400 saying so: a download lives only as long as the browser that took it. A Grid failure from one that is open is a real fault and is not hidden." FileFolder's description is "One of the two folders named beside Files' own files.", its name enum ["screenshots", "recordings"], and it has no browser. FolderList has no browser, and its description is "One folder's own listing: workspace://files/screenshots, workspace://files/recordings or session://downloads."
  • The app's chip (ruling 13): in ContextView.svelte, the chips block shows when data.site_data || data.capture || data.live, and while data.live it holds a Downloads chip with no pill. It is a button calling onshow('session://downloads') when onshow is given, and otherwise the same disabled span the other chips use.

If Dr K rules otherwise: ruling 7 changes the two strings in FOLDER_URI/MOVED (Task 1, Task 2); ruling 8 the one constant (Task 2); ruling 9 one row of FILE_ROUTES (Task 2); ruling 10 root (Task 2); ruling 13 deletes Task 4's chip. Ruling 6 builds nothing.

Global Constraints

  • Python 3.11 is the floor: nothing 3.12+.
  • No aliases (ruling 4). Nothing serves a moved URI or path; a moved URI is refused with its new spelling.
  • Console, network, workspace://current, flow://, secret://, skill:// and ui:// keep their URIs (rulings 1, 5, 6).
  • The admin API's paths, the signed link paths (/files/{session_id}/{name}), PUT /files/{folder}/{name}/kept and the admin's Files pane do not change (ruling 9).
  • Ruling 12's resource prefix is not built.
  • Earlier specs and plans, docs/saga/, and the shipped CHANGELOG sections are history and are not edited.
  • Every published shape that moves is a golden. Regenerate it with GOLDEN_UPDATE=1 pytest tests/test_golden.py, read the diff, and commit the goldens on their own (CONTRIBUTING.md).
  • Wiki pages are regenerated with python scripts/generate_wiki.py, and tests/test_wiki.py passes. The wiki is the docs/ submodule: a commit inside it, then the pointer here.
  • No new integration flow (AGENTS.md: less is more).
  • Never commit openapi.yaml or kubed/selenium_flow/http/static/.
  • The checks, as CI runs them:
    • Python (test.yml): ruff check . and pytest. The integration tests need a Grid and run in CI.
    • OpenAPI (quality.yml): python scripts/generate_openapi.py succeeds.
    • UI, for Task 4: npm --prefix ui ci && npm --prefix ui run build, then npm --prefix ui test, npm --prefix ui run check and npm --prefix ui run lint.
    • Changelog (pr.yml): a new line under ## [Unreleased].

Review Focus

  1. A saved flow whose upload_file step names workspace://files/downloads/export.csv. A person expects the run to stop at that step saying "workspace://files/downloads/export.csv is now session://downloads/export.csv", not "is not a file". Task 2 tests it through parse_uri.
  2. A flow from before 10-09 naming session://files/downloads/export.csv. A person expects one fix, straight to session://downloads/export.csv, not a hint to workspace://files/downloads/… that is itself refused. Task 1 tests it.
  3. The admin's Files pane for a workspace with no browser. A person expects the empty Downloads section ("No browser — downloads go with it."), not an error across the whole pane, because the no-session refusal lives in folder, not listing_of. Task 2 tests sections with no browser.
  4. workspace://files while the Grid is down, with a browser recorded. A person expects their kept files listed: the root no longer asks the Grid anything. Task 2 tests it with a Grid that raises.
  5. Completing session://downloads/{name} in VS Code with no session open. A person expects no suggestions, not an error dialog. Task 2 tests it.

Task 1: The refusal table

Files:

  • Modify: kubed/selenium_flow/names.py:222-233
  • Test: tests/test_names.py

Interfaces:

  • Consumes: nothing.

  • Produces: names.MOVED: tuple[tuple[str, str | None], ...], with every row of Design's table but the first; retired_uri(uri) -> None, unchanged in signature, raising ValueError per the table. RETIRED_SCHEME no longer exists. Task 2 adds the workspace://files/downloads row, in the same change that serves its new spelling, so the suite stays green between the two.

  • Step 1: Write the failing tests. In tests/test_names.py, keep the four tests there and add:

@pytest.mark.parametrize("old, new", [
    ("session://files/downloads/export.csv", "session://downloads/export.csv"),  # Review Focus 2
    ("session://files/a.png", "workspace://files/a.png"),
    ("session://console?level=error", "workspace://console?level=error"),
])
def test_a_moved_uri_names_where_it_is_now(old, new):  # ruling 11
    with pytest.raises(ValueError) as exc:
        names.retired_uri(old)
    assert str(exc.value) == f"`{old}` is now `{new}`"


@pytest.mark.parametrize("uri", [
    "session://downloads", "session://downloads/export.csv",
    "workspace://files/downloads (1)", "workspace://files/screenshots/a.png",
])
def test_a_served_uri_passes(uri):
    names.retired_uri(uri)
  • Step 2: Run them to see them fail

Run: pytest tests/test_names.py -v Expected: FAIL. session://downloads is refused as workspace://downloads, and session://files/downloads/… is pointed at workspace://files/downloads/….

  • Step 3: Implement MOVED, without its first row, and retired_uri, as Design has them. The comment above MOVED cites the rename spec's ruling 5 and this spec's ruling 11.

  • Step 4: Run the suite

Run: pytest && ruff check . Expected: PASS. Nothing else reads a session://downloads URI yet.

  • Step 5: Commit feat: the refusal of moved URIs is a table, and session://downloads is not refused.

Task 2: Downloads are session://downloads, on both surfaces

Files:

  • Modify: kubed/selenium_flow/names.py: MOVED's first row
  • Modify: kubed/selenium_flow/http/files.py: FOLDER_URI (92-97), SHAPES (105-110), FILE_ROUTES (128-134), DESCRIPTION (152-159), parse_uri (181-191), downloads_count (328-334, deleted), root (337-389), folder (392-423), keep (558-564), read_file (614-618), FOLDER_DESCRIPTION (678-683), keep_file's description (816-827), _routes (840-934); a new NO_DOWNLOADS beside OFF
  • Modify: kubed/selenium_flow/mcp/completions.py:25,32
  • Modify: kubed/selenium_flow/mcp/show.py: FOLDERS and VIEWS (28-70), DESCRIPTION (143-148)
  • Modify: kubed/selenium_flow/mcp/mirror.py: _binary (128-134)
  • Modify: kubed/selenium_flow/spec/builder.py: _file_paths (768-823), _named_in_path's docstring (561)
  • Modify: kubed/selenium_flow/spec/schemas.py: FileFolder, FolderList (724-769), _FILE_OPERATIONS (893-929)
  • Test: tests/test_file_sections.py, tests/test_file_surfaces.py, tests/test_kept_files.py, tests/test_resources.py, tests/test_show.py, tests/test_show_inventory.py, tests/test_openapi.py, tests/test_wiki.py, tests/test_http_answer.py, tests/test_admin_files.py
  • Regenerate: tests/golden/openapi.json; the wiki's generated pages

Interfaces:

  • Consumes: names.MOVED and names.retired_uri (Task 1).

  • Produces: files.FOLDER_URI[DOWNLOADS] == "session://downloads"; files.ITEM_URI[DOWNLOADS] == "session://downloads/{name}"; files.NO_DOWNLOADS: str; files.FILE_ROUTES with whole paths, as Design has it. files.downloads_count no longer exists. Task 4 reads session://downloads as a literal.

  • Step 1: Write the failing tests. In tests/test_names.py, test_a_moved_uri_names_where_it_is_now gains two rows: ("workspace://files/downloads", "session://downloads") and ("workspace://files/downloads/export.csv", "session://downloads/export.csv").

    In tests/test_file_sections.py:

    • test_a_uri_round_trips's downloads row becomes ("downloads", "export.csv", "session://downloads/export.csv").
    • test_a_uri_that_names_no_file_is_refused_with_the_shapes loses "workspace://files/downloads" (it is now a refusal) and gains "session://downloads" and "session://downloads/a/b".
    • test_the_root_lists_files_and_names_three_folders becomes …_names_two_folders, its folders the screenshots and recordings rows only.
    • Delete test_with_no_browser_the_downloads_folder_says_so and test_the_root_counts_downloads_without_describing_them.
    • Add, with this file's Actions, FakeWorkspaces, store, S and TOKEN, importing re, requests and kubed.selenium_flow.faults.NoSession:
def test_a_moved_download_uri_says_where_it_went():  # Review Focus 1
    with pytest.raises(ValueError, match=re.escape("is now `session://downloads/export.csv`")):
        files.parse_uri("workspace://files/downloads/export.csv")

def test_the_root_never_asks_the_grid(store):  # ruling 10, Review Focus 4
    class Down:
        def files(self, _session_id):
            raise requests.ConnectionError("grid down")
    store.write_file(S, "report.pdf", b"p")
    got = files.root(Actions(Down()), FakeWorkspaces(), store, TOKEN, S)
    assert [f["name"] for f in got["files"]] == ["report.pdf"]

def test_without_a_data_dir_files_is_off_even_with_a_browser():
    for read in (lambda: files.root(Actions(), FakeWorkspaces(), None, TOKEN, S),
                 lambda: files.folder(Actions(), FakeWorkspaces(), None, TOKEN, S, "screenshots")):
        with pytest.raises(ValueError, match=re.escape(files.OFF)):
            read()

def test_downloads_with_no_session_say_so(store):  # rulings 3, 8
    idle = FakeWorkspaces(browser="")
    for call in (
        lambda: files.folder(Actions(), idle, store, TOKEN, S, "downloads"),
        lambda: files.read_file(Actions(), idle, store, "session://downloads/export.csv", S),
        lambda: files.keep(Actions(), idle, store, "session://downloads/export.csv", S),
    ):
        with pytest.raises(NoSession) as exc:
            call()
        assert str(exc.value) == files.NO_DOWNLOADS

def test_the_downloads_listing_has_no_browser_flag(store):
    got = files.folder(Actions(), FakeWorkspaces(), store, TOKEN, S, "downloads")
    assert got["uri"] == "session://downloads" and "browser" not in got
    assert got["files"][0]["keep_with"].startswith('keep_file("session://downloads/')

def test_the_admins_sections_still_answer_with_no_browser(store):  # Review Focus 3
    got = files.sections(Actions(), FakeWorkspaces(), store, TOKEN, S, session_id="")
    assert got["downloads"] == [] and got["browser"] is False

Every other literal workspace://files/downloads in these files becomes session://downloads (test_file_sections.py:229, 237, 254, 333; test_file_surfaces.py:48, 54, 122; test_resources.py:150, 160, 264; test_show.py:58, 61, 103, 141, 239, 415; test_show_inventory.py:38, 42, 110; ui fixtures are Task 4's). In test_file_sections.py:254, the expected message is files.NO_DOWNLOADS.

In tests/test_resources.py, beside test_a_person_picking_a_file_is_offered_their_own_files, with its reader:

DOWNLOAD = ResourceTemplateReference(type="ref/resource", uri="session://downloads/{name}")

async def test_download_names_complete_to_nothing_with_no_session(reader):  # Review Focus 5
    reader.workspaces.browser = lambda name: ""
    async with Client(reader.mcp) as c:
        offered = await c.complete(DOWNLOAD, {"name": "name", "value": ""})
    assert offered.values == []

async def test_download_names_complete_from_the_open_browser(reader):
    reader.workspaces.browser = lambda name: "abc"
    reader.actions.grid.files = lambda session: [{"name": "export.csv", "size": 1}]
    async with Client(reader.mcp) as c:
        offered = await c.complete(DOWNLOAD, {"name": "name", "value": "exp"})
    assert offered.values == ["export.csv"]

In tests/test_show.py, add show on workspace://files/downloads/report.csv answering an error whose text contains "is now session://downloads/report.csv".

Over HTTP, in tests/test_kept_files.py:

  • every route built from FILE_ROUTES as f"/files{template}" (355-357, 404-406, 771, 810) becomes template;
  • add test_the_downloads_listing_is_under_browser: with a live browser and Grid.files patched to DOWNLOADS, GET /browser/downloads answers 200 with body["uri"] == "session://downloads"; with no browser it answers 400 with {"error": files.NO_DOWNLOADS}; GET /files/downloads is not 200.
  • PUT /files/downloads/report.pdf/kept (421, 439, 455) is unchanged, and still passes.

In tests/test_openapi.py:

def test_the_downloads_listing_is_the_sessions(spec):  # ruling 9
    operation = spec["paths"]["/browser/downloads"]["get"]
    assert operation["x-mcp-resource"] == "session://downloads"
    assert operation["tags"] == ["browser"]
    assert "/files/downloads" not in spec["paths"]
    assert "/files/{folder}/{name}/kept" in spec["paths"]

In tests/test_wiki.py:139-150, the /browser row gains "list_downloads", and the /files row is set(files_module.FILE_ENDPOINTS) - {"downloads"}.

  • Step 2: Run them to see them fail

Run: pytest tests/test_file_sections.py tests/test_file_surfaces.py tests/test_kept_files.py tests/test_resources.py tests/test_show.py tests/test_show_inventory.py tests/test_openapi.py -v Expected: FAIL. The URIs, the route and NO_DOWNLOADS do not exist yet.

  • Step 3: Implement MOVED's first row, then http/files.py as Design has it. parse_uri checks FOLDER_URI[DOWNLOADS] + "/" first, then workspace://files/, where a two-part path is a folder only for screenshots and recordings. folder raises NoSession(NO_DOWNLOADS) for downloads when workspaces.browser(name) is empty, before the NOTHING/OFF checks, which now apply to the other three folders only. keep and read_file raise the same in place of their two sentences. The module docstring's paragraph on listings says Downloads with no browser is refused, and the reserved-names paragraph says a workspace://files/downloads URI is refused naming session://downloads.

    Then completions.py: DOWNLOAD_NAMES = ("session://downloads", "files", "name") and the template key ("session://downloads/{name}", "name"). show.py: FOLDERS = ("screenshots", "recordings") for the workspace://files rows, and two rows for files.FOLDER_URI[DOWNLOADS] and its item (component folder, and file with entry=True), placed before the flow:// rows. _entry already reads the listing at the URI's parent, so it needs nothing. mirror.py, builder.py and schemas.py as Design has them.

  • Step 4: Regenerate and read the diffs

Run: GOLDEN_UPDATE=1 pytest tests/test_golden.py && python scripts/generate_wiki.py && pytest && ruff check . && python scripts/generate_openapi.py Expected: PASS. The golden diff shows /files/downloads gone, /browser/downloads with the browser tag and x-mcp-resource: session://downloads, the two descriptions, and FileFolder/FolderList without browser. tools-on.json and tools-off.json show only keep_file's and show's descriptions. In docs/, list_downloads.md, list_files.md, keep_file.md, Actions.md and upload_file.md change, and nothing else.

Then: grep -rn "files/downloads" kubed tests --include=*.py Expected: only the refusal's own table and the tests that check it, the admin paths (/admin/workspaces/{key}/files/downloads and its tests), PUT /files/downloads/…/kept and test_http_answer.py:40.

  • Step 5: Commit the code, then the golden on its own, then the wiki pages inside docs/ and the pointer: feat!: a browser's downloads are session://downloads and GET /browser/downloads.

Task 3: What a model, an operator and a maintainer read

Files:

  • Modify: kubed/selenium_flow/mcp/resources.py: DESCRIPTION (19-34), CONSOLE_DESCRIPTION (117-125), NETWORK_DESCRIPTION (127-137), a new CAPTURE_KEPT
  • Modify: kubed/selenium_flow/mcp/tools.py: READING_POINTER (206-210), upload_file's docstring (643-645)
  • Modify: kubed/selenium_flow/core/actions.py: upload_file's docstring (965-971) and refusal (995-999)
  • Modify: skills/selenium-flow/SKILL.md (164, 170-171); skills/selenium-flow/references/WORKSPACES.md (141-145)
  • Modify: README.md (193, 197, 198)
  • Modify: AGENTS.md: the vocabulary list (25-31), and the session:// paragraph under "Everything to read is a resource" (852-854)
  • Modify, inside docs/: Files.md (6-10, 42, 78-83, 98), Workspaces.md (71-72), Deployment.md (90), _Sidebar.md (56)
  • Modify: CHANGELOG.md (## [Unreleased])
  • Test: tests/test_resources.py, tests/test_capture_resources.py, tests/test_skill.py, tests/test_readme.py, tests/test_wiki.py
  • Regenerate: tests/golden/tools-on.json and tools-off.json (upload_file), openapi.json (upload_file, currentWorkspace); the wiki's generated pages

Interfaces:

  • Consumes: the URIs and paths of Task 2.

  • Produces: resources.CAPTURE_KEPT: str.

  • Step 1: Write the failing test. In tests/test_capture_resources.py:

async def test_the_capture_listings_say_they_outlive_the_browser(server):  # ruling 1
    async with Client(server.mcp) as c:
        listed = {str(r.uri): r.description for r in await c.list_resources()}
    for uri in ("workspace://console", "workspace://network", "workspace://current"):
        assert resources.CAPTURE_KEPT in listed[uri], uri

using that file's server fixture, and importing resources from kubed.selenium_flow.mcp.

  • Step 2: Run it to see it fail

Run: pytest tests/test_capture_resources.py -v -k outlive Expected: FAIL, CAPTURE_KEPT cannot be imported.

  • Step 3: Write the content.

    • The Python copy as Design has it.
    • SKILL.md: line 164 reads "Files' kept files, each with a link, and the two folders below". Lines 170-171 move below the recordings rows as | `session://downloads` | the open browser's downloads; with no session open it says so | and | `session://downloads/{name}` | one of those |.
    • WORKSPACES.md:141: "What is gone is the session's downloads, session://downloads."
    • README: 193 "…plus the two folders below"; 197 | a resource | `session://downloads`, `session://downloads/{name}` — the open browser's downloads |; 198 ends `GET /files/recordings`, `GET /browser/downloads`.
    • AGENTS.md, vocabulary, after the session bullet: session:// names what dies with the session's browser: today only its downloads, session://downloads. Everything the workspace keeps, captured console and network included, is workspace:// (spec 2026-10-29-session-uris).
    • AGENTS.md:852-854: A session:// URI from before the rename — in a saved flow's upload_file step, say — is refused with its workspace:// spelling, and workspace://files/downloads with session://downloads, by every tool that takes a URI (spec 2026-10-29-session-uris, ruling 11). session://downloads is the one session:// URI served. Saved flows are not rewritten; the error names the fix.
    • Files.md: 6-10 name two folders, then "The open browser's downloads are session://downloads." 42 reads "from workspace://files, its folders or session://downloads". 78-79 become the two session://downloads rows. 81-83: "The listings are mirrored as JSON over HTTP: GET /files, GET /files/screenshots, GET /files/recordings and GET /browser/downloads; PUT /files/{folder}/{name}/kept keeps one, from screenshots, recordings or downloads." 98: "workspace://files, session://downloads and the admin UI hand these out…".
    • Workspaces.md:71-72: "A session:// URI from before the rename is refused with its workspace:// spelling, and workspace://files/downloads with session://downloads, so a saved flow that names one says how to fix it. session://downloads, the open browser's downloads, is the one session:// URI served."
    • Deployment.md:90 and _Sidebar.md:56: session://downloads.
    • CHANGELOG, the spec's line, word for word: - **BREAKING:** a browser's downloads are now `session://downloads` (and `GET /browser/downloads`); `workspace://files/downloads` is refused with the new spelling.
  • Step 4: Check nothing was missed

Run: grep -rn "files/downloads" README.md AGENTS.md skills kubed scripts docs --exclude-dir=superpowers --exclude-dir=saga Expected: only the admin paths (Administration.md:144, http/admin/, WorkspaceDetail.svelte), PUT /files/{folder}/{name}/kept's mentions, the signed-link sentence in Files.md, the refusal table, and the sentences above that name the refusal.

  • Step 5: Regenerate, and run every check

Run: GOLDEN_UPDATE=1 pytest tests/test_golden.py && python scripts/generate_wiki.py && pytest && ruff check . && python scripts/generate_openapi.py Expected: PASS. The goldens and the generated pages change only in the sentences above (current_workspace.md, upload_file.md).

  • Step 6: Commit the code and the skill, the goldens on their own, the wiki pages inside docs/ with the pointer, then docs: session://downloads, and what capture keeps, then chore: changelog for session://downloads.

Task 4: The app reaches downloads from current (ruling 13)

Files:

  • Modify: ui/src/lib/views/ContextView.svelte (30-56)
  • Modify: ui/src/lib/types.ts (253, 268): browser off FolderRef and FolderData
  • Test: ui/src/lib/views/ContextView.test.ts, ui/src/lib/views/FilesView.test.ts (7-9), ui/src/App.test.ts (88)

Interfaces:

  • Consumes: session://downloads (Task 2), as a literal.

  • Step 1: Write the failing tests. In ContextView.test.ts, with its base:

test('an open session offers its downloads', async () => {
  const onshow = vi.fn()
  const { getByRole } = render(ContextView, { props: { data: { ...base, live: true }, onshow } })
  await fireEvent.click(getByRole('button', { name: 'Downloads' }))
  expect(onshow).toHaveBeenCalledWith('session://downloads')
})

test('an idle workspace offers no downloads', () => {
  const { queryByText } = render(ContextView, { props: { data: { ...base, live: false }, onshow: vi.fn() } })
  expect(queryByText('Downloads')).toBeNull()
})

FilesView.test.ts's fixture loses its downloads folder, and its expectations their downloads chip. App.test.ts:88 reads ['folder', { workspace: 's', folder: 'downloads', uri: 'session://downloads', count: 0, files: [] }, 'Downloads'].

  • Step 2: Run them to see them fail

Run: npm --prefix ui ci && npm --prefix ui test Expected: FAIL, no Downloads button.

  • Step 3: Implement the chip as Design has it, and drop browser from the two types.

  • Step 4: Run every UI check

Run: npm --prefix ui run build && npm --prefix ui test && npm --prefix ui run check && npm --prefix ui run lint && pytest tests/test_show_inventory.py Expected: PASS, with no svelte-check warnings.

  • Step 5: Commit feat(ui): the app opens a session's downloads from its context card.

Clone this wiki locally