#42061·frappe

Desk v2 — the root map

Author: shariquerikCreated Aug 25, 2026Updated Sep 18, 2026
Labelswayfinder:map

Read this issue, and only this issue, to orient. Everything below is either a fixed input (a wrapped map's conclusion) or a pointer to a child map. Child maps are expeditions; read one only when this page sends you there.

Destination

A new desk for Frappe Framework, built from the ground up on frappe-ui and @framework/ui, customizable by design from the first line.

Desk v1 grew customization as an afterthought — Client Script, Property Setter and Customize Form are bolted onto a UI that was not designed to be extended. Desk v2 inverts that: the customization path is the build path, so any app can use the desk to build its own product rather than working around it. CRM is the first consumer; ERPNext and the rest follow through the same doors.

Desk v2 reaches most of desk v1's feature surface. It deliberately drops what no longer makes sense, and subsumes what v2's better primitives already answer. Dropping is an explicit, recorded decision — never an omission.

The size of the thing

Measured 2026-08-25, so scope arguments start from numbers rather than impressions:

Desk v1 client ~270 JS files, ~90k LOC (frappe/public/js/frappe/)
Plus Vue builders ~17k LOC (form, print-format, workflow, layout builders)
Desk v2 consumer today 3 pages (Home, List, Record) on 4 routes
Field controls ~25 of ~47 ported
v1 groups entirely absent in v2 7 of 20

Absent outright: workspace widgets; every list view beyond plain list (Report, Kanban, Calendar, Gantt, Tree, Image, Inbox, Map, Dashboard-view); query/script reports and the report builder; print view and print formats; dashboards, charts and number cards; workflow with submit/cancel/amend; the awesomebar and global search; and the dev/ops consoles.

This is a program, not a project. Every child map must justify itself against the destination, and "v1 had it" is not a justification.

Notes

  • Domain: a customizable desk in frappe/ui (@framework/ui), consumed today by crm/frontend2. Bench root /Users/shariq/crm-bench/apps; dev at crm.localhost:8099.
  • Branch: all desk-v2 work lands on desk-v2 in frappe, based on develop. Sync with git merge upstream/develop — never rebase this branch, and never squash the sync either (see Decisions so far). Each sync is a task on the standing sync map, #42766. Ship to develop as small slice PRs cut off develop, never by merging desk-v2 wholesale.
  • CRM and ERPNext have desk-v2 branches too, and app-side work goes on those, via a PR. Created 2026-09-02 from each repo's develop tip — frappe/crm at 0b99b4ee26, frappe/erpnext at 4944df8733. Same rule as frappe's: never commit on them, always a branch on the fork and gh pr create -R frappe/<app> --base desk-v2. This is what a change like ERPNext's app_modular = True was waiting for: dead config on develop, and exactly right on desk-v2. A branch cut off develop before this existed retargets with no rebase, since the two start equal. First one through: ERPNext's rail, frappe/erpnext#58713.
  • Link the PR to its ticket by hand, in the PR's Development box. A Closes #NNNNN in the body does nothing on a desk-v2 PR: GitHub only creates that link when the PR targets the repository's default branch, which for frappe is develop. Forking is not the cause — a fork PR into develop links fine. Without the click, the ticket shows only a faint "mentioned this issue" line in its timeline and no linked PR in the sidebar, which is why desk-v2 tickets look bare next to maps whose PRs target a default branch. There is no API for it; it is one click at PR-open time.
  • Skills per ticket type: /grilling + /domain-modeling for grilling tickets, /prototype for prototypes, /research for research, /frappe-app-dev + /code-style for any code.
  • Run /quality-code-review before closing a build ticket. It found a real defect on every build ticket of map 4, twice inside a passing test written for the feature.
  • Judge every API decision by reading the script an author would write. The authored script is the primary design artifact. This is the rule that produced maps 1-4 and it does not expire.
  • Comments and field descriptions are checked before a ticket closes. The rule lives in the root AGENTS.md on desk-v2: a comment survives only where it records a constraint a reader would otherwise undo, two lines maximum, one summary line per docstring, no commented-out code, and a DocType field description is one line. Set by Desk v2 — the code reads as code, which measured 2,599 comment lines in 12,940 added ones before writing it.
  • Deviations found while building are dated amendments on the spec ticket, not new tickets and not map prose.
  • Every DocType desk v2 stores data in or reads for customization is listed in one place: the DocType register comment on this issue. It is kept current in place, not as a log. A ticket that adds, extends or drops a DocType on desk-v2 edits that comment before it closes: add, change or remove the row and name the ticket or PR in its Source column. Surveyed 2026-09-10 at b7d6830508.

Program principles, stated by the user 2026-09-07 so that no child map has to make these calls on its own:

  • Three authored tiers, and an LLM layer above them. Every customizable surface is authored at app level (shipped by the app), site level (an admin, for everyone) and per-user level (an overlay), in that precedence. Map 1's merge rules and the rail's per-user overlay (#42226) already follow this; a new surface names its three tiers at design time. Above the three sits an LLM layer. It is the ultimate goal, not the current priority, but it is planned for from the start: it drives the desk only through the same UI API a person or a script uses, so nothing is built that only a mouse can reach.
  • Every surface is drivable by name through one UI API. Open the settings dialog at a named pane; switch to a named tab and scroll to a named activity; open the composer. The API must reach nearly every aspect of the screen. Its name is open, and so is where its permission layer sits (area 12). Map 4's "chrome addressed by name" is its first instance. The test for every new design: can a script, and therefore an LLM, do this with no mouse?
  • Customization is UI-first on top of scripting. Scripting is the base. A person rearranges, hides, shows or adds tabs, panel sections and header actions from the UI without opening a script. Whether the UI writes a script or a declarative record is fog under area 6. The UI API must be able to do everything that UI can do.
  • Performance, caching and security are design inputs, not follow-ups. A ticket that adds a surface states its cache key and invalidation, its permission check and its cost on cold load. A complex framework must still be light to use and secure by default; the 100 KB per-key boot budget from #42226 is the precedent.
  • The basic desk layer comes first. The principles above shape the design of each slice; they do not pull the LLM layer, the settings dialog or the doctype-creation UI ahead of the record page, the list and the shell.
  • REST API v2, and nothing else (user ruling, 2026-09-16). Every request the desk v2 frontend and @framework/ui send goes to /api/v2: document routes for reads, lists, counts, meta, saves and deletes, and /api/v2/method/… for a dotted call. No /api/method/… or /api/resource/… URL is written into frontend/src or ui/src. Measured the same day: the desk was v1 everywhere except list rows, which frappe-ui's useList already sends to /api/v2/document/…. The migration and the check that holds the line are Desk v2 — REST API v2 everywhere.

Decisions so far

  • Map 1 — Generic Record page customization framework — the whole customization API, proven with a vertical slice: the page object and its surfaces, scripts as handlers objects run in source order, five merge rules with no solver, extension delivery by import map over host-published singleton chunks.

  • Map 2 — The Page Script tier, live — the Page Script doctype and blob-URL ESM runtime, the in-app editor, script-side permissions, API versioning, server-side error telemetry.

  • Map 3 — v1 Form Script parity — a v1 Form Script can be written as a Page Script with nothing missing: row identity, per-row field events, the Table handlers block, page.fields, the Commit model.

  • Map 4 — Record-page chrome, addressed by name — chrome is addressable by name, not position: tab identity, the Form Layout strip as a second surface, the header's flat vocabulary and fitting rule, dropdown sections.

  • Map 5 ticket 78 — One vocabulary for the whole header row — the header row is one flat list with a zone key; page.headerActions becomes page.header; each crumb is its own item; Save is an ordinary item. Shipped in code, so it stands even though map 5 is halted.

  • desk-v2 is based on develop and synced by merge, not rebase (2026-08-25). Measured: merging 99 commits of develop drift produced one conflicted file, a modify/delete resolved by git rm. The equivalent rebase stopped at commit 3 of 58. A rebase re-replays the whole stack on every sync against a target moving ~99 commits per 10 days; a merge resolves each conflict once, permanently.

  • The existing framework work moved to desk-v2 wholesale (2026-08-25). ~26k lines across ui/src and frappe/desk were already in the folders the routing pivot keeps, so re-deriving them map by map would redo reviewed work for no architectural gain. crm/frontend2 is the opposite case — its 123 files change house under the pivot, so they re-home gradually as child maps reach them.

  • Maps and tickets live on the orphan wayfinder branch of frappe/frappe (2026-08-25). Inside the repo, unmergeable into develop by construction, and clear of an issue tracker carrying 2027 open bug reports. Binaries (recordings, screenshots, prototypes) stay local — an orphan branch shares the object store, so they would bloat every clone forever.

  • Map #42062 — Routing and hosting — the framework hosts the shell and owns the URL space (wrapped 2026-09-04). An app declares one app_prefix in hooks.py and lives under /apps/<prefix>/…; /apps itself is the shell's index. Frappe gained frontend/, built as one bench-wide bundle whose manifest fails on a singleton clash, with contributions delivered through the import-map seam map 1 designed. A prefix is a lens over one doctype space, the owner's prefix is canonical for generated links, the page kind is named Record, and modules sit at depth one under a modular prefix (routeFor). Both walking skeletons — /apps/desk and /apps/crm — are merged.

  • Map #42226 — The rail and the sidebar — one authored item model drives both surfaces (wrapped 2026-09-04). Rail, Navigation Item and Navigation Item Type are new; desk v1's Sidebar is extended in place as v2's sidebar container. Eight built-in kinds plus a contribution path an app uses to add its own (ERPNext's Default Company proved it). Rows are seeded by apps, filtered by six permission buckets, arranged per user by an overlay with its own write endpoints, and delivered in boot under a 100 KB per-key budget. The sidebar is chosen from the address, remembered on the history entry (?sidebar=), and the section the address stands in opens itself. CRM's and ERPNext's rails run on it with no navigation code in either app.

  • Map #42413 — The code reads as code — the rule and the four sweeps, all merged (wrapped 2026-09-04). The rule lives in a root AGENTS.md; comment_equivalence.py proves a sweep changed no code. frontend/ went from ~3,000 comment lines to ~1,400, the shell's Python docstrings from 1,600 lines to 188, and all 38 DocType field descriptions fit on one line. The rule is a standing Note above and a pre-close check on every ticket from here on.

  • The develop syncs live on their own map since 2026-09-11, #42766 — keep desk-v2 in sync with develop, a standing map that carries execution. The four done so far (first #42348 on 2026-09-01, second #42560 as 7f22eb1730, third #42669 as 9125e96f66, fourth #42728 as 1c2d1b6d72) moved there with their conflicts and resolutions; the recipe, the checks that go red on develop's own findings, and the two stowaways (a foreign yarn.lock entry, the pypika pin) are its Notes.

Child maps

Ten are chartered (five wrapped, five open), listed in the order they were cut. Each is a sub-issue of this one and its tickets are sub-issues of it, so GitHub carries the tree — this page carries only the gist. Read one only when this page sends you there.

#42062 — Routing and hosting · WRAPPED 2026-09-04, 26 of 26 tickets closed

The framework hosts the SPA shell and owns the URL space. An app declares a route prefix and the desk serves that app's doctypes by default, with generated layouts and routes; it never hosts its own frontend. The framework claims one top-level segment, /apps, and every app lives beneath it — /apps/crm/…, /apps/erpnext/…. Frappe gains a frontend/ folder to host the shell.

Cut first because it was structurally cheapest then and never cheaper again — the migration cost 3 pages and 4 routes, and every view added before it multiplied that. The walking skeleton (#42073) is merged, the modular route shape and routeFor landed (#42225), and a docname ending in .html is v1's limitation inherited, not a v2 regression (#42270). Its conclusion is a fixed input below; its residual fog is handed up under area 1.

#42226 — The rail and the sidebar · WRAPPED 2026-09-04, 42 of 42 tickets closed

One item model that drives both the rail and the sidebar. A rail item is independent (opens no sidebar) or linked (opens one), and what an app puts in its rail is the app's choice — CRM makes doctypes its primary items, ERPNext makes modules. Items come in kinds and an app can add a kind. Ended on the decided model plus the walking skeleton, both merged: CRM's doctype-primary rail and ERPNext's module-primary rail run on desk-v2 with no navigation code in either app. Its conclusion is a fixed input below; its residual fog is handed up under area 1.

#42271 — The record page · WRAPPED 2026-09-16, 31 of 31 tickets closed

The generated record page renders a real document, and every part of it is reachable from a Page Script by name. Wrapped 2026-09-16: the header, the panel, the Details form (page.form), the body columns (page.body), the frame bands (page.frame), the stored-script tier (Client Script with view Record, Form Layout), the import map for stored scripts, live docinfo and the error endpoint are all merged on desk-v2, and the proof script's three acts were walked on a real CRM Deal. Every region of the page is a list a script names. This is area 3 below, and it is where halted maps 5 and 6 landed. Its charter fixes the layer boundary: the engine, PanelLayout and PageScriptEditor are desk layer and live in frappe/frontend, never ui/src/experimental; @framework/ui keeps only what the engine merely consumes.

Its first ticket landed the tier's tables (merged 2026-09-07 as 9ee80fb62c). Ruled then: no new Page Script doctype; Client Script is reused with a Record view, since v1 reads it by view and never sees the new value. Handed up to this root as fog: the script editor port (unblocked now that the import map landed), a URL builder on page, the declarative twin and the per-user tier of chrome (area 6), the cold-load skeleton, a read-only docinfo surface for scripts, translation for scripts, a new-record route, an unsaved-doc store, and per-field merge on a timestamp mismatch. The activity column continues on #42758.

#42758 — The activity column: tabs, feed and composer · chartered 2026-09-11, 3 tickets, all unblocked since the field build merged 2026-09-11

Split from the record page map on 2026-09-11, after an inventory of CRM frontend2 and the saved-view-sidebar branch (one stack; CRM is the only assembled record page) showed a region that map never named: the main column's tab strip, the activity timeline with emails and files, and the comment and email composer. Destination: a record's conversation lives on the generated page, and a Client Script drives every act in it by name (switch to a named tab, scroll to a named activity, open the composer, post), proved by a walk that posts a comment and sees it in the feed. Three grilling tickets; each was blocked by the field build (#42767), merged 2026-09-11, so all three are open and ready, and the record tabs grilling (#42764) is the frontier. Since 2026-09-16 the tabs ticket inherits the every-list rule and the component item shape from the record page map, and the composer and feed call /api/v2 under #42920. One ruling at charter: the framework page posts through framework endpoints, never an app's.

#42569 — The shell and the list, first version · WRAPPED 2026-09-16, 16 of 16 tickets closed

Chartered from area 1 with the plain-list slice of area 2 by pointer, after the record-page map's header landed and the user judged that a record page cannot be judged inside a hand-drawn shell. Destination: a person walks rail, sidebar, list and record and judges each in its frame. The rail, sidebar and page frame move onto frappe-ui's DesktopShell, Rail and Sidebar; the generated list page gets ListView with CRM's controls. Two user rulings at charter: CRM's frontend2 shell and list are the first version, and the rail's first item is the app logo, opening All apps, Customize sidebar and Copy link. Every ticket is closed (last: the singles task #42808, merged 2026-09-14 as fe150352bc). Its charter fog, the page frame and the header row's move into it, closed through the frame build (#42616), the header row's move (#42618) and the record map's page.frame build (#42873). Leftovers handed up to this root are listed in the status block at the end of the map: the list's New button, Home and Module stubs, bulk actions, keyboard shortcuts, the site scope's mouse path, app-contributed user-menu rows, the 401 mid-session answer, the list page's frame as a list, the mobile shell, and its v1 call sites now owned by #42920.

#42660 — @framework/ui, the package on its own · chartered 2026-09-09, 7 tickets, 1 closed

Chartered from the List build on the shell-and-list map (#42569), where a first pass at stories was rejected and the owner asked for a decision instead. Destination: a person can open, read and judge any @framework/ui control on its own, outside an app; the package has one settled way to show stories, a home that renders them, and a stated fate for its existing story files. Ruled a child of this root on 2026-09-11: ui/ is the gene