goose remote access (#5251)
This commit is contained in:
@@ -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
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1788,7 +1788,6 @@ async function appMain() {
|
||||
app.dock?.hide();
|
||||
}
|
||||
|
||||
// Parse command line arguments
|
||||
const { dirPath } = parseArgs();
|
||||
|
||||
await createNewWindow(app, dirPath);
|
||||
|
||||
Reference in New Issue
Block a user