diff --git a/backend/apps/outputs/models.py b/backend/apps/outputs/models.py index 70065106..9f64b12a 100644 --- a/backend/apps/outputs/models.py +++ b/backend/apps/outputs/models.py @@ -1,8 +1,12 @@ -from pydantic import BaseModel, Field, model_validator +import os + +from pydantic import BaseModel, Field, computed_field, model_validator from typing import Literal, Optional, Any from uuid import uuid4 from datetime import datetime +from backend.config.paths import OUTPUTS_WORKSPACE_DIR + class Output(BaseModel): id: str = Field(default_factory=lambda: uuid4().hex) @@ -49,6 +53,14 @@ class Output(BaseModel): data.pop("backend_code", None) return data + @computed_field + @property + def workspace_path(self) -> str: + """Absolute on-disk folder for this app, resolved from workspace_id. API-only (excluded from the saved JSON since it's machine-specific and re-derivable); lets the frontend show the real edit path when an App card is selected.""" + if not self.workspace_id: + return "" + return os.path.abspath(os.path.join(OUTPUTS_WORKSPACE_DIR, self.workspace_id)) + @property def frontend_code(self) -> str: return self.files.get("index.html", "") diff --git a/backend/apps/outputs/workspace_io.py b/backend/apps/outputs/workspace_io.py index 3cb7bcec..3fadfa9a 100644 --- a/backend/apps/outputs/workspace_io.py +++ b/backend/apps/outputs/workspace_io.py @@ -30,7 +30,7 @@ def load_all() -> list[Output]: def save(output: Output): - atomic_write_json(os.path.join(DATA_DIR, f"{output.id}.json"), output.model_dump()) + atomic_write_json(os.path.join(DATA_DIR, f"{output.id}.json"), output.model_dump(exclude={"workspace_path"})) def load(output_id: str) -> Output: diff --git a/frontend/src/app/pages/AgentChat/ChatInput/sendHelpers.ts b/frontend/src/app/pages/AgentChat/ChatInput/sendHelpers.ts index 340894b6..e40b7f73 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput/sendHelpers.ts +++ b/frontend/src/app/pages/AgentChat/ChatInput/sendHelpers.ts @@ -79,13 +79,23 @@ export function appendSelectedElements(trimmed: string, selectedEls: SelectedEle lines.push(` browser_id: ${el.semanticData.selectId}`); if (url) lines.push(` URL: ${url}`); lines.push(` (Use BrowserAgent with this browser_id to interact with it, or CreateBrowserAgent for a new browser)`); + } else if (el.semanticType === 'view-card') { + const appName = typeof el.semanticData?.name === 'string' ? el.semanticData.name : (el.semanticLabel || 'Untitled App'); + const appPath = typeof el.semanticData?.path === 'string' ? el.semanticData.path : ''; + lines.push(`${i + 1}. [App Card] ${appName}`); + if (appPath) { + lines.push(` Workspace path: ${appPath}`); + lines.push(` (Edit this app's files in place at the path above. Don't recreate it or write files elsewhere.)`); + } else { + // No resolved path means the app's folder is gone or unsaved; tell the agent rather than emit a confusing opaque id. + lines.push(` (This app has no on-disk workspace yet; ask the user to save or reopen it before editing.)`); + } } else if (el.semanticType && el.semanticData) { const typeLabel = { 'agent-card': 'Agent Card', 'message': 'Message', 'tool-call': 'Tool Call', 'tool-group': 'Tool Group', - 'view-card': 'App Card', 'browser-card': 'Browser Card', 'workflow-card': 'Workflow Card', 'workflows-hub-card': 'Workflows Hub', diff --git a/frontend/src/app/pages/Dashboard/cards/DashboardViewCard.tsx b/frontend/src/app/pages/Dashboard/cards/DashboardViewCard.tsx index 784310d0..09ee2bb2 100644 --- a/frontend/src/app/pages/Dashboard/cards/DashboardViewCard.tsx +++ b/frontend/src/app/pages/Dashboard/cards/DashboardViewCard.tsx @@ -342,7 +342,7 @@ const DashboardViewCard: React.FC = ({ onBringToFront?.(output.id, 'view')} onClick={(e: React.MouseEvent) => { if (justDraggedRef.current) return; diff --git a/frontend/src/shared/state/outputsSlice.ts b/frontend/src/shared/state/outputsSlice.ts index bfd52ecd..7ad98e62 100644 --- a/frontend/src/shared/state/outputsSlice.ts +++ b/frontend/src/shared/state/outputsSlice.ts @@ -20,6 +20,8 @@ export interface Output { /** Linkage so reopening App Builder reattaches to the in-progress session and workspace. */ session_id?: string | null; workspace_id?: string | null; + /** Backend-resolved absolute on-disk folder for this app; API-only, empty when no workspace_id. */ + workspace_path?: string; created_at: string; updated_at: string; /** App publishing to {slug}.openswarm.host. Server-managed; mirrored here after a publish/unpublish. */