Desk v2 — the root 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 bycrm/frontend2. Bench root/Users/shariq/crm-bench/apps; dev atcrm.localhost:8099. - Branch: all desk-v2 work lands on
desk-v2in frappe, based ondevelop. Sync withgit 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-v2branches too, and app-side work goes on those, via a PR. Created 2026-09-02 from each repo'sdeveloptip —frappe/crmat0b99b4ee26,frappe/erpnextat4944df8733. Same rule as frappe's: never commit on them, always a branch on the fork andgh pr create -R frappe/<app> --base desk-v2. This is what a change like ERPNext'sapp_modular = Truewas waiting for: dead config ondevelop, and exactly right ondesk-v2. A branch cut offdevelopbefore 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 #NNNNNin the body does nothing on adesk-v2PR: GitHub only creates that link when the PR targets the repository's default branch, which for frappe isdevelop. Forking is not the cause — a fork PR intodeveloplinks 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-modelingfor grilling tickets,/prototypefor prototypes,/researchfor research,/frappe-app-dev+/code-stylefor any code. - Run
/quality-code-reviewbefore 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.mdondesk-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 fielddescriptionis 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-v2edits 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 atb7d6830508.
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/uisend 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 intofrontend/srcorui/src. Measured the same day: the desk was v1 everywhere except list rows, which frappe-ui'suseListalready 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
pageobject 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
zonekey;page.headerActionsbecomespage.header; each crumb is its own item;Saveis an ordinary item. Shipped in code, so it stands even though map 5 is halted.desk-v2is 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 bygit 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-v2wholesale (2026-08-25). ~26k lines acrossui/srcandfrappe/deskwere already in the folders the routing pivot keeps, so re-deriving them map by map would redo reviewed work for no architectural gain.crm/frontend2is 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
wayfinderbranch offrappe/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_prefixinhooks.pyand lives under/apps/<prefix>/…;/appsitself is the shell's index. Frappe gainedfrontend/, 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 namedRecord, and modules sit at depth one under a modular prefix (routeFor). Both walking skeletons —/apps/deskand/apps/crm— are merged.Map #42226 — The rail and the sidebar — one authored item model drives both surfaces (wrapped 2026-09-04).
Rail,Navigation ItemandNavigation Item Typeare new; desk v1'sSidebaris extended in place as v2's sidebar container. Eight built-in kinds plus a contribution path an app uses to add its own (ERPNext'sDefault Companyproved 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.pyproves 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 as9125e96f66, fourth #42728 as1c2d1b6d72) moved there with their conflicts and resolutions; the recipe, the checks that go red on develop's own findings, and the two stowaways (a foreignyarn.lockentry, thepypikapin) 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
Source: frappe/frappe