mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-07 10:17:43 +02:00
Inject selected-app context into dashboard agent chat (#69)
When a user selects an App card on the dashboard via the "Select UI element" picker, thread the selected Output ids to the backend (mirroring the selected_browser_ids path) and inject a per-app context block into the system prompt: each app's absolute workspace path, entry point, meta.json, and a pointer to its SKILL.md. This lets the agent edit existing apps in place from the main view (the dashboard card's Vite runtime live-reloads on save) without switching to the Apps tab.
This commit is contained in:
@@ -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"<app_builder_reference>\n{load_app_builder_skill()}\n</app_builder_reference>"
|
||||
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"):
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 (
|
||||
"<selected_app_context>\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</selected_app_context>"
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
|
||||
@@ -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<AgentChatProps> = ({ 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<AgentChatProps> = ({ 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<AgentChatProps> = ({ 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);
|
||||
|
||||
@@ -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<ChatInputHandle, Props>(({ 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<ChatInputHandle, Props>(({ 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);
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
Reference in New Issue
Block a user