<script lang="ts">
import SettingsSection from "./SettingsSection.svelte";
import { settings } from "../../stores/settings.svelte.js";
import {
getServerUrl,
setServerUrl,
getAuthToken,
setAuthToken,
isRemoteConnection,
} from "../../api/client.js";
let serverUrl: string = $state(getServerUrl());
let tokenInput: string = $state(getAuthToken());
let testing: boolean = $state(false);
let testResult: { ok: boolean; message: string } | null = $state(null);
let saving: boolean = $state(false);
let isRemote: boolean = $derived(isRemoteConnection());
async function handleTestConnection() {
if (!serverUrl.trim()) return;
testing = true;
testResult = null;
try {
const base = serverUrl.replace(/\/+$/, "");
const headers: Record<string, string> = {};
if (tokenInput.trim()) {
headers["Authorization"] = `Bearer ${tokenInput.trim()}`;
}
const res = await fetch(`${base}/api/v1/version`, { headers });
if (res.ok) {
const data = await res.json();
testResult = {
ok: true,
message: `Connected (v${data.version || "unknown"})`,
};
} else {
testResult = { ok: false, message: `Server returned ${res.status}` };
}
} catch (e) {
testResult = {
ok: false,
message: e instanceof Error ? e.message : "Connection failed",
};
} finally {
testing = false;
}
}
function handleConnect() {
if (!serverUrl.trim()) return;
const url = serverUrl.replace(/\/+$/, "");
setServerUrl(url);
setAuthToken(tokenInput.trim());
setTimeout(() => window.location.reload(), 500);
}
function handleDisconnect() {
// Clear the remote token before clearing the URL, so the
// scoped key resolves to the remote server's token.
setAuthToken("");
setServerUrl("");
setTimeout(() => window.location.reload(), 500);
}
</script>
<SettingsSection
title="Remote Access"
description="Where this workbench is served from, and connecting to one served elsewhere."
>
{#if !isRemote}
<!--
Reported, not offered. Which address the workbench is served on is
decided when the daemon starts and cannot be changed from here.
There used to be an "Allow remote connections" toggle in this space. It
persisted a preference nothing read — the server bound loopback either
way — so it promised remote access it never delivered. Had anything
honoured it, a box ticked once would have reopened the port on every
restart afterwards, which is worse.
-->
<div class="subsection">
<div class="server-info">
<span class="field-label">Serving</span>
{#if settings.server.reachable_remotely}
<span class="info-value">
{settings.server.host}:{settings.server.port} — every interface.
Connect using this machine's IP address or hostname.
</span>
{:else}
<code class="info-value"
>http://{settings.server.host}:{settings.server.port}</code
>
{/if}
</div>
{#if settings.server.reachable_remotely}
<div class="security-warning">
Warning: this workbench is reachable from other machines and has no
authentication — anyone who can reach it can read this worktree and
start operations on this host. It is also unencrypted HTTP. Use a
secure tunnel (Tailscale, SSH tunnel, or a reverse proxy with TLS).
</div>
{:else}
<p class="restart-note">
Reachable only from this machine. To serve it wider, restart the
daemon with an address: <code>wsb daemon stop</code> then
<code>wsb serve --listen 0.0.0.0</code>.
</p>
{/if}
</div>
<div class="divider"></div>
{/if}
<div class="subsection">
<span class="subsection-title">
{isRemote ? "Remote Connection" : "Connect to Remote Server"}
</span>
{#if isRemote}
<div class="connected-info">
<span class="field-label">Connected to</span>
<code class="info-value">{getServerUrl()}</code>
</div>
<button class="disconnect-btn" onclick={handleDisconnect}>
Disconnect
</button>
{:else}
<div class="field">
<label class="field-label" for="remote-url">Server URL</label>
<input
id="remote-url"
class="setting-input"
type="url"
placeholder="http://192.168.1.100:8080"
bind:value={serverUrl}
/>
</div>
<div class="field">
<label class="field-label" for="remote-token">Auth Token</label>
<input
id="remote-token"
class="setting-input"
type="password"
placeholder="Paste auth token from server"
bind:value={tokenInput}
/>
</div>
<div class="actions">
<button
class="test-btn"
disabled={testing || !serverUrl.trim()}
onclick={handleTestConnection}
>
{testing ? "Testing..." : "Test Connection"}
</button>
<button
class="connect-btn"
disabled={saving || !serverUrl.trim()}
onclick={handleConnect}
>
Connect
</button>
</div>
{#if testResult}
<p class="msg" class:success={testResult.ok} class:error={!testResult.ok}>
{testResult.message}
</p>
{/if}
{/if}
</div>
</SettingsSection>
<style>
.subsection {
display: flex;
flex-direction: column;
gap: var(--wb-spacing-10);
}
.subsection-title {
font-size: var(--wb-font-size-12);
font-weight: 600;
color: var(--vscode-foreground);
}
.divider {
border-top: 1px solid var(--vscode-widget-border);
margin: var(--wb-spacing-2) 0;
}
.server-info,
.connected-info {
display: flex;
flex-direction: column;
gap: var(--wb-spacing-4);
}
.field-label {
font-size: var(--wb-font-size-11);
font-weight: 500;
color: var(--vscode-disabledForeground);
}
.info-value {
font-size: var(--wb-font-size-12);
font-family: var(--wsb-font-mono, monospace);
color: var(--vscode-editor-foreground);
}
.field {
display: flex;
flex-direction: column;
gap: var(--wb-spacing-4);
}
.setting-input {
height: var(--wb-control-size-30);
padding: 0 var(--wb-spacing-10);
border-radius: var(--wsb-radius-sm);
font-size: var(--wb-font-size-12);
font-family: var(--wsb-font-mono, monospace);
color: var(--vscode-editor-foreground);
background: var(--vscode-input-background);
border: 1px solid var(--vscode-widget-border);
transition: border-color 0.15s;
}
.setting-input:focus {
outline: none;
border-color: var(--vscode-focusBorder);
}
.actions {
display: flex;
gap: var(--wb-spacing-8);
}
.test-btn,
.connect-btn,
.disconnect-btn {
height: var(--wb-control-size-30);
padding: 0 var(--wb-spacing-14);
border-radius: var(--wsb-radius-sm);
font-size: var(--wb-font-size-12);
font-weight: 500;
border: none;
cursor: pointer;
white-space: nowrap;
transition: opacity 0.12s;
}
.test-btn {
color: var(--vscode-editor-foreground);
background: var(--vscode-input-background);
border: 1px solid var(--vscode-widget-border);
}
.connect-btn {
color: white;
background: var(--vscode-focusBorder);
}
.disconnect-btn {
color: white;
background: var(--vscode-errorForeground, #ef4444);
}
.test-btn:hover:not(:disabled),
.connect-btn:hover:not(:disabled),
.disconnect-btn:hover:not(:disabled) {
opacity: 0.9;
}
.test-btn:disabled,
.connect-btn:disabled {
opacity: 0.6;
cursor: default;
}
.msg {
font-size: var(--wb-font-size-11);
margin: 0;
}
.msg.error {
color: var(--vscode-errorForeground, #ef4444);
}
.msg.success {
color: var(--vscode-testing-iconPassed, #22c55e);
}
.restart-note {
font-size: var(--wb-font-size-11);
color: var(--vscode-disabledForeground);
margin: 0;
font-style: italic;
}
.security-warning {
font-size: var(--wb-font-size-11);
color: var(--vscode-editorWarning-foreground, #f59e0b);
background: color-mix(in srgb, var(--vscode-editorWarning-foreground, #f59e0b) 8%, transparent);
border: 1px solid color-mix(in srgb, var(--vscode-editorWarning-foreground, #f59e0b) 25%, transparent);
border-radius: var(--wsb-radius-sm);
padding: var(--wb-spacing-8) var(--wb-spacing-10);
line-height: 1.5;
}
</style>