Skip to content

feat: app-wide agentic mode - #244

Draft
ruchamahabal wants to merge 31 commits into
frappe:developfrom
ruchamahabal:agentic-app-2
Draft

feat: app-wide agentic mode#244
ruchamahabal wants to merge 31 commits into
frappe:developfrom
ruchamahabal:agentic-app-2

Conversation

@ruchamahabal

Copy link
Copy Markdown
Member

No description provided.

ruchamahabal and others added 30 commits August 16, 2026 17:43
OpenAI's models through a ChatGPT Plus/Pro subscription instead of API
credits: a PKCE browser sign-in (codex_login) stores the OAuth credential
on the provider row, and codex.py speaks the Responses SSE protocol that
litellm cannot. Ships with the provider setup flow — presets catalogue,
verify-then-save endpoints, model import, and add-model-by-id.

Co-Authored-By: Suraj Shetty <surajshetty3416@gmail.com>
Co-Authored-By: Shrihari Mahabal <shriharimahabal08@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Studio AI Provider + Studio AI Model rows replace the hardcoded
OpenRouter catalog: any OpenAI-compatible gateway works from the UI
without a deploy, model metadata (context window, vision) auto-fills
from OpenRouter/litellm catalogs, and the model docname is the exact
litellm string the agent calls with.

Keys live on providers now. A patch moves the Studio Settings key onto
the OpenRouter row and the settings field (and its dialog) is gone; the
key is resolved inside the worker job, never through enqueue kwargs.

The chat panel gains a providers dialog (preset list -> detail with
verify-then-save, ChatGPT OAuth, custom endpoints with model import,
add-model-by-id), a registry-backed model switcher that dims models
whose provider has no key, and Clear/providers actions in the panel
header. Ticks in the dialog are authoritative: unticked models are
disabled, so the picker always matches the dialog.

Ported from Frappe Builder's AI provider system.

Co-Authored-By: Suraj Shetty <surajshetty3416@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Previously the browser applied the agent's ops to whichever canvas was open
and saved them - navigating mid-turn corrupted builds, and edits died with
the tab. Now, following Builder's design:

- WorkingTree (tree.py) is the sole applier: the loop loads the page from
  the DB (the client no longer sends page_context) and persists draft_blocks
  after every round. Rejected ops are never emitted, so canvas and draft
  can't diverge.
- New-block ids are assigned server-side and shipped in the op
  (block_json/blocks_json), so canvas refs always match the draft and the
  model can edit blocks it just added. Query tools read the live tree.
- The editor only mirrors accepted ops: it flushes unsaved changes before a
  turn, adopts the server's modified stamp after each batch, and suspends
  autosave while a turn runs (re-asserted on every event, restored after a
  reload via get_ai_session.is_running).
- persist_tree commits before writing and retries once on deadlock, so a
  concurrent save can't fail the authoritative write (MariaDB error 1020).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sessions were keyed to a page, so every page had its own chats and
navigating away mid-turn lost the conversation view. Now a session belongs
to the app and the open page is just the turn's target:

- Studio AI Session gains a required `app` link; `page` becomes the
  session's focus (the page the running turn is editing, set on each run).
  A patch backfills app from each session's page.
- ai_chat_* events are suffixed by session id instead of page id, and
  every payload carries target_page_id. The panel attaches per session
  (ensureAttached), so a running turn keeps streaming into the chat across
  page switches within the app.
- Canvas events (tool_batch, page_json stream, reload) are mirrored only
  when target_page_id matches the page on the canvas - switching pages
  can't touch a build, and a background build lands via the DB refetch on
  navigation.
- get_ai_session / new_ai_session / list_app_ai_sessions take app_id;
  get_ai_session also returns the running turn's page so a reloaded editor
  suspends autosave only when the build targets the page it shows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The agent can now move its focus across the app's pages, one at a time:

- tools/pages.py: list_pages / read_page give app awareness without
  switching; open_page / create_page move the focus - AgentRunner.focus_page
  re-points the working tree, per-mode toolset and system prompt at the
  target, so every subsequent edit lands there. create_page checks route
  collisions, inherits is_standard/frappe_app from the app, and starts from
  the editor's body template.
- agent/locks.py: one page has one AI writer. A Redis NX lock per page
  (token = session id, TTL above the job timeout so a crashed worker can't
  wedge a page) is taken at turn start, moved with the focus, and released
  when the turn ends. A second chat targeting a locked page gets a clear
  refusal instead of a corrupted draft.
- generate_page no longer ends the turn: artifact ops are applied in call
  order by apply_round, so one turn can create and generate several pages
  sequentially (bounded by MAX_ROUNDS).
- System prompt gains a "Working across pages" section: one focus at a
  time, build sequentially, wire navigation to pages you create.

