<script lang="ts">
import { onMount, tick } from "svelte";
import { settings } from "../../stores/settings.svelte.js";
import { auth } from "../../stores/auth.svelte.js";
import { ui } from "../../stores/ui.svelte.js";
import { workbench } from "../../stores/workbench.svelte.js";
import { setAuthToken, getAuthToken, setServerUrl, isRemoteConnection } from "../../api/client.js";
import AppearanceSettings from "./AppearanceSettings.svelte";
import AgentDirSettings from "./AgentDirSettings.svelte";
import TerminalSettings from "./TerminalSettings.svelte";
import RemoteSettings from "./RemoteSettings.svelte";
import RepoSettings from "./RepoSettings.svelte";
import ProjectSettings from "./ProjectSettings.svelte";
import MachineSettings from "./MachineSettings.svelte";
import LogsSettings from "./LogsSettings.svelte";
let authTokenInput: string = $state("");
onMount(() => {
authTokenInput = getAuthToken();
settings.load();
});
/**
* Land on the section the sidebar asked for.
*
* Here rather than in the sidebar because only this page knows when its
* sections exist: the sidebar used to scroll to an element by id, which
* found nothing whenever Settings was not already open — a click that did
* nothing, every time, until the page happened to be open already.
*
* Cleared once it has been honoured, so re-opening Settings later stays
* where the reader left it.
*/
$effect(() => {
const wanted = workbench.settingsSection;
if (!wanted) return;
void tick().then(() => {
document.getElementById(wanted)?.scrollIntoView({ behavior: "smooth", block: "start" });
workbench.settingsSection = null;
});
});
function handleAuthSubmit() {
const token = authTokenInput.trim();
if (!token) return;
setAuthToken(token);
window.location.reload();
}
</script>
<div class="settings-page">
<div class="settings-header">
<h2 class="settings-title">Settings</h2>
</div>
{#if settings.loading}
<div class="skel-settings">
{#each { length: 5 } as _, i}
<div class="skel-section">
<div class="skel" style="width: {80 + ((i * 17) % 60)}px; height: var(--wb-control-size-14); margin-bottom: var(--wb-spacing-12);"></div>
{#each { length: 3 } as _, j}
<div class="skel-field">
<div class="skel" style="width: {60 + ((j * 29) % 80)}px; height: var(--wb-control-size-10);"></div>
<div class="skel" style="width: 100%; height: var(--wb-control-size-32); border-radius: var(--wb-corner-radius-6);"></div>
</div>
{/each}
</div>
{/each}
</div>
{:else if auth.needsToken}
<div class="auth-prompt">
<h3 class="auth-title">Authentication Required</h3>
<p class="auth-description">
This server requires an auth token. Enter the token displayed
on the server's console or settings page.
</p>
<div class="auth-field">
<input
class="auth-input"
type="password"
placeholder="Paste auth token"
bind:value={authTokenInput}
onkeydown={(e) => { if (e.key === "Enter") handleAuthSubmit(); }}
/>
<button
class="auth-btn"
disabled={!authTokenInput.trim()}
onclick={handleAuthSubmit}
>
Authenticate
</button>
</div>
<button
class="auth-disconnect"
onclick={() => {
setAuthToken("");
setServerUrl("");
auth.needsToken = false;
settings.load();
}}
>
Disconnect and reset
</button>
</div>
{:else if settings.error}
<div class="settings-error">
<p>{settings.error}</p>
{#if isRemoteConnection()}
<button
class="auth-disconnect"
onclick={() => {
setAuthToken("");
setServerUrl("");
window.location.reload();
}}
>
Disconnect and reset
</button>
{/if}
</div>
{:else}
<div class="settings-sections">
{#if settings.saveError}
<p class="settings-refused" role="alert">{settings.saveError}</p>
{/if}
<div id="settings-appearance">
<AppearanceSettings />
</div>
<div id="settings-repo">
<RepoSettings />
</div>
<div id="settings-project">
<ProjectSettings />
</div>
<div id="settings-machine">
<MachineSettings />
</div>
<div id="settings-agents">
<AgentDirSettings />
</div>
<div id="settings-terminal">
<TerminalSettings />
</div>
<div id="settings-logs">
<LogsSettings />
</div>
<div id="settings-remote">
<RemoteSettings />
</div>
<div class="settings-actions">
<button
class="resync-btn"
onclick={() => (ui.activeModal = "resync")}
>
Full Resync
</button>
<span class="settings-actions-hint">
Re-scan all session files from scratch
</span>
</div>
</div>
{/if}
</div>
<style>
.settings-page {
max-width: 640px;
margin: 0 auto;
padding: var(--wb-spacing-24) var(--wb-spacing-20) var(--wb-spacing-48);
}
.settings-header {
margin-bottom: var(--wb-spacing-20);
}
.settings-title {
font-size: var(--wb-font-size-18);
font-weight: 600;
color: var(--vscode-editor-foreground);
margin: 0;
}
.settings-sections {
display: flex;
flex-direction: column;
gap: var(--wb-spacing-16);
}
.skel-settings {
display: flex;
flex-direction: column;
gap: var(--wb-spacing-28);
padding: var(--wb-spacing-8) 0;
}
.skel-section {
display: flex;
flex-direction: column;
}
.skel-field {
display: flex;
flex-direction: column;
gap: var(--wb-spacing-6);
margin-bottom: var(--wb-spacing-14);
}
.settings-refused {
color: var(--wb-error);
font-size: var(--wb-font-size-12);
}
.settings-error {
font-size: var(--wb-font-size-13);
color: var(--vscode-disabledForeground);
padding: var(--wb-spacing-40) 0;
text-align: center;
}
.settings-error {
color: var(--vscode-errorForeground, #ef4444);
display: flex;
flex-direction: column;
align-items: center;
gap: var(--wb-spacing-8);
}
.settings-error p {
margin: 0;
}
.settings-actions {
display: flex;
align-items: center;
gap: var(--wb-spacing-12);
padding: var(--wb-spacing-16) 0 0;
border-top: 1px solid var(--vscode-widget-border);
}
.resync-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;
color: var(--vscode-editor-foreground);
background: var(--vscode-input-background);
border: 1px solid var(--vscode-widget-border);
cursor: pointer;
white-space: nowrap;
transition: opacity 0.12s;
}
.resync-btn:hover {
opacity: 0.8;
}
.settings-actions-hint {
font-size: var(--wb-font-size-11);
color: var(--vscode-disabledForeground);
}
.auth-prompt {
text-align: center;
padding: var(--wb-spacing-40) var(--wb-spacing-20);
}
.auth-title {
font-size: var(--wb-font-size-16);
font-weight: 600;
color: var(--vscode-editor-foreground);
margin: 0 0 var(--wb-spacing-8);
}
.auth-description {
font-size: var(--wb-font-size-13);
color: var(--vscode-disabledForeground);
margin: 0 0 var(--wb-spacing-20);
max-width: 400px;
margin-left: auto;
margin-right: auto;
}
.auth-field {
display: flex;
gap: var(--wb-spacing-8);
justify-content: center;
max-width: 400px;
margin: 0 auto;
}
.auth-input {
flex: 1;
height: var(--wb-control-size-34);
padding: 0 var(--wb-spacing-12);
border-radius: var(--wsb-radius-sm);
font-size: var(--wb-font-size-13);
font-family: var(--wsb-font-mono, monospace);
color: var(--vscode-editor-foreground);
background: var(--vscode-input-background);
border: 1px solid var(--vscode-widget-border);
}
.auth-input:focus {
outline: none;
border-color: var(--vscode-focusBorder);
}
.auth-btn {
height: var(--wb-control-size-34);
padding: 0 var(--wb-spacing-16);
border-radius: var(--wsb-radius-sm);
font-size: var(--wb-font-size-13);
font-weight: 500;
color: white;
background: var(--vscode-focusBorder);
border: none;
cursor: pointer;
white-space: nowrap;
}
.auth-btn:disabled {
opacity: 0.6;
cursor: default;
}
.auth-btn:hover:not(:disabled) {
opacity: 0.9;
}
.auth-disconnect {
margin-top: var(--wb-spacing-12);
background: none;
border: none;
color: var(--vscode-disabledForeground);
font-size: var(--wb-font-size-12);
cursor: pointer;
text-decoration: underline;
}
.auth-disconnect:hover {
color: var(--vscode-foreground);
}
</style>