This commit is contained in:
Douwe Osinga
2025-12-10 12:16:33 -05:00
parent ebdcc406a4
commit 5941fe5250
9 changed files with 389 additions and 51 deletions
+2
View File
@@ -391,6 +391,8 @@ derive_utoipa!(Icon as IconSchema);
super::routes::goose_apps::iterate_app,
super::routes::goose_apps::store_app,
super::routes::goose_apps::delete_app,
super::routes::goose_apps::import_app,
super::routes::goose_apps::export_app,
super::routes::recipe::scan_recipe,
super::routes::recipe::list_recipes,
super::routes::recipe::delete_recipe,
+107 -18
View File
@@ -1,21 +1,21 @@
use crate::routes::errors::ErrorResponse;
use crate::state::AppState;
use axum::extract::Query;
use axum::{
extract::{Path, State},
http::StatusCode,
routing::{delete, get, post, put},
Json, Router,
};
use goose::agents::ExtensionManager;
use goose::conversation::message::{Message, MessageContent};
use goose::goose_apps::{GooseApp, GooseAppsManager};
use goose::providers::create_with_named_model;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use axum::extract::Query;
use tokio_util::sync::CancellationToken;
use tracing::{error, warn};
use utoipa::ToSchema;
use goose::agents::ExtensionManager;
#[derive(Deserialize, utoipa::IntoParams, ToSchema)]
pub struct ListAppsRequest {
@@ -53,6 +53,8 @@ pub struct IterateAppRequest {
pub html: String,
pub screenshot_base64: Option<String>,
pub errors: String,
pub width: u32,
pub height: u32,
}
#[derive(Serialize, Deserialize, ToSchema)]
@@ -114,7 +116,10 @@ async fn list_mcp_apps(
});
}
Err(e) => {
warn!("Failed to read resource {} from {}: {}", resource.uri, extension_name, e);
warn!(
"Failed to read resource {} from {}: {}",
resource.uri, extension_name, e
);
}
}
}
@@ -309,9 +314,11 @@ Here is the specification of what the user wants the app to do:
{prd}
{errors}
{screenshot_instruction}
You are also provided a screenshot. Compare the current implementation and the screenshot
with the specification/PRD. If everything looks good and matches the spec, reply with:
Make sure the app matches the desired size the user specified of {width} width and {height} height
If everything looks good and matches the spec, reply with:
DONE
MSG: <anything you want to tell the user>
@@ -338,11 +345,7 @@ If you need to adjust the HTML/CSS/JavaScript, return the complete HTML:
````
MSG: <the modifications you made>
Note: if you change the HTML, you will be called back with the next render, so you
don't have to get it right in one iteration. For complicated things, use multiple turns.
In the message, describe exactly what you see on the screenshot (or say there's no screenshot),
then explain the changes you made or need to make.
{screenshot_note}
"#;
fn iterate_app_prompt(iterate_on: &IterateAppRequest) -> String {
@@ -354,10 +357,25 @@ fn iterate_app_prompt(iterate_on: &IterateAppRequest) -> String {
iterate_on.errors
)
};
let (screenshot_instruction, screenshot_note) = if iterate_on.screenshot_base64.is_some() {
(
"You are also provided a screenshot. Compare the current implementation and the screenshot with the specification/PRD.",
"Note: if you change the HTML, you will be called back with the next render, so you don't have to get it right in one iteration. For complicated things, use multiple turns.\n\nIn the message, describe exactly what you see on the screenshot, then explain the changes you made or need to make."
)
} else {
("", "")
};
ITERATE_APP_PROMPT
.replace("{prd}", &iterate_on.prd)
.replace("{html}", &iterate_on.html)
.replace("{errors}", &errors)
.replace("{screenshot_instruction}", screenshot_instruction)
.replace("{screenshot_note}", screenshot_note)
.replace("{width}", &iterate_on.width.to_string())
.replace("{height}", &iterate_on.height.to_string())
}
fn extract_code_and_message(text: &str) -> (Option<String>, String) {
@@ -415,16 +433,16 @@ async fn iterate_app(
let model_name: String = config.get_goose_model()?;
let provider = create_with_named_model(&provider_name, &model_name).await?;
let message_with_image = Message::user()
.with_text(prompt)
.with_image(&request.screenshot_base64, "image/png".to_string());
let message = if let Some(ref screenshot) = request.screenshot_base64 {
Message::user()
.with_text(prompt)
.with_image(screenshot, "image/png".to_string())
} else {
Message::user().with_text(prompt)
};
let (response, _) = provider
.complete(
"You are a helpful coding assistant.",
&[message_with_image],
&[],
)
.complete("You are a helpful coding assistant.", &[message], &[])
.await
.map_err(|e| ErrorResponse::internal(format!("Provider error: {}", e)))?;
@@ -478,6 +496,75 @@ async fn delete_app(
}))
}
#[utoipa::path(
get,
path = "/apps/export/{name}",
responses(
(status = 200, description = "App HTML exported successfully"),
(status = 404, description = "App not found", body = ErrorResponse),
),
params(
("name" = String, Path, description = "Name of the app to export")
),
security(
("api_key" = [])
),
tag = "App Management"
)]
async fn export_app(
State(_state): State<Arc<AppState>>,
Path(name): Path<String>,
) -> Result<String, ErrorResponse> {
let manager = GooseAppsManager::new()?;
let app = manager.get_app(&name)?;
match app {
Some(app) => app
.to_file_content()
.map_err(|e| ErrorResponse::internal(format!("Failed to generate HTML: {}", e))),
None => Err(ErrorResponse::internal("App not found")),
}
}
#[utoipa::path(
post,
path = "/apps/import",
request_body = String,
responses(
(status = 201, description = "App imported successfully", body = SuccessResponse),
(status = 400, description = "Bad request - Invalid HTML", body = ErrorResponse),
),
security(
("api_key" = [])
),
tag = "App Management"
)]
async fn import_app(
State(_state): State<Arc<AppState>>,
body: String,
) -> Result<(StatusCode, Json<SuccessResponse>), ErrorResponse> {
let manager = GooseAppsManager::new()?;
let mut app = GooseApp::from_html(&body)
.map_err(|e| ErrorResponse::internal(format!("Invalid Goose App HTML: {}", e)))?;
let original_name = app.name.clone();
let mut counter = 1;
while manager.app_exists(&app.name) {
app.name = format!("{}_{}", original_name, counter);
counter += 1;
}
manager.update_app(&app)?;
Ok((
StatusCode::CREATED,
Json(SuccessResponse {
message: format!("App '{}' imported successfully", app.name),
}),
))
}
pub fn routes(state: Arc<AppState>) -> Router {
Router::new()
.route("/apps", post(create_app))
@@ -486,5 +573,7 @@ pub fn routes(state: Arc<AppState>) -> Router {
.route("/apps/app/{name}", put(store_app))
.route("/apps/app/{name}", delete(delete_app))
.route("/apps/app/{name}", get(get_app))
.route("/apps/import", post(import_app))
.route("/apps/export/{name}", get(export_app))
.with_state(state)
}
+14 -6
View File
@@ -39,7 +39,10 @@ use crate::config::{get_all_extensions, Config};
use crate::oauth::oauth_flow;
use crate::prompt_template;
use crate::subprocess::configure_command_no_window;
use rmcp::model::{CallToolRequestParam, Content, ErrorCode, ErrorData, GetPromptResult, Prompt, RawContent, Resource, ResourceContents, ServerInfo, Tool};
use rmcp::model::{
CallToolRequestParam, Content, ErrorCode, ErrorData, GetPromptResult, Prompt, RawContent,
Resource, ResourceContents, ServerInfo, Tool,
};
use rmcp::transport::auth::AuthClient;
use schemars::_private::NoSerialize;
use serde_json::Value;
@@ -843,9 +846,7 @@ impl ExtensionManager {
Ok(result)
}
pub async fn get_ui_resources(
&self,
) -> Result<Vec<(String, Resource)>, ErrorData> {
pub async fn get_ui_resources(&self) -> Result<Vec<(String, Resource)>, ErrorData> {
let mut ui_resources = Vec::new();
let extensions_to_check: Vec<(String, McpClientBox)> = {
@@ -862,9 +863,16 @@ impl ExtensionManager {
info!("Checking extension: {}", extension_name);
let client_guard = client.lock().await;
match client_guard.list_resources(None, CancellationToken::default()).await {
match client_guard
.list_resources(None, CancellationToken::default())
.await
{
Ok(list_response) => {
info!("List resources for {}: {} resources found", extension_name, list_response.resources.len());
info!(
"List resources for {}: {} resources found",
extension_name,
list_response.resources.len()
);
for resource in list_response.resources {
if resource.uri.starts_with("ui://") {
ui_resources.push((extension_name.clone(), resource));
+99 -3
View File
@@ -746,6 +746,90 @@
]
}
},
"/apps/export/{name}": {
"get": {
"tags": [
"App Management"
],
"operationId": "export_app",
"parameters": [
{
"name": "name",
"in": "path",
"description": "Name of the app to export",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "App HTML exported successfully"
},
"404": {
"description": "App not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/apps/import": {
"post": {
"tags": [
"App Management"
],
"operationId": "import_app",
"requestBody": {
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
},
"required": true
},
"responses": {
"201": {
"description": "App imported successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SuccessResponse"
}
}
}
},
"400": {
"description": "Bad request - Invalid HTML",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/apps/iterate": {
"post": {
"tags": [
@@ -4005,13 +4089,19 @@
"required": [
"prd",
"html",
"screenshotBase64",
"errors"
"errors",
"width",
"height"
],
"properties": {
"errors": {
"type": "string"
},
"height": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"html": {
"type": "string"
},
@@ -4019,7 +4109,13 @@
"type": "string"
},
"screenshotBase64": {
"type": "string"
"type": "string",
"nullable": true
},
"width": {
"type": "integer",
"format": "int32",
"minimum": 0
}
}
},
+3 -3
View File
@@ -39,8 +39,8 @@ import GooseAppsView from './components/apps/GooseAppsView';
import { NoProviderOrModelError, useAgent } from './hooks/useAgent';
import { useNavigation } from './hooks/useNavigation';
import { errorMessage } from './utils/conversionUtils';
import Hub from './components/hub';
import Pair, { PairRouteState } from './components/pair';
import Hub from './components/Hub';
import Pair, { PairRouteState } from './components/Pair';
// Route Components
const HubRouteWrapper = ({ isExtensionsLoading }: { isExtensionsLoading: boolean }) => {
@@ -672,7 +672,7 @@ export function AppInner() {
<Route path="sessions" element={<SessionsRoute />} />
<Route path="schedules" element={<SchedulesRoute />} />
<Route path="recipes" element={<RecipesRoute />} />
<Route path="apps" element={<GooseAppsView sessionId={chat.sessionId}/>} />
<Route path="apps" element={<GooseAppsView/>} />
<Route
path="shared-session"
element={
+13 -1
View File
@@ -2,7 +2,7 @@
import type { Client, Options as Options2, TDataShape } from './client';
import { client } from './client.gen';
import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CallToolData, CallToolErrors, CallToolResponses, CheckProviderData, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateAppData, CreateAppErrors, CreateAppResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteAppData, DeleteAppErrors, DeleteAppResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetAppData, GetAppErrors, GetAppResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, IterateAppData, IterateAppErrors, IterateAppResponses, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsErrors, ListAppsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, StoreAppData, StoreAppErrors, StoreAppResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen';
import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CallToolData, CallToolErrors, CallToolResponses, CheckProviderData, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateAppData, CreateAppErrors, CreateAppResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteAppData, DeleteAppErrors, DeleteAppResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportAppData, ExportAppErrors, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetAppData, GetAppErrors, GetAppResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportAppData, ImportAppErrors, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, IterateAppData, IterateAppErrors, IterateAppResponses, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsErrors, ListAppsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, StoreAppData, StoreAppErrors, StoreAppResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen';
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
/**
@@ -132,6 +132,18 @@ export const storeApp = <ThrowOnError extends boolean = false>(options: Options<
}
});
export const exportApp = <ThrowOnError extends boolean = false>(options: Options<ExportAppData, ThrowOnError>) => (options.client ?? client).get<ExportAppResponses, ExportAppErrors, ThrowOnError>({ url: '/apps/export/{name}', ...options });
export const importApp = <ThrowOnError extends boolean = false>(options: Options<ImportAppData, ThrowOnError>) => (options.client ?? client).post<ImportAppResponses, ImportAppErrors, ThrowOnError>({
bodySerializer: null,
url: '/apps/import',
...options,
headers: {
'Content-Type': 'text/plain',
...options.headers
}
});
export const iterateApp = <ThrowOnError extends boolean = false>(options: Options<IterateAppData, ThrowOnError>) => (options.client ?? client).post<IterateAppResponses, IterateAppErrors, ThrowOnError>({
url: '/apps/iterate',
...options,
+56 -1
View File
@@ -382,9 +382,11 @@ export type InspectJobResponse = {
export type IterateAppRequest = {
errors: string;
height: number;
html: string;
prd: string;
screenshotBase64: string;
screenshotBase64?: string | null;
width: number;
};
export type IterateAppResponse = {
@@ -1579,6 +1581,59 @@ export type StoreAppResponses = {
export type StoreAppResponse = StoreAppResponses[keyof StoreAppResponses];
export type ExportAppData = {
body?: never;
path: {
/**
* Name of the app to export
*/
name: string;
};
query?: never;
url: '/apps/export/{name}';
};
export type ExportAppErrors = {
/**
* App not found
*/
404: ErrorResponse;
};
export type ExportAppError = ExportAppErrors[keyof ExportAppErrors];
export type ExportAppResponses = {
/**
* App HTML exported successfully
*/
200: unknown;
};
export type ImportAppData = {
body: string;
path?: never;
query?: never;
url: '/apps/import';
};
export type ImportAppErrors = {
/**
* Bad request - Invalid HTML
*/
400: ErrorResponse;
};
export type ImportAppError = ImportAppErrors[keyof ImportAppErrors];
export type ImportAppResponses = {
/**
* App imported successfully
*/
201: SuccessResponse;
};
export type ImportAppResponse = ImportAppResponses[keyof ImportAppResponses];
export type IterateAppData = {
body: IterateAppRequest;
path?: never;
@@ -105,13 +105,14 @@ export default function GooseAppEditor({ app, onReturn }: GooseAppEditorProps) {
prd,
screenshotBase64,
errors: iframeErrors.join('\n'),
width: parseInt(width) || 240, height: parseInt(height) || 320
},
throwOnError: true,
});
setIterationMessage(response.data.message);
if (response.data.done) {
if (response.data.done || !screenshotBase64) {
done = true;
setIterationMessage('Done! ' + response.data.message);
} else {
@@ -1,8 +1,15 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { MainPanelLayout } from '../Layout/MainPanelLayout';
import { Button } from '../ui/button';
import { Play, Plus, Trash, Pencil } from 'lucide-react';
import { createApp, deleteApp, GooseApp, listApps, resumeAgent } from '../../api';
import { Play, Plus, Trash, Pencil, Download, Upload } from 'lucide-react';
import {
deleteApp,
exportApp,
GooseApp,
importApp,
listApps,
resumeAgent,
} from '../../api';
import GooseAppEditor from './GooseAppEditor';
import { createSession } from '../../sessions';
@@ -70,17 +77,17 @@ export default function GooseAppsView() {
try {
const response = await listApps({ throwOnError: true, query: { session_id: appsSessionId } });
const apps = response.data?.apps || [];
if (apps.length === 0) {
await createApp({
throwOnError: true,
body: {
app: {
name: '',
},
},
});
return await loadApps();
}
// if (apps.length === 0) {
// await createApp({
// throwOnError: true,
// body: {
// app: {
// name: '',
// },
// },
// });
// return await loadApps();
// }
setApps(apps);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load apps');
@@ -91,6 +98,53 @@ export default function GooseAppsView() {
loadApps();
}, [loadApps]);
const handleDownloadApp = async (app: GooseApp) => {
try {
const response = await exportApp({
throwOnError: true,
path: { name: app.name }
});
if (response.data) {
const blob = new Blob([response.data as string], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${app.name}.html`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to export app');
}
};
const fileInputRef = useRef<HTMLInputElement>(null);
const handleImportClick = () => {
fileInputRef.current?.click();
};
const handleUploadApp = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
try {
const text = await file.text();
await importApp({
throwOnError: true,
body: text,
});
await loadApps();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to import app');
}
event.target.value = '';
};
const handleLaunchApp = async (app: GooseApp) => {
await window.electron.launchGooseApp(app);
};
@@ -140,14 +194,27 @@ export default function GooseAppsView() {
return (
<MainPanelLayout>
<div className="flex flex-col min-w-0 flex-1 overflow-y-auto relative">
<div className="bg-background-default px-8 pb-4 pt-16">
<div className="flex-1 flex flex-col min-h-0">
<div className="bg-background-default px-8 pb-8 pt-16">
<div className="flex flex-col page-transition">
<div className="flex justify-between items-center mb-1">
<h1 className="text-4xl font-light">Apps</h1>
<label>
<Button onClick={handleImportClick} variant="outline" size="sm" className="flex items-center gap-2">
<Upload className="h-4 w-4" />
Import App
</Button>
<input
ref={fileInputRef}
type="file"
accept=".html"
onChange={handleUploadApp}
style={{ display: 'none' }}
/>
</label>
</div>
<p className="text-sm text-text-muted mb-6">
Self-contained Html applications that run within Goose.
<p className="text-sm text-text-muted mb-4">
Self-contained HTML applications that run within Goose.
</p>
</div>
</div>
@@ -188,6 +255,14 @@ export default function GooseAppsView() {
>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDownloadApp(app)}
className="flex items-center gap-2"
>
<Download className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"