No frontend changes needed - the session-keyed events and target_page_id
mirror guards already keep background builds off the open canvas.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
selected_model was written only at session creation, so switching models in
the picker never reached the session doc - and the panel's post-turn session
reload snapped the picker back to the stale stored value. api.run now records
the resolved model on the session each turn (set_selected_model), alongside
the focus-page tracking.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sessions are app-scoped but outlived their app - a recreated app reusing
the same autoname inherited the deleted app's chat history. Studio App's
on_trash now cascades its AI sessions (each session's own on_trash takes
its messages), matching how it already cascades pages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hardening from the first real multi-page runs:

- ai_chat_page event (created/focused/updated): the editor refreshes its
  pages list live (agent-created pages showed up only after a reload) and
  the panel renders a dismissible "Building <page>" chip with an Open
  button that jumps to the target page.
- Watchable background builds: page_json chunks are buffered whatever page
  is open (render still only on the target), so clicking Open mid-stream
  picks up the live preview instead of a blank canvas. Chunks carry an
  offset - the buffer resets per generation (a multi-page turn streams
  several) and gapped chunks are skipped in favor of the final op.
- set_page_meta: retitle/re-route the focused page, collision-checked.
  list_pages flags title_is_default / route_is_default (regex on the
  auto-titler's patterns), and the prompt only allows renaming flagged
  pages - a user-chosen title is never replaced. The open page adopts new
  meta + modified stamp live.
- Filter validation: add/update_data_source refuse a bare value-list
  filter ({"status": ["Completed","Cancelled"]}) with corrective guidance
  - Frappe reads a list as [operator, value], so these crashed every
  fetch with a KeyError and spammed the error log.
- Ordering: the prompt now requires building the user's open page first
  in multi-page requests (and using existing empty pages, home included,
  before creating new ones), so the visible canvas shows progress
  immediately instead of sitting frozen while background pages build.
- The panel fully resets chat state when the app changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Event scripts only exposed the DOM event through a user-defined
handleEvent(...) function, but everyone's muscle memory (humans and
LLMs alike) is Vue's inline-handler $event - the AI kept writing
$event.preventDefault() and the script errored. executeUserScript now
puts $event (= the first event argument, exactly Vue's semantics) in
the script context. handleEvent stays for naming multiple arguments,
and event-name modifiers (dragover.prevent, keydown.enter) remain the
preferred form for prevent/stop.

The event dialog's help text and the AI's prompt/tool guidance now
document all three forms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A reload during a running turn used to come back blind: no spinner, no
transcript-in-progress, a dead preview, and a lost "Building page" chip.
Everything about a turn is durable except the in-flight generation stream,
so (following Builder's design) snapshot it to Redis as it streams and let
a (re)loading editor replay it:

- artifact.py snapshots the generate_page stream every ~512 chars
  (session-keyed, cleared when the generation ends); get_active_build
  returns it.
- The panel re-enters a running turn on load: spinner + pending bubble,
  preview replayed when the build targets the open page, chip restored
  when it doesn't.
- onStream self-heals gaps: a chunk ahead of the local buffer refetches
  the snapshot instead of killing the preview; a generation counter guards
  fetches that resolve across an offset-0 boundary.
- focus_page now commits the session's page pointer immediately — it was
  invisible to web requests until the round's next persist (a generation
  away), so a reloaded editor saw a stale focus: wrong autosave suspension
  and no chip.
- Chip wording: "Building" only for pages the turn created; refocusing an
  existing page shows "Updating" (it read as redoing a finished page).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add .agents/skills/studio-app-building — one skill for every agent that
builds Studio pages, human-driven (Claude Code, Codex CLIs discover it by
convention) or Studio's own runtime:

- SKILL.md: the mechanics — where pages live, stored vs compact block JSON,
  the contracts that bite (variable binds, $event/handleEvent/modifiers,
  arrow-function handler props, repeater scope, filter format), data
  sources, the two page-script forms. A condensed map of prompts.py, which
  stays the source of truth.
- DESIGN.md: the design language, hand-translated from frappe-ui's skill
  (DESIGN.md/TOKENS.md) into Studio vocabulary — style props + espresso CSS
  vars: gray-first principles, the ink ladder, divider-lists-over-boxed-
  cards, density numbers, screen archetypes, empty states, mstyle rules.
  A translation, not a mirror: fold upstream changes in deliberately on
  frappe-ui upgrades.

prompts.py bakes DESIGN.md into all three prompts (generation + both
agents) at import — the generator has no tools, so design knowledge must
ride the prompt; missing file degrades to the old prompt. STYLING_RULES
drops the judgment it duplicated (card radius default, ink role list,
button theme rules) and keeps only mechanics: taste now has one home.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
preview_page screenshots the focused page's /dev draft preview (renders
draft_blocks) via frappe's headless Chromium with a short-lived minted
sid, trims the blank tail, and tiles the capture into readable
screenfuls attached as a follow-up user message (tool results can't
carry images). Screenshot/tiling approach ports from Builder's
preview_page, adapted for app shells: trim diffs against the bottom row
(a sidebar column defeats a colour diff) and ignores chrome pinned to
the canvas bottom (sidebar footers).

