summaryrefslogtreecommitdiffhomepage
path: root/src/features/cwd-lsp/ui/CwdField.svelte
blob: c83f5cefdf481f07165556759d5853a8ff7b1e36 (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>