File size: 4,284 Bytes
fd0a8b6
 
bcfec39
fd0a8b6
 
cae0460
 
fd0a8b6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bcfec39
fd0a8b6
 
 
 
 
 
 
 
 
 
 
 
cae0460
 
 
fd0a8b6
cae0460
fd0a8b6
 
 
 
cae0460
fd0a8b6
 
 
cae0460
 
fd0a8b6
 
 
 
cae0460
 
 
fd0a8b6
cae0460
fd0a8b6
 
 
 
 
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import { useEffect, useState } from 'react';
import * as api from '../api';
import { FolderGlyph } from './icons';

const join = (a: string, b: string) => (a ? `${a}/${b}` : b);
const ROOT = '.';
const isRoot = (p: string) => p === '' || p === ROOT;
// Folder names are typed freely; just strip separators and traversal.
const cleanName = (s: string) => s.replace(/[/\\]/g, '').replace(/\.\./g, '').trim();

// Inline "+ new folder" row shown at the end of every level, so a fresh
// subfolder anywhere in the tree is one click + type away.
function NewFolderRow({ base, depth, onPick }: {
  base: string; depth: number; onPick: (p: string) => void;
}) {
  const [adding, setAdding] = useState(false);
  const [name, setName] = useState('');
  const commit = () => {
    const n = cleanName(name);
    if (n) onPick(join(base, n));
    setAdding(false); setName('');
  };
  if (!adding) {
    return (
      <button className="fp-row fp-new" style={{ paddingLeft: `${depth * 14 + 8}px` }} onClick={() => setAdding(true)}>
        + new folder…
      </button>
    );
  }
  return (
    <div className="fp-row" style={{ paddingLeft: `${depth * 14 + 8}px` }}>
      <input
        autoFocus className="fp-input" placeholder="folder name" value={name}
        onChange={(e) => setName(e.target.value)}
        onBlur={commit}
        onKeyDown={(e) => { if (e.key === 'Enter') commit(); if (e.key === 'Escape') { setAdding(false); setName(''); } }}
      />
    </div>
  );
}

// One level of the workspace folder tree: click a name to pick it as the
// location, click the caret to expand its subfolders.
function Level({ path, depth, value, onPick }: {
  path: string; depth: number; value: string; onPick: (p: string) => void;
}) {
  const [folders, setFolders] = useState<string[] | null>(null);
  const [open, setOpen] = useState<Set<string>>(new Set());
  useEffect(() => {
    let alive = true;
    api.listFolders(path).then((r) => { if (alive) setFolders(r.folders); }).catch(() => { if (alive) setFolders([]); });
    return () => { alive = false; };
  }, [path]);

  if (!folders) return <div className="fp-row fp-msg" style={{ paddingLeft: `${depth * 14 + 8}px` }}></div>;
  return (
    <>
      {folders.map((f) => {
        const p = join(path, f);
        const expanded = open.has(f);
        return (
          <div key={f}>
            <div className={`fp-row${value === p ? ' picked' : ''}`} style={{ paddingLeft: `${depth * 14 + 8}px` }}>
              <button
                className="fp-caret"
                aria-label={expanded ? 'Collapse' : 'Expand'}
                onClick={() => setOpen((s) => { const n = new Set(s); n.has(f) ? n.delete(f) : n.add(f); return n; })}
              >{expanded ? '▾' : '▸'}</button>
              <FolderGlyph className="fp-ico" open={expanded} />
              <button className="fp-name" onClick={() => onPick(p)}>{f}</button>
            </div>
            {expanded && <Level path={p} depth={depth + 1} value={value} onPick={onPick} />}
          </div>
        );
      })}
      <NewFolderRow base={path} depth={depth} onPick={onPick} />
    </>
  );
}

/**
 * Workspace location picker. `value` is a workspace-relative path; '.' is the
 * workspace root. Picking a not-yet-existing folder is fine: the server
 * creates it when the agent is created.
 */
export default function FolderPicker({ value, onChange }: {
  value: string;
  onChange: (p: string) => void;
}) {
  const [open, setOpen] = useState(false);
  const root = isRoot(value);
  return (
    <div className="fp">
      <button className="fp-current" onClick={() => setOpen((o) => !o)} title="Choose where this agent runs">
        <FolderGlyph className="fp-ico" open={open || root} />
        <span className="fp-path">{root ? 'workspaces' : value}</span>
        <span className="fp-toggle">{open ? '▾' : '▸'}</span>
      </button>
      {open && (
        <div className="fp-tree">
          <button className={`fp-row fp-root${root ? ' picked' : ''}`} onClick={() => onChange(ROOT)}>
            <FolderGlyph className="fp-ico" open />
            <span>workspaces</span>
          </button>
          <Level path="" depth={0} value={root ? ROOT : value} onPick={(p) => { onChange(p); }} />
        </div>
      )}
    </div>
  );
}