summaryrefslogtreecommitdiffhomepage
path: root/src/features/workspace/ui/CwdField.svelte
blob: bd8b87098498e798dae32d7e404ff35a4a77661b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<script lang="ts">
	import { untrack } from "svelte";
	import { cwdChanged, normalizeCwd, type SaveCwd } from "../logic/view-model";

	let {
		cwd,
		canEdit,
		save,
	}: {
		/** The active conversation's persisted cwd, or null when unset. */
		cwd: string | null;
		/** Whether a real conversation is focused (a draft can't persist a cwd yet). */
		canEdit: boolean;
		save: SaveCwd;
	} = $props();

	// Start empty; the $effect below seeds from the (async-loaded) cwd prop. (Reading
	// the prop directly into initial $state would only capture its first value.)
	let value = $state("");
	let lastSeed = $state("");
	let saving = $state(false);
	let error = $state<string | null>(null);
	let justSaved = $state(false);

	// Seed the input from the persisted cwd (it loads async). Only reseed while the
	// field is untouched, so an in-flight load can't clobber what the user typed.
	// Re-mounted per conversation, so there is no cross-tab bleed.
	$effect(() => {
		const incoming = cwd ?? "";
		untrack(() => {
			if (value === lastSeed) value = incoming;
			lastSeed = incoming;
		});
	});

	const dirty = $derived(cwdChanged(value, cwd));

	async function handleSave() {
		if (saving || !canEdit || !dirty) return;
		saving = true;
		error = null;
		justSaved = false;
		const result = await save(normalizeCwd(value));
		saving = false;
		if (result === null) return;
		if (result.ok) {
			justSaved = true;
		} else {
			error = result.error;
		}
	}

	function onInput() {
		justSaved = false;
		error = null;
	}
</script>

<div class="flex flex-col gap-1">
	<span class="text-xs font-semibold uppercase opacity-60">Working directory</span>
	<div class="flex items-center gap-2">
		<input
			type="text"
			class="input input-bordered input-sm w-full font-mono text-xs"
			placeholder={canEdit ? "/abs/path/to/project" : "Open a conversation first"}
			bind:value
			disabled={!canEdit || saving}
			oninput={onInput}
			onkeydown={(e) => {
				if (e.key === "Enter") handleSave();
			}}
			aria-label="Working directory"
		/>
		<button
			type="button"
			class="btn btn-primary btn-sm"
			disabled={!canEdit || saving || !dirty}
			onclick={handleSave}
		>
			{#if saving}
				<span class="loading loading-spinner loading-xs"></span>
			{:else}
				Set
			{/if}
		</button>
	</div>
	{#if !canEdit}
		<p class="text-xs opacity-60">Start or open a conversation to set its working directory.</p>
	{:else if error}
		<p class="text-xs text-error">{error}</p>
	{:else if justSaved && !dirty}
		<p class="text-xs text-success">Saved.</p>
	{:else}
		<p class="text-xs opacity-50">Defaults each turn's cwd; drives the language servers below.</p>
	{/if}
</div>