@oddessentials/agent-guild 0.28.1 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oddessentials/agent-guild",
3
- "version": "0.28.1",
3
+ "version": "0.29.0",
4
4
  "description": "Launch and watch AI coding-assistant terminal sessions from one local web page. A bundled session manager owns the terminals so the page can close and reconnect.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -16,6 +16,20 @@ export function folderSegments(dir, paths = path) {
16
16
  return segments;
17
17
  }
18
18
 
19
+ const WINDOWS_RESERVED = /^(con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(\..*)?$/i;
20
+
21
+ export function folderNameProblem(name, platform = process.platform) {
22
+ if (!name) return 'Enter a folder name.';
23
+ if (name === '.' || name === '..') return 'Choose a different folder name.';
24
+ if (/[/\\]/.test(name)) return 'A folder name cannot contain / or \\.';
25
+ if (/[\u0000-\u001f]/.test(name)) return 'A folder name cannot contain control characters.';
26
+ if (platform !== 'win32') return null;
27
+ if (/[<>:"|?*]/.test(name)) return 'A folder name cannot contain < > : " | ? or *.';
28
+ if (/[. ]$/.test(name)) return 'A folder name cannot end with a dot or a space.';
29
+ if (WINDOWS_RESERVED.test(name)) return `Windows reserves the name ${name}.`;
30
+ return null;
31
+ }
32
+
19
33
  export function createFolderBrowser({ platform = process.platform, home = os.homedir(), limit = 2000, driveTimeoutMs = 500, fsp = fs.promises } = {}) {
20
34
  const paths = platform === 'win32' ? path.win32 : path.posix;
21
35
 
@@ -59,7 +73,30 @@ export function createFolderBrowser({ platform = process.platform, home = os.hom
59
73
  return folders.filter(Boolean).sort(byName);
60
74
  }
61
75
 
62
- return {
76
+ const browser = {
77
+ async create(rawParent, rawName) {
78
+ if (typeof rawParent !== 'string' || !rawParent.trim() || rawParent.includes('\0')) throw failure(400, 'bad_path', 'The folder must be a path.');
79
+ const name = typeof rawName === 'string' ? rawName.trim() : '';
80
+ const problem = folderNameProblem(name, platform);
81
+ if (problem) throw failure(400, 'bad_name', problem);
82
+ const parent = paths.resolve(rawParent.trim());
83
+ const missing = () => failure(409, 'folder_missing', `${parent} no longer exists.`);
84
+ try {
85
+ if (!(await fsp.stat(parent)).isDirectory()) throw missing();
86
+ } catch (error) {
87
+ if (error.status) throw error;
88
+ throw error.code === 'ENOENT' || error.code === 'ENOTDIR' ? missing() : failure(409, 'folder_unreadable', `Agent Guild cannot read ${parent}.`);
89
+ }
90
+ const dir = paths.join(parent, name);
91
+ try {
92
+ await fsp.mkdir(dir);
93
+ } catch (error) {
94
+ if (error.code === 'EEXIST') throw failure(409, 'folder_exists', `${name} already exists in ${parent}.`);
95
+ if (error.code === 'ENOENT') throw missing();
96
+ throw failure(409, 'folder_unwritable', `Agent Guild cannot create ${dir}.`);
97
+ }
98
+ return browser.list(dir);
99
+ },
63
100
  async list(raw) {
64
101
  if (raw !== undefined && raw !== null && (typeof raw !== 'string' || raw.includes('\0'))) {
65
102
  throw failure(400, 'bad_path', 'The folder must be a path.');
@@ -82,4 +119,5 @@ export function createFolderBrowser({ platform = process.platform, home = os.hom
82
119
  };
83
120
  },
84
121
  };
122
+ return browser;
85
123
  }
@@ -287,6 +287,10 @@ export function createManagerServer({
287
287
  if (route === '/folders' && method === 'GET') {
288
288
  return sendJson(res, 200, await folderBrowser.list(url.searchParams.get('path') ?? undefined));
289
289
  }
290
+ if (route === '/folders' && method === 'POST') {
291
+ const { path: parent, name } = await readJsonBody(req);
292
+ return sendJson(res, 201, await folderBrowser.create(parent, name));
293
+ }
290
294
  if (route === '/upgrade' && method === 'POST') {
291
295
  const session = await manager.upgrade();
292
296
  return sendJson(res, 201, { session: session.toJSON() });
package/web/app.js CHANGED
@@ -23,6 +23,7 @@ const GITHUB_ACCOUNT_KEY = 'agentGuild.githubAccount';
23
23
  const CLONE_PARENT_KEY = 'agentGuild.cloneParent';
24
24
  const HIDDEN_FOLDERS_KEY = 'agentGuild.showHiddenFolders';
25
25
  const RECENT_CWDS_KEY = 'agentGuild.recentCwds';
26
+ const RECENT_CLONE_PARENTS_KEY = 'agentGuild.recentCloneParents';
26
27
  const GITHUB_REPO_KEY = 'agentGuild.githubRepo';
27
28
  const GITHUB_RECENT_KEY = 'agentGuild.githubRecent';
28
29
  const GITHUB_VIEW_KEY = 'agentGuild.githubView';
@@ -767,68 +768,78 @@ async function openWorkingFolder() {
767
768
  }
768
769
  }
769
770
 
770
- // ---- recent working folders -----------------------------------------------
771
+ // ---- recent folders ---------------------------------------------------------
771
772
 
772
- const recentView = { open: false, typed: false, active: -1 };
773
+ const RECENT_FIELDS = {
774
+ cwd: { input: 'cwd', list: 'cwd-recent', key: RECENT_CWDS_KEY, pick: (dir) => useFolder(dir) },
775
+ clone: { input: 'github-parent', list: 'github-parent-recent', key: RECENT_CLONE_PARENTS_KEY, pick: (dir) => setCloneParent(dir) },
776
+ };
777
+ const recentView = { field: null, typed: false, active: -1 };
773
778
 
774
- function recentCwds() {
775
- return readRecentFolders(load(RECENT_CWDS_KEY));
779
+ function recentFolders(field) {
780
+ return readRecentFolders(load(RECENT_FIELDS[field].key));
776
781
  }
777
782
 
778
- function rememberCwd(dir) {
779
- if (dir) save(RECENT_CWDS_KEY, JSON.stringify(rememberFolder(recentCwds(), dir, { caseless: state.platform === 'win32' })));
783
+ function rememberRecent(field, dir) {
784
+ if (dir) save(RECENT_FIELDS[field].key, JSON.stringify(rememberFolder(recentFolders(field), dir, { caseless: state.platform === 'win32' })));
780
785
  }
781
786
 
782
- function renderRecentCwds() {
783
- const input = $('cwd'), list = $('cwd-recent');
784
- const shown = recentView.open ? matchFolders(recentCwds(), recentView.typed ? input.value : '') : [];
785
- recentView.active = Math.min(recentView.active, shown.length - 1);
787
+ function renderRecent(field) {
788
+ const { input: inputId, list: listId } = RECENT_FIELDS[field];
789
+ const input = $(inputId), list = $(listId);
790
+ const open = recentView.field === field;
791
+ const shown = open ? matchFolders(recentFolders(field), recentView.typed ? input.value : '') : [];
792
+ if (open) recentView.active = Math.min(recentView.active, shown.length - 1);
786
793
  list.replaceChildren(...shown.map((dir, i) => {
787
794
  const option = el('li', 'cwd-recent-option', dir);
788
- option.id = `cwd-recent-${i}`;
795
+ option.id = `${listId}-${i}`;
789
796
  option.setAttribute('role', 'option');
790
- option.setAttribute('aria-selected', String(i === recentView.active));
797
+ option.setAttribute('aria-selected', String(open && i === recentView.active));
791
798
  option.addEventListener('pointerdown', (e) => e.preventDefault());
792
- option.addEventListener('click', () => pickRecentCwd(dir));
799
+ option.addEventListener('click', () => pickRecent(field, dir));
793
800
  return option;
794
801
  }));
795
802
  list.hidden = !shown.length;
796
803
  input.setAttribute('aria-expanded', String(!list.hidden));
797
- if (recentView.active >= 0) input.setAttribute('aria-activedescendant', `cwd-recent-${recentView.active}`);
804
+ if (open && recentView.active >= 0) input.setAttribute('aria-activedescendant', `${listId}-${recentView.active}`);
798
805
  else input.removeAttribute('aria-activedescendant');
799
806
  }
800
807
 
801
- function showRecentCwds(typed = false) {
802
- Object.assign(recentView, { open: true, typed, active: -1 });
803
- renderRecentCwds();
808
+ function showRecent(field, typed = false) {
809
+ const previous = recentView.field;
810
+ Object.assign(recentView, { field, typed, active: -1 });
811
+ if (previous && previous !== field) renderRecent(previous);
812
+ renderRecent(field);
804
813
  }
805
814
 
806
- function hideRecentCwds() {
807
- Object.assign(recentView, { open: false, active: -1 });
808
- renderRecentCwds();
815
+ function hideRecent(field) {
816
+ if (recentView.field !== field) return;
817
+ Object.assign(recentView, { field: null, active: -1 });
818
+ renderRecent(field);
809
819
  }
810
820
 
811
- function pickRecentCwd(dir) {
812
- hideRecentCwds();
813
- useFolder(dir);
821
+ function pickRecent(field, dir) {
822
+ hideRecent(field);
823
+ RECENT_FIELDS[field].pick(dir);
814
824
  }
815
825
 
816
- function recentCwdKeys(e) {
817
- const options = $('cwd-recent').hidden ? [] : [...$('cwd-recent').children];
826
+ function recentKeys(field, e) {
827
+ const list = $(RECENT_FIELDS[field].list);
828
+ const options = list.hidden ? [] : [...list.children];
818
829
  if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
819
830
  e.preventDefault();
820
- if (!recentView.open) showRecentCwds();
821
- const count = $('cwd-recent').children.length;
831
+ if (recentView.field !== field) showRecent(field);
832
+ const count = list.children.length;
822
833
  if (!count) return;
823
834
  recentView.active = e.key === 'ArrowDown' ? (recentView.active + 1) % count : recentView.active <= 0 ? count - 1 : recentView.active - 1;
824
- renderRecentCwds();
825
- $(`cwd-recent-${recentView.active}`).scrollIntoView?.({ block: 'nearest' });
835
+ renderRecent(field);
836
+ $(`${RECENT_FIELDS[field].list}-${recentView.active}`).scrollIntoView?.({ block: 'nearest' });
826
837
  } else if (e.key === 'Enter' && recentView.active >= 0 && options[recentView.active]) {
827
838
  e.preventDefault();
828
- pickRecentCwd(options[recentView.active].textContent);
839
+ pickRecent(field, options[recentView.active].textContent);
829
840
  } else if (e.key === 'Escape' && options.length) {
830
841
  e.preventDefault();
831
- hideRecentCwds();
842
+ hideRecent(field);
832
843
  }
833
844
  }
834
845
 
@@ -854,7 +865,52 @@ function chooseFolder(field) {
854
865
  browseTo($(FOLDER_FIELDS[field].input).value.trim());
855
866
  }
856
867
 
868
+ function showNewFolder(open) {
869
+ $('folder-new').hidden = !open;
870
+ $('folder-new-open').setAttribute('aria-expanded', String(open));
871
+ if (open) $('folder-new-name').value = '';
872
+ renderFolderBrowser();
873
+ (open ? $('folder-new-name') : $('folder-new-open')).focus();
874
+ }
875
+
876
+ async function createFolder(e) {
877
+ e?.preventDefault();
878
+ const parent = folderView.listing?.path;
879
+ const name = $('folder-new-name').value.trim();
880
+ if (!parent || !name || folderView.loading) return;
881
+ const request = ++folderView.request;
882
+ folderView.loading = true;
883
+ folderView.error = null;
884
+ renderFolderBrowser();
885
+ try {
886
+ const listing = await api('POST', '/folders', { path: parent, name });
887
+ if (request !== folderView.request) return;
888
+ folderView.listing = listing;
889
+ $('folder-filter').value = '';
890
+ $('folder-new').hidden = true;
891
+ $('folder-new-open').setAttribute('aria-expanded', 'false');
892
+ } catch (err) {
893
+ if (request !== folderView.request) return;
894
+ if (err instanceof AuthError) {
895
+ $('folder-browser').close();
896
+ return showAuth(err.message);
897
+ }
898
+ folderView.error = err.message || 'Could not create the folder.';
899
+ }
900
+ folderView.loading = false;
901
+ renderFolderBrowser();
902
+ if ($('folder-browser').open) ($('folder-new').hidden ? $('folder-use') : $('folder-new-name')).focus();
903
+ }
904
+
905
+ function newFolderKeys(e) {
906
+ if (e.key !== 'Escape') return;
907
+ e.preventDefault();
908
+ showNewFolder(false);
909
+ }
910
+
857
911
  async function browseTo(dir) {
912
+ $('folder-new').hidden = true;
913
+ $('folder-new-open').setAttribute('aria-expanded', 'false');
858
914
  const request = ++folderView.request;
859
915
  folderView.loading = true;
860
916
  folderView.error = null;
@@ -908,6 +964,8 @@ function renderFolderBrowser() {
908
964
  status.classList.toggle('error', Boolean(error));
909
965
  $('folder-current').textContent = listing?.path ?? '';
910
966
  $('folder-use').disabled = loading || !listing;
967
+ $('folder-new-open').disabled = loading || !listing;
968
+ $('folder-new-create').disabled = loading || !listing || !$('folder-new-name').value.trim();
911
969
  }
912
970
 
913
971
  function folderBrowserClosed() {
@@ -925,11 +983,9 @@ function useBrowsedFolder() {
925
983
  if (!dir || folderView.loading) return;
926
984
  const field = folderView.field;
927
985
  $('folder-browser').close();
986
+ rememberRecent(field, dir);
928
987
  if (field === 'clone') setCloneParent(dir);
929
- else {
930
- rememberCwd(dir);
931
- useFolder(dir);
932
- }
988
+ else useFolder(dir);
933
989
  }
934
990
 
935
991
  function moveInFolders(e) {
@@ -2418,7 +2474,7 @@ async function startSession(provider, card, { resume, cwd, account = selectedAcc
2418
2474
  ({ session } = await api('POST', '/sessions', { ...body, cwd: working || undefined }));
2419
2475
  }
2420
2476
  upsertSession(session);
2421
- rememberCwd(session.cwd);
2477
+ rememberRecent('cwd', session.cwd);
2422
2478
  closeHistory({ focusOpener: false });
2423
2479
  openPanel(session.id);
2424
2480
  } catch (err) {
@@ -2628,7 +2684,7 @@ const GITHUB_SCOPES = {
2628
2684
  repo: 'Read and write access to your repositories, private ones included, so you can browse repositories and create or edit issues.',
2629
2685
  'write:public_key': 'Add the SSH key Agent Guild creates for this account.',
2630
2686
  };
2631
- const githubView = { accountId: null, repos: null, reposFor: null, loading: null, error: null, parentError: null, card: null, started: new Set(), announced: new Set() };
2687
+ const githubView = { accountId: null, repos: null, reposFor: null, loading: null, error: null, parentError: null, card: null, started: new Map(), announced: new Set() };
2632
2688
  let githubLoading = null;
2633
2689
 
2634
2690
  function el(tag, className, ...children) {
@@ -3093,8 +3149,9 @@ function renderGitHubRepos(github, account, repos) {
3093
3149
  }
3094
3150
 
3095
3151
  async function startClone(account, fullName) {
3096
- const { session } = await api('POST', '/github/clone', { account: account.id, repo: fullName, parent: cloneParent() });
3097
- githubView.started.add(session.id);
3152
+ const parent = cloneParent();
3153
+ const { session } = await api('POST', '/github/clone', { account: account.id, repo: fullName, parent });
3154
+ githubView.started.set(session.id, parent);
3098
3155
  upsertSession(session);
3099
3156
  openPanel(session.id);
3100
3157
  }
@@ -3204,8 +3261,10 @@ function noticeClone(s) {
3204
3261
  githubPick.origins.clear();
3205
3262
  if (dockShows('github')) loadRepos();
3206
3263
  if (!githubView.started.has(s.id)) return;
3207
- if (clonedPath(s)) toast(`Cloned ${s.clone.repo} into ${s.clone.path}.`, 12000, { label: 'Use folder', run: () => useFolder(s.clone.path) });
3208
- else toast(`Cloning ${s.clone?.repo ?? 'the repository'} did not finish. Its session shows why.`, 8000);
3264
+ if (clonedPath(s)) {
3265
+ rememberRecent('clone', githubView.started.get(s.id));
3266
+ toast(`Cloned ${s.clone.repo} into ${s.clone.path}.`, 12000, { label: 'Use folder', run: () => useFolder(s.clone.path) });
3267
+ } else toast(`Cloning ${s.clone?.repo ?? 'the repository'} did not finish. Its session shows why.`, 8000);
3209
3268
  }
3210
3269
 
3211
3270
  function useFolder(dir) {
@@ -5587,11 +5646,13 @@ $('panel-stop').addEventListener('click', () => {
5587
5646
  });
5588
5647
  $('cwd').value = load(CWD_KEY) || '';
5589
5648
  $('cwd-pick').addEventListener('click', firstClick(() => chooseFolder('cwd')));
5590
- $('cwd').addEventListener('focus', () => showRecentCwds());
5591
- $('cwd').addEventListener('click', () => { if (!recentView.open) showRecentCwds(); });
5592
- $('cwd').addEventListener('input', () => showRecentCwds(true));
5593
- $('cwd').addEventListener('blur', hideRecentCwds);
5594
- $('cwd').addEventListener('keydown', recentCwdKeys);
5649
+ for (const [field, { input }] of Object.entries(RECENT_FIELDS)) {
5650
+ $(input).addEventListener('focus', () => showRecent(field));
5651
+ $(input).addEventListener('click', () => { if (recentView.field !== field) showRecent(field); });
5652
+ $(input).addEventListener('input', () => showRecent(field, true));
5653
+ $(input).addEventListener('blur', () => hideRecent(field));
5654
+ $(input).addEventListener('keydown', (e) => recentKeys(field, e));
5655
+ }
5595
5656
  $('folder-cancel').addEventListener('click', () => $('folder-browser').close());
5596
5657
  $('folder-browser').addEventListener('click', (e) => { if (e.target === $('folder-browser')) $('folder-browser').close(); });
5597
5658
  $('folder-browser').addEventListener('close', folderBrowserClosed);
@@ -5605,6 +5666,11 @@ $('folder-hidden').addEventListener('change', (e) => {
5605
5666
  renderFolderBrowser();
5606
5667
  });
5607
5668
  $('folder-use').addEventListener('click', useBrowsedFolder);
5669
+ $('folder-new-open').addEventListener('click', () => showNewFolder($('folder-new').hidden));
5670
+ $('folder-new-cancel').addEventListener('click', () => showNewFolder(false));
5671
+ $('folder-new').addEventListener('submit', createFolder);
5672
+ $('folder-new-name').addEventListener('input', renderFolderBrowser);
5673
+ $('folder-new-name').addEventListener('keydown', newFolderKeys);
5608
5674
  $('cwd-open').addEventListener('click', firstClick(openWorkingFolder));
5609
5675
  try { state.accounts = JSON.parse(load(ACCOUNTS_KEY)) || {}; } catch { state.accounts = {}; }
5610
5676
  try { state.shellPicks = JSON.parse(load(SHELLS_KEY)) || {}; } catch { state.shellPicks = {}; }
package/web/index.html CHANGED
@@ -244,7 +244,8 @@
244
244
  <div id="github-clone-into" class="history-form github-form">
245
245
  <label for="github-parent">Clone into</label>
246
246
  <div class="cwd-field single">
247
- <input id="github-parent" type="text" placeholder="Folder that holds your clones" spellcheck="false" autocomplete="off">
247
+ <input id="github-parent" type="text" placeholder="Folder that holds your clones" spellcheck="false" autocomplete="off" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="github-parent-recent">
248
+ <ul id="github-parent-recent" class="cwd-recent above" role="listbox" aria-label="Recent clone folders" hidden></ul>
248
249
  <div class="cwd-tools">
249
250
  <button id="github-parent-pick" class="cwd-tool" type="button" aria-label="Choose a folder…" title="Connect to the session manager to choose folders" disabled>
250
251
  <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
@@ -434,7 +435,13 @@
434
435
  <button id="folder-up" class="btn" type="button">Up</button>
435
436
  <button id="folder-home" class="btn folder-chip" type="button">Home</button>
436
437
  <div id="folder-roots" class="folder-roots"></div>
438
+ <button id="folder-new-open" class="btn folder-new-open" type="button" aria-expanded="false" aria-controls="folder-new">New folder</button>
437
439
  </div>
440
+ <form id="folder-new" class="history-tools folder-new" hidden>
441
+ <input id="folder-new-name" type="text" placeholder="New folder name" aria-label="New folder name" spellcheck="false" autocomplete="off" autocapitalize="off" enterkeyhint="done">
442
+ <button id="folder-new-create" class="btn primary" type="submit">Create</button>
443
+ <button id="folder-new-cancel" class="btn" type="button">Cancel</button>
444
+ </form>
438
445
  <div class="history-tools">
439
446
  <input id="folder-filter" type="search" placeholder="Filter folders" aria-label="Filter folders" spellcheck="false" autocomplete="off">
440
447
  <label class="history-here"><input id="folder-hidden" type="checkbox"><span>Show hidden</span></label>
@@ -243,7 +243,7 @@
243
243
  }
244
244
  .btn.primary:hover { filter: brightness(1.08); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), inset 0 0 0 1px rgba(233, 196, 122, 0.6), 0 5px 16px rgba(40, 70, 30, 0.35); }
245
245
  .btn.danger:hover { border-color: var(--danger); box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 14%, transparent); }
246
- .cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select, .notes-text { background: var(--panel-strong); }
246
+ .cwd input, .auth input, .history-tools input[type="search"], .folder-new input, .history-form input, .github-fields input, .github-fields select, .notes-text { background: var(--panel-strong); }
247
247
 
248
248
  .models, .news-panel, .dock, .auth, .news-latest, .menu:popover-open { background: var(--panel-strong); backdrop-filter: blur(16px); }
249
249
  .models, .news-panel { box-shadow: var(--dialog-shadow); }
@@ -362,7 +362,7 @@
362
362
  .btn.primary { border-color: #6a8f3a; background: linear-gradient(#55924a, #35662f); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), inset 0 0 0 1px rgba(233, 196, 122, 0.3), 0 0 16px rgba(110, 170, 80, 0.3); }
363
363
  .btn.danger { color: #f59a8c; }
364
364
  .btn.danger:hover { border-color: #b8402f; box-shadow: 0 0 12px rgba(240, 123, 106, 0.3); }
365
- .cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select, .notes-text { border-color: #4a3824; background: rgba(10, 7, 4, 0.7); }
365
+ .cwd input, .auth input, .history-tools input[type="search"], .folder-new input, .history-form input, .github-fields input, .github-fields select, .notes-text { border-color: #4a3824; background: rgba(10, 7, 4, 0.7); }
366
366
 
367
367
  .models::backdrop, .news-panel::backdrop { background: rgba(6, 4, 2, 0.66); }
368
368
  .terminal-panel { border-color: #5a4226; box-shadow: 0 0 0 1px rgba(233, 196, 122, 0.06), 0 30px 80px rgba(0, 0, 0, 0.7); }
@@ -249,7 +249,7 @@
249
249
  }
250
250
  .btn.primary:hover { filter: brightness(1.08); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), inset 0 0 0 1px rgba(227, 166, 119, 0.6), 0 5px 16px rgba(20, 70, 56, 0.35); }
251
251
  .btn.danger:hover { border-color: var(--danger); box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 14%, transparent); }
252
- .cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select, .notes-text { background: var(--panel-strong); }
252
+ .cwd input, .auth input, .history-tools input[type="search"], .folder-new input, .history-form input, .github-fields input, .github-fields select, .notes-text { background: var(--panel-strong); }
253
253
 
254
254
  .models, .news-panel, .dock, .auth, .news-latest, .menu:popover-open { background: var(--panel-strong); backdrop-filter: blur(16px); }
255
255
  .models, .news-panel { box-shadow: var(--dialog-shadow); }
@@ -369,7 +369,7 @@
369
369
  .btn.primary { border-color: #3f9a80; background: linear-gradient(#2f8a73, #1b5a4b); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), inset 0 0 0 1px rgba(227, 166, 119, 0.3), 0 0 16px rgba(60, 170, 140, 0.3); }
370
370
  .btn.danger { color: #f59a8c; }
371
371
  .btn.danger:hover { border-color: #b8402f; box-shadow: 0 0 12px rgba(240, 123, 106, 0.3); }
372
- .cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select, .notes-text { border-color: #3a4a3f; background: rgba(5, 8, 6, 0.7); }
372
+ .cwd input, .auth input, .history-tools input[type="search"], .folder-new input, .history-form input, .github-fields input, .github-fields select, .notes-text { border-color: #3a4a3f; background: rgba(5, 8, 6, 0.7); }
373
373
 
374
374
  .models::backdrop, .news-panel::backdrop { background: rgba(2, 5, 3, 0.66); }
375
375
  .terminal-panel { border-color: #5a3a22; box-shadow: 0 0 0 1px rgba(227, 166, 119, 0.06), 0 30px 80px rgba(0, 0, 0, 0.7); }
@@ -244,7 +244,7 @@
244
244
  }
245
245
  .btn.primary:hover { filter: brightness(1.07); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22), 0 6px 20px rgba(45, 107, 69, 0.36); }
246
246
  .btn.danger:hover { border-color: var(--danger); box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 14%, transparent); }
247
- .cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select, .notes-text { background: var(--paper-strong); }
247
+ .cwd input, .auth input, .history-tools input[type="search"], .folder-new input, .history-form input, .github-fields input, .github-fields select, .notes-text { background: var(--paper-strong); }
248
248
 
249
249
  .models, .news-panel, .dock, .auth, .news-latest, .menu:popover-open { background: var(--paper-strong); backdrop-filter: blur(16px); }
250
250
  .models, .news-panel { box-shadow: var(--dialog-shadow); }
@@ -371,7 +371,7 @@
371
371
  .btn.primary { background: linear-gradient(135deg, #4a8c5e, #2a6340); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 18px rgba(80, 160, 105, 0.35); }
372
372
  .btn.danger { color: #ff9a8e; }
373
373
  .btn.danger:hover { border-color: #b8443a; box-shadow: 0 0 12px rgba(240, 122, 108, 0.3); }
374
- .cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select, .notes-text { border-color: #2e4a3d; background: rgba(4, 10, 8, 0.7); }
374
+ .cwd input, .auth input, .history-tools input[type="search"], .folder-new input, .history-form input, .github-fields input, .github-fields select, .notes-text { border-color: #2e4a3d; background: rgba(4, 10, 8, 0.7); }
375
375
 
376
376
  .models::backdrop, .news-panel::backdrop { background: rgba(2, 6, 5, 0.66); }
377
377
  .terminal-panel { border-color: #2e4a3d; box-shadow: 0 0 0 1px rgba(140, 210, 160, 0.06), 0 30px 80px rgba(0, 0, 0, 0.7); }
@@ -480,7 +480,7 @@
480
480
  }
481
481
  .btn.primary:hover { border-color: #b8892f; filter: brightness(1.08); }
482
482
  .btn.danger:hover { border-color: #c4625d; }
483
- .cwd input, .auth input, .history-tools input[type="search"], .history-form input, .notes-text {
483
+ .cwd input, .auth input, .history-tools input[type="search"], .folder-new input, .history-form input, .notes-text {
484
484
  border-color: #dccba5; background: #fffdf8; box-shadow: inset 0 1px 2px rgba(70, 48, 12, 0.06);
485
485
  }
486
486
  .auth { border-color: #e4d6b6; box-shadow: var(--card-shadow); }
@@ -246,7 +246,7 @@
246
246
  }
247
247
  .btn.primary:hover { filter: brightness(1.08); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 6px 20px rgba(122, 79, 232, 0.4); }
248
248
  .btn.danger:hover { border-color: var(--danger); box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 14%, transparent); }
249
- .cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select, .notes-text { background: var(--glass-strong); }
249
+ .cwd input, .auth input, .history-tools input[type="search"], .folder-new input, .history-form input, .github-fields input, .github-fields select, .notes-text { background: var(--glass-strong); }
250
250
 
251
251
  .models, .news-panel, .dock, .auth, .news-latest, .menu:popover-open { background: var(--glass-strong); backdrop-filter: blur(16px); }
252
252
  .models, .news-panel { box-shadow: var(--dialog-shadow); }
@@ -367,7 +367,7 @@
367
367
  .btn.primary { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22), 0 0 18px rgba(106, 92, 240, 0.45); }
368
368
  .btn.danger { color: #ff8c9c; }
369
369
  .btn.danger:hover { border-color: #c23a52; box-shadow: 0 0 12px rgba(255, 107, 129, 0.3); }
370
- .cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select, .notes-text { border-color: #26355c; background: rgba(4, 7, 16, 0.7); }
370
+ .cwd input, .auth input, .history-tools input[type="search"], .folder-new input, .history-form input, .github-fields input, .github-fields select, .notes-text { border-color: #26355c; background: rgba(4, 7, 16, 0.7); }
371
371
 
372
372
  .models::backdrop, .news-panel::backdrop { background: rgba(2, 4, 10, 0.66); }
373
373
  .terminal-panel { border-color: #2a3a66; box-shadow: 0 0 0 1px rgba(120, 170, 255, 0.06), 0 30px 80px rgba(0, 0, 0, 0.7); }
package/web/styles.css CHANGED
@@ -749,7 +749,7 @@ a.news-link:visited { color: var(--muted); }
749
749
  }
750
750
 
751
751
  .history-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 10px 14px 0; }
752
- .history-tools input[type="search"] {
752
+ .history-tools input[type="search"], .folder-new input {
753
753
  flex: 1 1 220px; min-width: 0; padding: 7px 10px; border-radius: var(--radius); border: 1px solid var(--border);
754
754
  background: var(--surface); color: var(--text); font: inherit; font-size: 0.9rem;
755
755
  }
@@ -1035,6 +1035,9 @@ a.btn.github-out::after {
1035
1035
  .folder-crumb:focus-visible, .folder-row:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
1036
1036
  .folder-places, .folder-roots { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
1037
1037
  .folder-places .btn { min-height: 36px; }
1038
+ .folder-new-open { margin-left: auto; }
1039
+ .folder-new { flex-wrap: nowrap; }
1040
+ .folder-new input { flex: 1; min-width: 0; }
1038
1041
  .folder-list { display: flex; flex-direction: column; }
1039
1042
  .folder-row {
1040
1043
  display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 8px; border: 0; border-bottom: 1px solid var(--border);
@@ -1047,6 +1050,9 @@ a.btn.github-out::after {
1047
1050
  .folder-footer { justify-content: space-between; }
1048
1051
  .folder-current { flex: 1; min-width: 0; font-family: ui-monospace, "Cascadia Code", "SF Mono", Menlo, Consolas, monospace; font-size: 0.8rem; overflow-wrap: anywhere; }
1049
1052
  .folder-footer .btn { min-height: 42px; }
1053
+ @media (pointer: coarse), (max-width: 640px) {
1054
+ .folder-places .btn, .folder-new .btn, .folder-new input, .folder-footer .btn, #folder-cancel { min-height: 44px; }
1055
+ }
1050
1056
  .cwd-recent {
1051
1057
  position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 5; margin: 0; padding: 4px; list-style: none;
1052
1058
  border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3);
@@ -1056,4 +1062,5 @@ a.btn.github-out::after {
1056
1062
  color: var(--text); font-family: ui-monospace, "Cascadia Code", "SF Mono", Menlo, Consolas, monospace; font-size: 0.82rem; overflow-wrap: anywhere; cursor: pointer;
1057
1063
  }
1058
1064
  .cwd-recent-option:hover, .cwd-recent-option[aria-selected="true"] { background: color-mix(in srgb, var(--accent) 14%, transparent); }
1059
- @media (pointer: coarse) { .cwd-recent-option { min-height: 46px; } }
1065
+ @media (pointer: coarse), (max-width: 640px) { .cwd-recent-option { min-height: 46px; } }
1066
+ .cwd-recent.above { top: auto; bottom: calc(100% + 4px); }