collet 0.1.0

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
<script lang="ts">
	import { projectsStore } from '$lib/stores/projects.svelte.js';
	import { appStore } from '$lib/store.svelte.js';

	let { onselect, onclose }: { onselect: (path: string) => void; onclose: () => void } = $props();

	let pathInput = $state(projectsStore.browsePath);

	function handleNavigate(path: string) {
		projectsStore.browse(path, appStore.authToken ?? undefined);
	}

	function handlePathSubmit(e: Event) {
		e.preventDefault();
		const trimmed = pathInput.trim();
		if (trimmed) {
			projectsStore.browse(trimmed, appStore.authToken ?? undefined);
		}
	}

	// Sync pathInput when browsePath changes
	$effect(() => {
		pathInput = projectsStore.browsePath;
	});
</script>

<div class="browser">
	<div class="browser-header">
		<span class="browser-title">Select folder</span>
		<button class="browser-close" onclick={onclose} aria-label="Close browser">&#x2715;</button>
	</div>

	<form class="path-form" onsubmit={handlePathSubmit}>
		<input
			type="text"
			class="path-input"
			bind:value={pathInput}
			placeholder="Enter path..."
			aria-label="Directory path"
		/>
	</form>

	<div class="entries" role="listbox" aria-label="Directory entries">
		{#if projectsStore.browseLoading}
			<div class="empty">Loading...</div>
		{:else}
			{#if projectsStore.browseParent !== null}
				<div
					class="entry"
					onclick={() => handleNavigate(projectsStore.browseParent!)}
					onkeydown={(e) => { if (e.key === 'Enter') handleNavigate(projectsStore.browseParent!); }}
					tabindex="0"
					role="option"
					aria-selected={false}
				>
					<span class="entry-icon">&larr;</span>
					<span class="entry-name">..</span>
				</div>
			{/if}
			{#each projectsStore.browseEntries as entry (entry.path)}
				<div
					class="entry"
					onclick={() => handleNavigate(entry.path)}
					onkeydown={(e) => { if (e.key === 'Enter') handleNavigate(entry.path); }}
					tabindex="0"
					role="option"
					aria-selected={false}
				>
					<span class="entry-icon">&#x1F4C1;</span>
					<span class="entry-name">{entry.name}</span>
				</div>
			{/each}
			{#if projectsStore.browseEntries.length === 0 && projectsStore.browseParent === null}
				<div class="empty">No directories</div>
			{/if}
		{/if}
	</div>

	<div class="browser-footer">
		<button
			class="select-btn"
			onclick={() => onselect(projectsStore.browsePath)}
			disabled={!projectsStore.browsePath}
		>
			Select this folder
		</button>
	</div>
</div>

<style>
	.browser {
		display: flex;
		flex-direction: column;
		border-bottom: 1px solid var(--border);
	}

	.browser-header {
		display: flex;
		align-items: center;
		justify-content: space-between;
		padding: 0.625rem 1rem;
		border-bottom: 1px solid var(--border);
	}

	.browser-title {
		font-size: 0.875rem;
		font-weight: 600;
		color: var(--fg-bright);
	}

	.browser-close {
		all: unset;
		cursor: pointer;
		color: var(--fg-dim);
		padding: 0.125rem 0.25rem;
		font-size: 0.875rem;
	}

	.browser-close:hover {
		color: var(--fg);
	}

	.browser-close:focus-visible {
		outline: 2px solid var(--accent);
		border-radius: 3px;
	}

	.path-form {
		padding: 0.5rem 0.75rem;
	}

	.path-input {
		width: 100%;
		background: var(--bg);
		border: 1px solid var(--border);
		border-radius: 6px;
		color: var(--fg);
		font-family: var(--font-mono);
		font-size: 0.75rem;
		padding: 0.375rem 0.5rem;
		outline: none;
		box-sizing: border-box;
	}

	.path-input:focus {
		border-color: var(--accent);
	}

	.entries {
		max-height: 240px;
		overflow-y: auto;
		padding: 0.25rem 0;
	}

	.empty {
		padding: 1rem;
		color: var(--fg-dim);
		font-size: 0.875rem;
		text-align: center;
	}

	.entry {
		display: flex;
		align-items: center;
		gap: 0.5rem;
		padding: 0.375rem 1rem;
		cursor: pointer;
		font-size: 0.875rem;
		color: var(--fg);
		transition: background 0.1s;
	}

	.entry:hover {
		background: var(--bg-elevated);
	}

	.entry:focus-visible {
		outline: 2px solid var(--accent);
		outline-offset: -2px;
	}

	.entry-icon {
		flex-shrink: 0;
		font-size: 0.875rem;
	}

	.entry-name {
		overflow: hidden;
		text-overflow: ellipsis;
		white-space: nowrap;
	}

	.browser-footer {
		padding: 0.5rem 0.75rem;
	}

	.select-btn {
		width: 100%;
		padding: 0.4375rem;
		background: var(--accent-dim);
		border: 1px solid var(--accent);
		border-radius: 6px;
		color: var(--accent);
		font-family: var(--font-mono);
		font-size: 0.8125rem;
		cursor: pointer;
		transition: background 0.15s;
	}

	.select-btn:hover {
		background: var(--accent);
		color: var(--bg);
	}

	.select-btn:disabled {
		opacity: 0.4;
		cursor: default;
	}

	.select-btn:focus-visible {
		outline: 2px solid var(--accent);
		outline-offset: 2px;
	}
</style>