feat(ui,server): surface active instance service URLs in About settings (#2669)
Show the running instance's local server URL and tunnel URL (when a tunnel is active) as labeled, click-to-open buttons on the About page. /api/system/info now reports the instance port and tunnel URL, resolved lazily from the tunnel runtime so each Git-worktree instance identifies itself in the UI without parsing terminal output. Refs OPE-194
This commit is contained in:
@@ -9,6 +9,7 @@ import { Icon } from "@/components/icon/Icon";
|
||||
import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
import { InstanceServiceUrls } from './InstanceServiceUrls';
|
||||
import {
|
||||
SettingsSection,
|
||||
SETTINGS_BRAND_TITLE_CLASS,
|
||||
@@ -135,6 +136,7 @@ export const AboutSettings: React.FC<AboutSettingsProps> = ({ initialUpdateDialo
|
||||
<p>{t('aboutDialog.openChamberVersionLabel', { version: currentVersion })}</p>
|
||||
<p>{t('aboutDialog.openCodeVersionLabel', { version: openCodeVersion || t('settings.openchamber.about.state.unknown') })}</p>
|
||||
</div>
|
||||
<InstanceServiceUrls />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
@@ -278,6 +280,11 @@ export const AboutSettings: React.FC<AboutSettingsProps> = ({ initialUpdateDialo
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2 border-b border-border/40 px-4 py-3 @xl:flex-row @xl:items-center @xl:justify-between">
|
||||
<span className={SETTINGS_FIELD_LABEL_CLASS}>{t('settings.openchamber.about.field.instanceUrls')}</span>
|
||||
<InstanceServiceUrls />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 px-4 py-4">
|
||||
<a
|
||||
href={GITHUB_URL}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
import { openExternalUrl } from '@/lib/url';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Icon } from '@/components/icon/Icon';
|
||||
|
||||
type InstanceServiceInfo = {
|
||||
port: number | null;
|
||||
tunnelUrl: string | null;
|
||||
};
|
||||
|
||||
type InstanceService = {
|
||||
key: string;
|
||||
label: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows the active instance's service URLs (local server port + tunnel URL,
|
||||
* when a tunnel is active) as labeled buttons that open the URL in the
|
||||
* browser. The data comes from `/api/system/info`, which the server derives
|
||||
* from its own runtime state — this is what makes each Git-worktree instance
|
||||
* distinguishable in the UI without reading terminal output.
|
||||
*
|
||||
* The section stays hidden when the endpoint is unavailable or reports no
|
||||
* port/tunnel (e.g. VS Code runtime), so a failed fetch never renders stale
|
||||
* or wrong URLs.
|
||||
*/
|
||||
export const InstanceServiceUrls: React.FC = () => {
|
||||
const { t } = useI18n();
|
||||
const [info, setInfo] = React.useState<InstanceServiceInfo | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
const controller = typeof AbortController !== 'undefined' ? new AbortController() : undefined;
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
const response = await runtimeFetch('/api/system/info', {
|
||||
signal: controller?.signal,
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!response.ok) return;
|
||||
const data = await response.json().catch(() => null) as { port?: unknown; tunnelUrl?: unknown } | null;
|
||||
if (!data || cancelled) return;
|
||||
const port = typeof data.port === 'number' && Number.isFinite(data.port) && data.port > 0 ? data.port : null;
|
||||
const tunnelUrl = typeof data.tunnelUrl === 'string' && data.tunnelUrl.trim().length > 0
|
||||
? data.tunnelUrl.trim()
|
||||
: null;
|
||||
setInfo({ port, tunnelUrl });
|
||||
} catch {
|
||||
// Best-effort: a failed fetch keeps the section hidden instead of
|
||||
// showing data we cannot verify.
|
||||
}
|
||||
};
|
||||
|
||||
void load();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
controller?.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const services: InstanceService[] = [];
|
||||
if (info?.port !== null && info?.port !== undefined) {
|
||||
services.push({
|
||||
key: 'application',
|
||||
label: t('settings.openchamber.about.field.applicationUrl'),
|
||||
url: `http://localhost:${info.port}/`,
|
||||
});
|
||||
}
|
||||
if (info?.tunnelUrl) {
|
||||
services.push({
|
||||
key: 'tunnel',
|
||||
label: t('settings.openchamber.about.field.tunnelUrl'),
|
||||
url: info.tunnelUrl,
|
||||
});
|
||||
}
|
||||
|
||||
if (services.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{services.map((service) => (
|
||||
<Button
|
||||
key={service.key}
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
title={service.label}
|
||||
className="max-w-full gap-1.5 px-2.5"
|
||||
onClick={() => {
|
||||
void openExternalUrl(service.url);
|
||||
}}
|
||||
>
|
||||
<Icon name="external-link" className="size-3.5 shrink-0" />
|
||||
<span className="max-w-64 truncate font-mono typography-micro">{service.url}</span>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -445,6 +445,9 @@ export const settingsDict = {
|
||||
'settings.openchamber.about.title': 'Über OpenChamber',
|
||||
'settings.openchamber.about.field.version': 'Version',
|
||||
'settings.openchamber.about.field.openCodeVersion': 'OpenCode-Version',
|
||||
'settings.openchamber.about.field.instanceUrls': 'Instanz-URLs',
|
||||
'settings.openchamber.about.field.applicationUrl': 'Anwendung',
|
||||
'settings.openchamber.about.field.tunnelUrl': 'Tunnel',
|
||||
'settings.openchamber.about.state.checking': 'Wird geprüft...',
|
||||
'settings.openchamber.about.state.upToDate': 'Aktuell',
|
||||
'settings.openchamber.about.state.unknown': 'unbekannt',
|
||||
|
||||
@@ -464,6 +464,9 @@ export const settingsDict = {
|
||||
'settings.openchamber.about.title': 'About OpenChamber',
|
||||
'settings.openchamber.about.field.version': 'Version',
|
||||
'settings.openchamber.about.field.openCodeVersion': 'OpenCode version',
|
||||
'settings.openchamber.about.field.instanceUrls': 'Instance URLs',
|
||||
'settings.openchamber.about.field.applicationUrl': 'Application',
|
||||
'settings.openchamber.about.field.tunnelUrl': 'Tunnel',
|
||||
'settings.openchamber.about.state.checking': 'Checking...',
|
||||
'settings.openchamber.about.state.upToDate': 'Up to date',
|
||||
'settings.openchamber.about.state.unknown': 'unknown',
|
||||
|
||||
@@ -431,6 +431,9 @@ export const settingsDict = {
|
||||
"settings.openchamber.about.title": "Acerca de OpenChamber",
|
||||
"settings.openchamber.about.field.version": "Versión",
|
||||
"settings.openchamber.about.field.openCodeVersion": "Versión de OpenCode",
|
||||
"settings.openchamber.about.field.instanceUrls": "URLs de la instancia",
|
||||
"settings.openchamber.about.field.applicationUrl": "Aplicación",
|
||||
"settings.openchamber.about.field.tunnelUrl": "Túnel",
|
||||
"settings.openchamber.about.state.checking": "Comprobando...",
|
||||
"settings.openchamber.about.state.upToDate": "Actualizado",
|
||||
"settings.openchamber.about.state.unknown": "desconocido",
|
||||
|
||||
@@ -2059,6 +2059,9 @@ export const settingsDict = {
|
||||
'settings.remoteInstances.relay.toast.offerFailed': 'Échec de la création du lien d’association',
|
||||
'settings.remoteInstances.relay.toast.linkCopied': 'Lien d’association copié',
|
||||
'settings.openchamber.about.field.openCodeVersion': 'Version d’OpenCode',
|
||||
'settings.openchamber.about.field.instanceUrls': 'URLs de l’instance',
|
||||
'settings.openchamber.about.field.applicationUrl': 'Application',
|
||||
'settings.openchamber.about.field.tunnelUrl': 'Tunnel',
|
||||
'settings.openchamber.about.state.unknown': 'inconnue',
|
||||
'settings.voice.page.field.ttsInputMode': 'Mode d’entrée TTS',
|
||||
'settings.voice.page.field.ttsInputModeSanitized': 'Nettoyé',
|
||||
|
||||
@@ -464,6 +464,9 @@ export const settingsDict = {
|
||||
'settings.openchamber.about.title': 'OpenChamber について',
|
||||
'settings.openchamber.about.field.version': 'バージョン',
|
||||
'settings.openchamber.about.field.openCodeVersion': 'OpenCode バージョン',
|
||||
'settings.openchamber.about.field.instanceUrls': 'インスタンスのURL',
|
||||
'settings.openchamber.about.field.applicationUrl': 'アプリケーション',
|
||||
'settings.openchamber.about.field.tunnelUrl': 'トンネル',
|
||||
'settings.openchamber.about.state.checking': '確認中...',
|
||||
'settings.openchamber.about.state.upToDate': '最新です',
|
||||
'settings.openchamber.about.state.unknown': '不明',
|
||||
|
||||
@@ -431,6 +431,9 @@ export const settingsDict = {
|
||||
'settings.openchamber.about.title': 'OpenChamber 정보',
|
||||
'settings.openchamber.about.field.version': '버전',
|
||||
'settings.openchamber.about.field.openCodeVersion': 'OpenCode 버전',
|
||||
'settings.openchamber.about.field.instanceUrls': '인스턴스 URL',
|
||||
'settings.openchamber.about.field.applicationUrl': '애플리케이션',
|
||||
'settings.openchamber.about.field.tunnelUrl': '터널',
|
||||
'settings.openchamber.about.state.checking': '확인 중...',
|
||||
'settings.openchamber.about.state.upToDate': '최신 상태',
|
||||
'settings.openchamber.about.state.unknown': '알 수 없음',
|
||||
|
||||
@@ -722,6 +722,9 @@ export const settingsDict = {
|
||||
'settings.openchamber.about.actions.updateToVersion': 'Aktualizuj do wersji {version}',
|
||||
'settings.openchamber.about.field.version': 'Wersja',
|
||||
'settings.openchamber.about.field.openCodeVersion': 'Wersja OpenCode',
|
||||
'settings.openchamber.about.field.instanceUrls': 'Adresy URL instancji',
|
||||
'settings.openchamber.about.field.applicationUrl': 'Aplikacja',
|
||||
'settings.openchamber.about.field.tunnelUrl': 'Tunel',
|
||||
'settings.openchamber.about.state.checking': 'Sprawdzanie...',
|
||||
'settings.openchamber.about.state.upToDate': 'Aktualna wersja',
|
||||
'settings.openchamber.about.state.unknown': 'nieznane',
|
||||
|
||||
@@ -431,6 +431,9 @@ export const settingsDict = {
|
||||
"settings.openchamber.about.title": "Sobre o OpenChamber",
|
||||
"settings.openchamber.about.field.version": "Versão",
|
||||
"settings.openchamber.about.field.openCodeVersion": "Versão do OpenCode",
|
||||
"settings.openchamber.about.field.instanceUrls": "URLs da instância",
|
||||
"settings.openchamber.about.field.applicationUrl": "Aplicativo",
|
||||
"settings.openchamber.about.field.tunnelUrl": "Túnel",
|
||||
"settings.openchamber.about.state.checking": "Verificando...",
|
||||
"settings.openchamber.about.state.upToDate": "Atualizado",
|
||||
"settings.openchamber.about.state.unknown": "desconhecido",
|
||||
|
||||
@@ -431,6 +431,9 @@ export const settingsDict = {
|
||||
"settings.openchamber.about.title": "Про OpenChamber",
|
||||
"settings.openchamber.about.field.version": "Версія",
|
||||
"settings.openchamber.about.field.openCodeVersion": "Версія OpenCode",
|
||||
"settings.openchamber.about.field.instanceUrls": "URL-адреси екземпляра",
|
||||
"settings.openchamber.about.field.applicationUrl": "Застосунок",
|
||||
"settings.openchamber.about.field.tunnelUrl": "Тунель",
|
||||
"settings.openchamber.about.state.checking": "Перевірка...",
|
||||
"settings.openchamber.about.state.upToDate": "В актуальному стані",
|
||||
"settings.openchamber.about.state.unknown": "невідомо",
|
||||
|
||||
@@ -431,6 +431,9 @@ export const settingsDict = {
|
||||
'settings.openchamber.about.title': '关于 OpenChamber',
|
||||
'settings.openchamber.about.field.version': '版本',
|
||||
'settings.openchamber.about.field.openCodeVersion': 'OpenCode 版本',
|
||||
'settings.openchamber.about.field.instanceUrls': '实例 URL',
|
||||
'settings.openchamber.about.field.applicationUrl': '应用',
|
||||
'settings.openchamber.about.field.tunnelUrl': '隧道',
|
||||
'settings.openchamber.about.state.checking': '检查中...',
|
||||
'settings.openchamber.about.state.upToDate': '已是最新',
|
||||
'settings.openchamber.about.state.unknown': '未知',
|
||||
|
||||
@@ -428,6 +428,9 @@
|
||||
'settings.openchamber.about.title': '關於 OpenChamber',
|
||||
'settings.openchamber.about.field.version': '版本',
|
||||
'settings.openchamber.about.field.openCodeVersion': 'OpenCode 版本',
|
||||
'settings.openchamber.about.field.instanceUrls': '執行個體 URL',
|
||||
'settings.openchamber.about.field.applicationUrl': '應用程式',
|
||||
'settings.openchamber.about.field.tunnelUrl': '隧道',
|
||||
'settings.openchamber.about.state.checking': '檢查中...',
|
||||
'settings.openchamber.about.state.upToDate': '已是最新',
|
||||
'settings.openchamber.about.state.unknown': '未知',
|
||||
|
||||
Reference in New Issue
Block a user