<script lang="ts">
// The logs directory: where it is, what it holds, and how big it may get.
//
// Every operation started from the workbench is written to a file there, so
// this is the setting that decides how much of the past a page can still
// show. What it manages is stated per stream rather than as one number: an
// operator deciding whether 2 GB is too much needs to know whether it is
// test sessions or their own build logs.
import SettingsSection from "./SettingsSection.svelte";
import { settings } from "../../stores/settings.svelte.js";
import SizeSlider from "./SizeSlider.svelte";
const GB = 1024 * 1024 * 1024;
/**
* The largest cap worth offering.
*
* Not a limit the daemon enforces — it accepts anything at or above its
* minimum — but the top of the slider's travel. Past this the setting stops
* being a cap and becomes "never sweep", which is what turning the sweep off
* is for.
*/
const MAX_BYTES = 256 * GB;
/** What each stream directory is, for somebody who has not read the code. */
const STREAMS: Record<string, string> = {
ops: "operations run from here",
test: "test sessions",
daemon: "the daemon's own log",
};
let localMax: number = $state(settings.logs.max_bytes);
let copied = $state(false);
$effect(() => {
localMax = settings.logs.max_bytes;
});
const dirty = $derived(localMax !== settings.logs.max_bytes);
const usage = $derived(settings.logs.usage);
/** How full it is, against the limit that is actually in force. */
const fraction = $derived(
settings.logs.max_bytes > 0
? Math.min(1, usage.total_bytes / settings.logs.max_bytes)
: 0,
);
/** Lowering the limit below what is held means the next sweep deletes. */
const wouldSweep = $derived(dirty && localMax < usage.managed_bytes);
function human(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
const units = ["KB", "MB", "GB", "TB"];
let value = bytes / 1024;
let unit = 0;
while (value >= 1024 && unit + 1 < units.length) {
value /= 1024;
unit += 1;
}
return `${value >= 100 ? value.toFixed(0) : value.toFixed(1)} ${units[unit]}`;
}
async function copyRoot() {
try {
await navigator.clipboard.writeText(settings.logs.root);
copied = true;
setTimeout(() => (copied = false), 1500);
} catch {
// The path is on screen and selectable; a clipboard the browser
// withheld is not worth an error.
}
}
async function save() {
await settings.save({ logs: { max_bytes: localMax } });
}
</script>
<SettingsSection
title="Logs"
description="Every operation run from the workbench is written to a file here, so its output survives closing the tab, restarting the daemon, and however much the run says."
>
<div class="setting-row column">
<span class="setting-label">Directory</span>
<div class="path-row">
<code class="path">{settings.logs.root}</code>
<button class="path-copy" onclick={() => void copyRoot()}>
{copied ? "copied" : "copy"}
</button>
</div>
</div>
<div class="setting-row column">
<span class="setting-label">
Holding {human(usage.total_bytes)} of {human(settings.logs.max_bytes)}
</span>
<div class="meter" class:meter-full={fraction > 0.9}>
<div class="meter-fill" style="width: {Math.round(fraction * 100)}%"></div>
</div>
<div class="streams">
{#each Object.entries(usage.by_group) as [name, bytes] (name)}
<span class="stream">
<span class="stream-name">{name}</span>
<span class="stream-what">{STREAMS[name] ?? ""}</span>
<span class="stream-bytes">{human(bytes)}</span>
</span>
{/each}
{#if usage.unmanaged_bytes > 0}
<span class="stream">
<span class="stream-name">other</span>
<span class="stream-what">not written here — counted, never removed</span>
<span class="stream-bytes">{human(usage.unmanaged_bytes)}</span>
</span>
{/if}
</div>
</div>
<div class="setting-row">
<span class="setting-label">Maximum size</span>
<SizeSlider
value={localMax}
min={settings.logs.min_bytes}
max={MAX_BYTES}
onchange={(bytes) => (localMax = bytes)}
/>
</div>
<p class="setting-note">
As the limit is reached, whole logs are removed oldest first — a whole
operation, a whole test session — and never one that is being written.
</p>
{#if wouldSweep}
<p class="setting-warn">
{human(usage.managed_bytes)} is held now, so saving this removes the
oldest logs until {human(localMax)} is left.
</p>
{/if}
{#if dirty}
<div class="save-row">
<button class="save-btn" disabled={settings.saving} onclick={save}>
{settings.saving ? "Saving..." : "Save"}
</button>
</div>
{/if}
</SettingsSection>
<style>
.setting-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--wb-spacing-12);
padding: var(--wb-spacing-8) 0;
}
.setting-row.column {
flex-direction: column;
align-items: stretch;
gap: var(--wb-spacing-6);
}
.setting-label {
font-size: var(--wb-font-size-12);
color: var(--vscode-foreground);
}
.path-row {
display: flex;
align-items: center;
gap: var(--wb-spacing-8);
min-width: 0;
}
.path {
font-family: var(--wsb-font-mono, monospace);
font-size: var(--wb-font-size-11);
color: var(--vscode-descriptionForeground, #999);
overflow-x: auto;
white-space: nowrap;
user-select: text;
min-width: 0;
}
.path-copy {
background: none;
border: 1px solid var(--vscode-widget-border);
border-radius: var(--wsb-radius-sm);
color: var(--vscode-descriptionForeground, #999);
cursor: pointer;
font-size: var(--wb-font-size-10);
padding: var(--wb-spacing-1) var(--wb-spacing-6);
flex-shrink: 0;
}
.path-copy:hover {
color: var(--vscode-foreground);
border-color: var(--vscode-focusBorder);
}
.meter {
height: var(--wb-control-size-6);
border-radius: var(--wb-corner-radius-3);
background: var(--vscode-input-background);
border: 1px solid var(--vscode-widget-border);
overflow: hidden;
}
.meter-fill {
height: 100%;
background: var(--vscode-focusBorder);
transition: width 0.2s;
}
.meter-full .meter-fill {
background: var(--vscode-errorForeground, #f14c4c);
}
.streams {
display: flex;
flex-direction: column;
gap: var(--wb-spacing-2);
margin-top: var(--wb-spacing-4);
}
.stream {
display: flex;
align-items: baseline;
gap: var(--wb-spacing-8);
font-size: var(--wb-font-size-11);
color: var(--vscode-descriptionForeground, #999);
}
.stream-name {
font-family: var(--wsb-font-mono, monospace);
min-width: var(--wb-control-size-56);
}
.stream-what {
flex: 1;
opacity: 0.8;
}
.stream-bytes {
font-variant-numeric: tabular-nums;
}
.setting-note {
margin: var(--wb-spacing-4) 0 0;
font-size: var(--wb-font-size-11);
color: var(--vscode-descriptionForeground, #999);
}
.setting-warn {
margin: var(--wb-spacing-4) 0 0;
font-size: var(--wb-font-size-11);
color: var(--vscode-editorWarning-foreground, #cca700);
}
.save-row {
display: flex;
justify-content: flex-end;
}
.save-btn {
height: var(--wb-control-size-28);
padding: 0 var(--wb-spacing-16);
border-radius: var(--wsb-radius-sm);
font-size: var(--wb-font-size-12);
font-weight: 500;
color: white;
background: var(--vscode-focusBorder);
border: none;
cursor: pointer;
}
.save-btn:disabled {
opacity: 0.6;
cursor: default;
}
</style>