goose remote access (#5251)

This commit is contained in:
Michael Neale
2025-11-25 14:54:34 +11:00
committed by GitHub
parent f56cb8764e
commit 08d0a7cb20
22 changed files with 2069 additions and 212 deletions
+31 -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, CheckProviderData, ConfirmPermissionData, ConfirmPermissionErrors, ConfirmPermissionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, 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, 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, StatusData, StatusResponses, 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, CheckProviderData, ConfirmPermissionData, ConfirmPermissionErrors, ConfirmPermissionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, 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, KillRunningJobData, KillRunningJobResponses, 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, 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, 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> & {
/**
@@ -604,3 +604,33 @@ export const status = <ThrowOnError extends boolean = false>(options?: Options<S
...options
});
};
/**
* Start the tunnel
*/
export const startTunnel = <ThrowOnError extends boolean = false>(options?: Options<StartTunnelData, ThrowOnError>) => {
return (options?.client ?? client).post<StartTunnelResponses, StartTunnelErrors, ThrowOnError>({
url: '/tunnel/start',
...options
});
};
/**
* Get tunnel info
*/
export const getTunnelStatus = <ThrowOnError extends boolean = false>(options?: Options<GetTunnelStatusData, ThrowOnError>) => {
return (options?.client ?? client).get<GetTunnelStatusResponses, unknown, ThrowOnError>({
url: '/tunnel/status',
...options
});
};
/**
* Stop the tunnel
*/
export const stopTunnel = <ThrowOnError extends boolean = false>(options?: Options<StopTunnelData, ThrowOnError>) => {
return (options?.client ?? client).post<StopTunnelResponses, StopTunnelErrors, ThrowOnError>({
url: '/tunnel/stop',
...options
});
};
+77
View File
@@ -921,6 +921,15 @@ export type ToolResponse = {
};
};
export type TunnelInfo = {
hostname: string;
secret: string;
state: TunnelState;
url: string;
};
export type TunnelState = 'idle' | 'starting' | 'running' | 'error' | 'disabled';
export type UpdateCustomProviderRequest = {
api_key: string;
api_url: string;
@@ -2728,3 +2737,71 @@ export type StatusResponses = {
};
export type StatusResponse = StatusResponses[keyof StatusResponses];
export type StartTunnelData = {
body?: never;
path?: never;
query?: never;
url: '/tunnel/start';
};
export type StartTunnelErrors = {
/**
* Bad request
*/
400: ErrorResponse;
/**
* Internal server error
*/
500: ErrorResponse;
};
export type StartTunnelError = StartTunnelErrors[keyof StartTunnelErrors];
export type StartTunnelResponses = {
/**
* Tunnel started successfully
*/
200: TunnelInfo;
};
export type StartTunnelResponse = StartTunnelResponses[keyof StartTunnelResponses];
export type GetTunnelStatusData = {
body?: never;
path?: never;
query?: never;
url: '/tunnel/status';
};
export type GetTunnelStatusResponses = {
/**
* Tunnel info
*/
200: TunnelInfo;
};
export type GetTunnelStatusResponse = GetTunnelStatusResponses[keyof GetTunnelStatusResponses];
export type StopTunnelData = {
body?: never;
path?: never;
query?: never;
url: '/tunnel/stop';
};
export type StopTunnelErrors = {
/**
* Internal server error
*/
500: ErrorResponse;
};
export type StopTunnelError = StopTunnelErrors[keyof StopTunnelErrors];
export type StopTunnelResponses = {
/**
* Tunnel stopped successfully
*/
200: unknown;
};
@@ -4,6 +4,7 @@ import { Button } from '../../ui/button';
import { Settings, RefreshCw, ExternalLink } from 'lucide-react';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '../../ui/dialog';
import UpdateSection from './UpdateSection';
import TunnelSection from '../tunnel/TunnelSection';
import { COST_TRACKING_ENABLED, UPDATES_ENABLED } from '../../../updates';
import { getApiUrl } from '../../../config';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
@@ -394,6 +395,8 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
</CardContent>
</Card>
<TunnelSection />
<Card className="rounded-lg">
<CardHeader className="pb-0">
<CardTitle className="mb-1">Help & feedback</CardTitle>
@@ -0,0 +1,335 @@
import { useState, useEffect } from 'react';
import { Button } from '../../ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '../../ui/dialog';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
import { QRCodeSVG } from 'qrcode.react';
import {
Loader2,
Copy,
Check,
ChevronDown,
ChevronUp,
Info,
ExternalLink,
QrCode,
} from 'lucide-react';
import { errorMessage } from '../../../utils/conversionUtils';
import { startTunnel, stopTunnel, getTunnelStatus } from '../../../api/sdk.gen';
import type { TunnelInfo } from '../../../api/types.gen';
const STATUS_MESSAGES = {
idle: 'Tunnel is not running',
starting: 'Starting tunnel...',
running: 'Tunnel is active',
error: 'Tunnel encountered an error',
disabled: 'Tunnel is disabled',
} as const;
const IOS_APP_STORE_URL = 'https://apps.apple.com/us/app/goose-ai/id6752889295';
export default function TunnelSection() {
const [tunnelInfo, setTunnelInfo] = useState<TunnelInfo>({
state: 'idle',
url: '',
hostname: '',
secret: '',
});
const [showQRModal, setShowQRModal] = useState(false);
const [showAppStoreQRModal, setShowAppStoreQRModal] = useState(false);
const [error, setError] = useState<string | null>(null);
const [copiedUrl, setCopiedUrl] = useState(false);
const [copiedSecret, setCopiedSecret] = useState(false);
const [showDetails, setShowDetails] = useState(false);
useEffect(() => {
const loadTunnelInfo = async () => {
try {
const { data } = await getTunnelStatus();
if (data) {
setTunnelInfo(data);
}
} catch (err) {
const errorMsg = errorMessage(err, 'Failed to load tunnel status');
setError(errorMsg);
setTunnelInfo({ state: 'error', url: '', hostname: '', secret: '' });
}
};
loadTunnelInfo();
}, []);
const handleToggleTunnel = async () => {
if (tunnelInfo.state === 'running') {
try {
await stopTunnel();
setTunnelInfo({ state: 'idle', url: '', hostname: '', secret: '' });
setShowQRModal(false);
} catch (err) {
setError(errorMessage(err, 'Failed to stop tunnel'));
try {
const { data } = await getTunnelStatus();
if (data) {
setTunnelInfo(data);
}
} catch (statusErr) {
console.error('Failed to fetch tunnel status after stop error:', statusErr);
}
}
} else {
setError(null);
setTunnelInfo({ state: 'starting', url: '', hostname: '', secret: '' });
try {
const { data } = await startTunnel();
if (data) {
setTunnelInfo(data);
setShowQRModal(true);
}
} catch (err) {
const errorMsg = errorMessage(err, 'Failed to start tunnel');
setError(errorMsg);
setTunnelInfo({ state: 'error', url: '', hostname: '', secret: '' });
}
}
};
const copyToClipboard = async (text: string, type: 'url' | 'secret') => {
try {
await navigator.clipboard.writeText(text);
if (type === 'url') {
setCopiedUrl(true);
setTimeout(() => setCopiedUrl(false), 2000);
} else {
setCopiedSecret(true);
setTimeout(() => setCopiedSecret(false), 2000);
}
} catch (err) {
console.error('Failed to copy to clipboard:', err);
}
};
const getQRCodeData = () => {
if (tunnelInfo.state !== 'running') return '';
const configJson = JSON.stringify({
url: tunnelInfo.url,
secret: tunnelInfo.secret,
});
const urlEncodedConfig = encodeURIComponent(configJson);
return `goosechat://configure?data=${urlEncodedConfig}`;
};
if (tunnelInfo.state === 'disabled') {
return null;
}
return (
<>
<Card className="rounded-lg">
<CardHeader className="pb-0">
<CardTitle className="mb-1">Remote Access</CardTitle>
<CardDescription className="flex flex-col gap-2">
<div className="flex items-start gap-2 p-2 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded">
<Info className="h-4 w-4 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
<div className="text-xs text-blue-800 dark:text-blue-200">
<strong>Preview feature:</strong> Enable remote access to goose from mobile devices
using secure tunneling.{' '}
<a
href={IOS_APP_STORE_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 underline hover:no-underline"
>
Get the iOS app
<ExternalLink className="h-3 w-3" />
</a>
{' or '}
<button
onClick={() => setShowAppStoreQRModal(true)}
className="inline-flex items-center gap-1 underline hover:no-underline"
>
scan QR code
<QrCode className="h-3 w-3" />
</button>
</div>
</div>
</CardDescription>
</CardHeader>
<CardContent className="pt-4 px-4 space-y-4">
{error && (
<div className="p-3 bg-red-100 dark:bg-red-900/20 border border-red-300 dark:border-red-800 rounded text-sm text-red-800 dark:text-red-200">
{error}
</div>
)}
<div className="flex items-center justify-between">
<div>
<h3 className="text-text-default text-xs">Tunnel Status</h3>
<p className="text-xs text-text-muted max-w-md mt-[2px]">
{STATUS_MESSAGES[tunnelInfo.state]}
</p>
</div>
<div className="flex items-center gap-2">
{tunnelInfo.state === 'starting' ? (
<Button disabled variant="secondary" size="sm">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Starting...
</Button>
) : tunnelInfo.state === 'running' ? (
<>
<Button onClick={() => setShowQRModal(true)} variant="default" size="sm">
Show QR Code
</Button>
<Button onClick={handleToggleTunnel} variant="destructive" size="sm">
Stop Tunnel
</Button>
</>
) : (
<Button onClick={handleToggleTunnel} variant="default" size="sm">
{tunnelInfo.state === 'error' ? 'Retry' : 'Start Tunnel'}
</Button>
)}
</div>
</div>
{tunnelInfo.state === 'running' && (
<div className="p-3 bg-green-100 dark:bg-green-900/20 border border-green-300 dark:border-green-800 rounded">
<p className="text-xs text-green-800 dark:text-green-200">
<strong>URL:</strong> {tunnelInfo.url}
</p>
</div>
)}
</CardContent>
</Card>
<Dialog open={showQRModal} onOpenChange={setShowQRModal}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Remote Access Connection</DialogTitle>
</DialogHeader>
{tunnelInfo.state === 'running' && (
<div className="py-4 space-y-4">
<div className="flex justify-center">
<div className="p-4 bg-white rounded-lg">
<QRCodeSVG value={getQRCodeData()} size={200} />
</div>
</div>
<div className="text-center text-sm text-text-muted">
Scan this QR code with the goose mobile app. Do not share this code with anyone else
as it is for your personal access.
</div>
<div className="border-t pt-4">
<button
onClick={() => setShowDetails(!showDetails)}
className="flex items-center justify-between w-full text-sm font-medium hover:opacity-70 transition-opacity"
>
<span>Connection Details</span>
{showDetails ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</button>
{showDetails && (
<div className="mt-3 space-y-3">
<div>
<h3 className="text-xs font-medium mb-1 text-text-muted">Tunnel URL</h3>
<div className="flex items-center gap-2">
<code className="flex-1 p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs break-all overflow-hidden">
{tunnelInfo.url}
</code>
<Button
size="sm"
variant="ghost"
className="flex-shrink-0"
onClick={() => tunnelInfo.url && copyToClipboard(tunnelInfo.url, 'url')}
>
{copiedUrl ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</div>
<div>
<h3 className="text-xs font-medium mb-1 text-text-muted">Secret Key</h3>
<div className="flex items-center gap-2">
<code className="flex-1 p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs break-all overflow-hidden">
{tunnelInfo.secret}
</code>
<Button
size="sm"
variant="ghost"
className="flex-shrink-0"
onClick={() =>
tunnelInfo.secret && copyToClipboard(tunnelInfo.secret, 'secret')
}
>
{copiedSecret ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
</div>
</div>
)}
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setShowQRModal(false)}>
Close
</Button>
<Button variant="destructive" onClick={handleToggleTunnel}>
Stop Tunnel
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={showAppStoreQRModal} onOpenChange={setShowAppStoreQRModal}>
<DialogContent className="sm:max-w-[400px]">
<DialogHeader>
<DialogTitle>Download goose iOS App</DialogTitle>
</DialogHeader>
<div className="py-4 space-y-4">
<div className="flex justify-center">
<div className="p-4 bg-white rounded-lg">
<QRCodeSVG value={IOS_APP_STORE_URL} size={200} />
</div>
</div>
<div className="text-center text-sm text-text-muted">
Scan this QR code with your iPhone camera to install the goose mobile app from the App
Store
</div>
<div className="text-center">
<a
href={IOS_APP_STORE_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-sm text-blue-600 dark:text-blue-400 hover:underline"
>
<ExternalLink className="h-4 w-4" />
Open in App Store
</a>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowAppStoreQRModal(false)}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
-1
View File
@@ -1788,7 +1788,6 @@ async function appMain() {
app.dock?.hide();
}
// Parse command line arguments
const { dirPath } = parseArgs();
await createNewWindow(app, dirPath);