The tool result is a three-pass rubric — breakage; sizing & fill
(vision glosses over proportions; confirm symptom vs cause with
read_block); design language (points at the baked rules) — with
Builder's guardrails: static shots can't show motion, admit remaining
problems and stop, never loop screenshots. Caps: 2/page, 8/turn.
Degrades cleanly for text-only models or a dead renderer. Prompt: the
final summary must reflect what was actually SEEN.

Co-Authored-By: Suraj Shetty <surajshetty3416@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The AI only knew a hand-written catalog — around 45 registered
components were invisible to it, and it misused the ones it found
(a frappe-ui List built without columns).

Now its knowledge comes from the same files the editor uses:

- Component names from constants.js, prop types from the json_types
  schemas, and descriptions/defaults/slots/events from frappe-ui's
  generated docs (distilled into component_api.json).
- family_templates.json: the ready-made List / SettingsDialog / Sidebar
  structures the editor inserts, so the AI builds them the same way.
- New tools: list_components and describe_component.
- The system prompt appends an auto-generated list of components the
  catalog doesn't cover, plus the full List structure. Being generated,
  it can't go stale — it already caught Pill, which was in the catalog
  but doesn't exist (removed).
- yarn sync-component-api refreshes the generated files on every
  frappe-ui upgrade.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Type extraction now keeps the JSDoc descriptions and defaults written in
frappe-ui's type files, and also extracts each component's slots and
emits. It additionally scans component folders directly, picking up
sub-components like the Sidebar family that were missed before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The AI now reads props, slots and emits straight from the generated type
schemas the editor already uses, so the separate distilled API file and
its script are gone. The distill script now only builds the family
scaffolds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The catalog now documents date pickers, Autocomplete and @framework/ui
widgets directly. Component families (List, Settings, Sidebar) get their
own block with the editor's canonical structure for each.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Component names now come from data/components.ts (what the panel actually
offers) instead of the stale constants.js arrays. This drops phantom
entries — including Autocomplete, which was advertised but never
registered, so pages using it rendered a missing-component error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Multi-value filters crashed pages three ways:
- the agent emitted flat ["in", "A", "B"] shapes; normalize them to
  ["in", ["A", "B"]] at the tool boundary and reject other malformed lists
  with a corrective message
- getEvaluatedFilters stripped the operator off every [operator, value]
  filter before fetching, degrading "!=" / "like" to equality and sending
  in-lists as bare lists ("too many values to unpack"); keep the pair
  intact and recover legacy flat shapes at fetch time
- the Edit Data Source dialog had no in/not in operators and silently
  dropped values from malformed filters; add the operators with proper
  multi-value inputs (ported from @framework/ui's Filter module: picked
  options for Select, live record search for Link, comma text otherwise)
  and lossless de/serialization

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The studio file explorer (api.py, throwing) and the agent's standard-page
script writer (scripts.py, FAILED string) each carried their own copy of
the developer-mode + System Manager check. Extract
developer_file_access_denial / ensure_developer_file_access into
studio.utils so every code-file write surface shares one gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On a developer bench, the standard-app agent can now author the app's
server side — whitelisted api modules, DocType controllers, shared
helpers — without ever mutating directly:

- pending.py: sensitive tools only PROPOSE. An explicit kind whitelist
  dispatches the stored payload to ctx-free apply functions, run in the
  APPROVING USER's request context. The card persists as a Studio AI
  Message so it survives editor reloads, is superseded by a new turn,
  and the agent auto-resumes once the user decides.
- backend_files.py: realpath jail over the app's Python package with
  .py-only writes; hooks.py/patches.txt/modules.txt/install.py/
  __init__.py are read-only (one line in those registers code to run
  implicitly); path-aware lint — controller paths need the Document
  class, api modules get a whitelist scan with allow_guest surfaced on
  the card; unified diff + content hash for drift detection. The frappe
  framework package is refused as a target.
- tools/backend.py: list/read/write_backend_file. A valid proposal ends
  the turn; an invalid one returns FAILED so the model self-corrects
  in-turn — terminal handlers may now decline by returning a string
  (loop.py feeds it back as the tool result and keeps looping).
- Applying is idempotent and commits before responding: landing a .py
  restarts the dev server, which can kill the very request writing it.
- Prompts: runtime-composed BACKEND_CODE section (standard + dev-mode
  only), backend-before-wiring ordering so pages never call an endpoint
  still pending approval, and read requests must show actual content in
  replies (tool results are visible only to the model).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Approve/Skip card for pending backend writes: colored unified diff,
  lint warnings, and applied/skipped/superseded states. Gated by the
  PERSISTED message status (never lastMessageId) so it survives editor
  reloads and resolves exactly once.
- Approving re-pulls the session before re-entering the live turn (the
  reload replaces the message list, which would orphan the streamed
  bubble), and a failed approve explains the dev-server restart and
  invites a retry instead of printing the raw endpoint name.
- The panel no longer dead-ends: a failed models fetch shows a Retry
  state that distinguishes a restarting server from a logged-out
  session, instead of misreporting "no provider connected" — which hid
  pending approval cards behind an unrecoverable empty state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant