wsb-daemon 0.275.7622

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the machine
<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>