feat: app-wide agentic mode - #244
Draft
ruchamahabal wants to merge 31 commits into
Draft
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.