diff --git a/backend/apps/agents/agent_manager.py b/backend/apps/agents/agent_manager.py
index 955ef29f..d7e4f754 100644
--- a/backend/apps/agents/agent_manager.py
+++ b/backend/apps/agents/agent_manager.py
@@ -56,6 +56,7 @@ from backend.apps.agents.manager.session.history_compaction import (
)
from backend.apps.agents.manager.prompt.prompt_context import (
_build_browser_context,
+ _build_selected_app_context,
_build_connected_tools_context,
_build_mcp_registry_summary,
_compose_system_prompt,
@@ -212,6 +213,9 @@ class AgentManager:
def _build_browser_context(self, dashboard_id: str | None, selected_browser_ids: list[str] | None = None) -> str | None:
return _build_browser_context(dashboard_id, selected_browser_ids)
+ def _build_selected_app_context(self, selected_app_output_ids: list[str] | None) -> str | None:
+ return _build_selected_app_context(selected_app_output_ids)
+
def _get_pre_selected_browser_ids(self, dashboard_id: str | None) -> list[str]:
return _get_pre_selected_browser_ids(dashboard_id)
@@ -384,7 +388,7 @@ class AgentManager:
def _resolve_context_paths(self, context_paths: list | None) -> str:
return _resolve_context_paths(context_paths)
- async def _run_agent_loop(self, session_id: str, prompt: str, images: list | None = None, context_paths: list | None = None, forced_tools: list[str] | None = None, attached_skills: list | None = None, fork_session: bool = False, selected_browser_ids: list[str] | None = None):
+ async def _run_agent_loop(self, session_id: str, prompt: str, images: list | None = None, context_paths: list | None = None, forced_tools: list[str] | None = None, attached_skills: list | None = None, fork_session: bool = False, selected_browser_ids: list[str] | None = None, selected_app_output_ids: list[str] | None = None):
"""Run the Claude Agent SDK query loop for a session."""
session = self.sessions.get(session_id)
if not session:
@@ -1107,6 +1111,14 @@ class AgentManager:
skill_block = f"\n{load_app_builder_skill()}\n"
composed_prompt = f"{composed_prompt}\n\n{skill_block}" if composed_prompt else skill_block
+ # App cards the user picked via the dashboard element picker: give
+ # the agent each app's on-disk path + meta + SKILL.md pointer so it
+ # can edit them in place (the dashboard card's runtime live-reloads).
+ # Additive and independent of view-builder mode above.
+ app_ctx = self._build_selected_app_context(selected_app_output_ids)
+ if app_ctx:
+ composed_prompt = f"{composed_prompt}\n\n{app_ctx}" if composed_prompt else app_ctx
+
# Per-turn estimate of framework overhead (subtracted from displayed
# input). Conservative on purpose so honest over-shows beat lies.
# 16K Claude Code preset, 12K base+deferred tools, ~3K/MCP (real
@@ -3214,6 +3226,7 @@ class AgentManager:
attached_skills: list | None = None,
hidden: bool = False,
selected_browser_ids: list[str] | None = None,
+ selected_app_output_ids: list[str] | None = None,
client_message_id: str | None = None,
):
"""Send a follow-up message to an existing session."""
@@ -3341,7 +3354,7 @@ class AgentManager:
if fast_verdict != "no":
task = asyncio.create_task(self._run_browser_fast_path(session_id, prompt, selected_browser_ids, fast_brief, fast_verdict))
else:
- task = asyncio.create_task(self._run_agent_loop(session_id, prompt, images=images, context_paths=context_paths, forced_tools=forced_tools, attached_skills=attached_skills, selected_browser_ids=selected_browser_ids))
+ task = asyncio.create_task(self._run_agent_loop(session_id, prompt, images=images, context_paths=context_paths, forced_tools=forced_tools, attached_skills=attached_skills, selected_browser_ids=selected_browser_ids, selected_app_output_ids=selected_app_output_ids))
self.tasks[session_id] = task
async def _run_browser_fast_path(self, session_id: str, prompt: str, selected_browser_ids: list[str] | None, brief: str = "", verdict: str = "act"):
diff --git a/backend/apps/agents/agents.py b/backend/apps/agents/agents.py
index f32e05f5..537b9e89 100644
--- a/backend/apps/agents/agents.py
+++ b/backend/apps/agents/agents.py
@@ -98,6 +98,7 @@ async def send_message(session_id: str, body: dict):
attached_skills=body.get("attached_skills"),
hidden=body.get("hidden", False),
selected_browser_ids=body.get("selected_browser_ids"),
+ selected_app_output_ids=body.get("selected_app_output_ids"),
client_message_id=body.get("client_message_id"),
)
return {"ok": True}
diff --git a/backend/apps/agents/manager/prompt/prompt_context.py b/backend/apps/agents/manager/prompt/prompt_context.py
index 16afb6c7..116ffec3 100644
--- a/backend/apps/agents/manager/prompt/prompt_context.py
+++ b/backend/apps/agents/manager/prompt/prompt_context.py
@@ -174,6 +174,65 @@ def _build_browser_context(dashboard_id: str | None, selected_browser_ids: list[
return "\n".join(lines)
+def _build_selected_app_context(selected_app_output_ids: list[str] | None) -> str | None:
+ """Build a context block for dashboard App cards the user selected to edit.
+
+ Resolves each Output id to its on-disk workspace so the agent edits the
+ right files; the dashboard card's Vite runtime live-reloads on save. Skips
+ deleted apps / missing folders, returns None if nothing resolves.
+ """
+ if not selected_app_output_ids:
+ return None
+ import os
+ from backend.apps.outputs.workspace_io import load_output
+ from backend.config.paths import OUTPUTS_WORKSPACE_DIR
+
+ entries: list[str] = []
+ for output_id in selected_app_output_ids:
+ try:
+ output = load_output(output_id)
+ except Exception:
+ output = None
+ if not output or not output.workspace_id:
+ continue
+ path = os.path.abspath(os.path.join(OUTPUTS_WORKSPACE_DIR, output.workspace_id))
+ if not os.path.isdir(path):
+ continue
+ meta_raw = ""
+ meta_path = os.path.join(path, "meta.json")
+ if os.path.isfile(meta_path):
+ try:
+ with open(meta_path, "r", encoding="utf-8") as f:
+ meta_raw = f.read().strip()
+ except Exception:
+ meta_raw = ""
+ name = output.name or "Untitled App"
+ lines = [
+ f'- App: "{name}"',
+ f" Workspace path: {path}",
+ f" Entry point: {os.path.join(path, 'index.html')}",
+ ]
+ if meta_raw:
+ lines.append(f" meta.json: {meta_raw}")
+ lines.append(
+ f" Before changing anything, Read {os.path.join(path, 'SKILL.md')} "
+ f"for the App platform spec."
+ )
+ entries.append("\n".join(lines))
+
+ if not entries:
+ return None
+ return (
+ "\n"
+ "The user selected these App cards on the dashboard for you to edit. "
+ "They are existing web apps; edit the files in place at the paths below "
+ "and the dashboard preview live-reloads on save. Do not scaffold a new "
+ "project or write files anywhere else.\n\n"
+ + "\n\n".join(entries)
+ + "\n"
+ )
+
+
def _get_pre_selected_browser_ids(dashboard_id: str | None) -> list[str]:
"""Return browser_ids of all browser cards currently on the dashboard."""
if not dashboard_id:
diff --git a/frontend/src/app/pages/AgentChat/AgentChat.tsx b/frontend/src/app/pages/AgentChat/AgentChat.tsx
index b0925caf..4723f036 100644
--- a/frontend/src/app/pages/AgentChat/AgentChat.tsx
+++ b/frontend/src/app/pages/AgentChat/AgentChat.tsx
@@ -147,6 +147,7 @@ interface QueuedMessage {
forcedTools?: string[];
attachedSkills?: Array<{ id: string; name: string; content: string }>;
selectedBrowserIds?: string[];
+ selectedAppIds?: string[];
}
interface AgentChatProps {
@@ -300,7 +301,7 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose
if (session?.system_prompt) config.system_prompt = session.system_prompt;
if (session?.target_directory) config.target_directory = session.target_directory;
dispatch(
- launchAndSendFirstMessage({ draftId: id, config, prompt: msg.prompt, mode, model, images: msg.images, contextPaths: msg.contextPaths, forcedTools: msg.forcedTools, attachedSkills: msg.attachedSkills, selectedBrowserIds: msg.selectedBrowserIds })
+ launchAndSendFirstMessage({ draftId: id, config, prompt: msg.prompt, mode, model, images: msg.images, contextPaths: msg.contextPaths, forcedTools: msg.forcedTools, attachedSkills: msg.attachedSkills, selectedBrowserIds: msg.selectedBrowserIds, selectedAppIds: msg.selectedAppIds })
).then((action) => {
if (launchAndSendFirstMessage.fulfilled.match(action)) {
const realId = action.payload.session.id;
@@ -314,7 +315,7 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose
if (msg.selectedBrowserIds?.length) {
dispatch(setGlowingBrowserCards({ browserIds: msg.selectedBrowserIds, sessionId: id, label: 'Use Browser' }));
}
- dispatch(sendMessageThunk({ sessionId: id, prompt: msg.prompt, mode, model, images: msg.images, contextPaths: msg.contextPaths, forcedTools: msg.forcedTools, attachedSkills: msg.attachedSkills, selectedBrowserIds: msg.selectedBrowserIds }))
+ dispatch(sendMessageThunk({ sessionId: id, prompt: msg.prompt, mode, model, images: msg.images, contextPaths: msg.contextPaths, forcedTools: msg.forcedTools, attachedSkills: msg.attachedSkills, selectedBrowserIds: msg.selectedBrowserIds, selectedAppIds: msg.selectedAppIds }))
.then((action) => {
if (sendMessageThunk.rejected.match(action)) {
setAwaitingResponse(false);
@@ -580,10 +581,11 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose
forcedTools?: string[],
attachedSkills?: Array<{ id: string; name: string; content: string }>,
selectedBrowserIds?: string[],
+ selectedAppIds?: string[],
) => {
if (!id) return;
scrollToBottom();
- const msg: QueuedMessage = { prompt, images, contextPaths, forcedTools, attachedSkills, selectedBrowserIds };
+ const msg: QueuedMessage = { prompt, images, contextPaths, forcedTools, attachedSkills, selectedBrowserIds, selectedAppIds };
if (agentBusy) {
messageQueueRef.current.push(msg);
setQueueLength(messageQueueRef.current.length);
diff --git a/frontend/src/app/pages/AgentChat/ChatInput.tsx b/frontend/src/app/pages/AgentChat/ChatInput.tsx
index 243a0c51..9d02a1f4 100644
--- a/frontend/src/app/pages/AgentChat/ChatInput.tsx
+++ b/frontend/src/app/pages/AgentChat/ChatInput.tsx
@@ -23,7 +23,7 @@ export type { AttachedImage, ForcedToolGroup, ChatInputHandle };
export type { AttachedSkill } from '@/app/components/editor/richEditorUtils';
interface Props {
- onSend: (message: string, images?: Array<{ data: string; media_type: string }>, contextPaths?: ContextPath[], forcedTools?: string[], attachedSkills?: Array<{ id: string; name: string; content: string }>, selectedBrowserIds?: string[]) => void;
+ onSend: (message: string, images?: Array<{ data: string; media_type: string }>, contextPaths?: ContextPath[], forcedTools?: string[], attachedSkills?: Array<{ id: string; name: string; content: string }>, selectedBrowserIds?: string[], selectedAppIds?: string[]) => void;
disabled?: boolean;
mode: string;
onModeChange: (mode: string) => void;
@@ -213,6 +213,9 @@ const ChatInput = forwardRef(({ onSend, disabled, mode,
const browserIds = selectedEls
.filter((el) => el.semanticType === 'browser-card' && el.semanticData?.selectId)
.map((el) => el.semanticData!.selectId as string);
+ const appIds = selectedEls
+ .filter((el) => el.semanticType === 'view-card' && el.semanticData?.selectId)
+ .map((el) => el.semanticData!.selectId as string);
onSend(
trimmed,
sendImages,
@@ -220,6 +223,7 @@ const ChatInput = forwardRef(({ onSend, disabled, mode,
allForcedToolNames.length > 0 ? allForcedToolNames : undefined,
sendSkills,
browserIds.length > 0 ? browserIds : undefined,
+ appIds.length > 0 ? appIds : undefined,
);
if (editor.tagName === 'TEXTAREA') (editor as unknown as HTMLTextAreaElement).value = ''; else editor.innerHTML = '';
deleteDraft(ownerId);
diff --git a/frontend/src/shared/state/agentsSlice.ts b/frontend/src/shared/state/agentsSlice.ts
index 2e1d5b4c..291a9a5b 100644
--- a/frontend/src/shared/state/agentsSlice.ts
+++ b/frontend/src/shared/state/agentsSlice.ts
@@ -191,6 +191,7 @@ export interface SendMessagePayload {
attachedSkills?: Array<{ id: string; name: string; content: string }>;
hidden?: boolean;
selectedBrowserIds?: string[];
+ selectedAppIds?: string[];
}
function _genOptimisticId(): string {
@@ -199,7 +200,7 @@ function _genOptimisticId(): string {
export const sendMessage = createAsyncThunk(
'agents/sendMessage',
- async ({ sessionId, prompt, mode, model, provider, images, contextPaths, forcedTools, attachedSkills, hidden, selectedBrowserIds }: SendMessagePayload, { dispatch }) => {
+ async ({ sessionId, prompt, mode, model, provider, images, contextPaths, forcedTools, attachedSkills, hidden, selectedBrowserIds, selectedAppIds }: SendMessagePayload, { dispatch }) => {
// Mint client id and dispatch optimistic bubble before awaiting the network; id round-trips for echo dedupe.
const clientMessageId = _genOptimisticId();
dispatch(addOptimisticMessage({
@@ -216,7 +217,7 @@ export const sendMessage = createAsyncThunk(
const res = await fetch(`${AGENTS_API}/sessions/${sessionId}/message`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ prompt, mode, model, provider, images, context_paths: contextPaths, forced_tools: forcedTools, attached_skills: attachedSkills, hidden, selected_browser_ids: selectedBrowserIds, client_message_id: clientMessageId }),
+ body: JSON.stringify({ prompt, mode, model, provider, images, context_paths: contextPaths, forced_tools: forcedTools, attached_skills: attachedSkills, hidden, selected_browser_ids: selectedBrowserIds, selected_app_output_ids: selectedAppIds, client_message_id: clientMessageId }),
});
if (!res.ok) throw new Error(`send failed: ${res.status}`);
} catch (err) {
@@ -276,6 +277,7 @@ export interface LaunchAndSendPayload {
attachedSkills?: Array<{ id: string; name: string; content: string }>;
expand?: boolean;
selectedBrowserIds?: string[];
+ selectedAppIds?: string[];
}
export const fetchSession = createAsyncThunk(
@@ -293,7 +295,7 @@ export const fetchSession = createAsyncThunk(
export const launchAndSendFirstMessage = createAsyncThunk(
'agents/launchAndSendFirstMessage',
- async ({ draftId, config, prompt, mode, model, provider, images, contextPaths, forcedTools, attachedSkills, selectedBrowserIds }: LaunchAndSendPayload) => {
+ async ({ draftId, config, prompt, mode, model, provider, images, contextPaths, forcedTools, attachedSkills, selectedBrowserIds, selectedAppIds }: LaunchAndSendPayload) => {
const launchRes = await fetch(`${AGENTS_API}/launch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -305,7 +307,7 @@ export const launchAndSendFirstMessage = createAsyncThunk(
await fetch(`${AGENTS_API}/sessions/${session.id}/message`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ prompt, mode, model, provider, images, context_paths: contextPaths, forced_tools: forcedTools, attached_skills: attachedSkills, selected_browser_ids: selectedBrowserIds }),
+ body: JSON.stringify({ prompt, mode, model, provider, images, context_paths: contextPaths, forced_tools: forcedTools, attached_skills: attachedSkills, selected_browser_ids: selectedBrowserIds, selected_app_output_ids: selectedAppIds }),
});
const refreshRes = await fetch(`${AGENTS_API}/sessions/${session.id}`);