clay-server 5.2.0-beta.10 → 5.2.0-beta.12

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.
Files changed (104) hide show
  1. package/README.md +0 -4
  2. package/lib/clay-product-control-tools.js +1 -0
  3. package/lib/clay-product-control.js +14 -11
  4. package/lib/clay-vendor-usage.js +44 -0
  5. package/lib/github-star-count.js +147 -0
  6. package/lib/notes-checklist.js +43 -1
  7. package/lib/notes.js +2 -1
  8. package/lib/project-session-notes.js +33 -8
  9. package/lib/project-sessions.js +63 -24
  10. package/lib/project-start-protocol.js +29 -0
  11. package/lib/project-vendor-login.js +2 -0
  12. package/lib/project.js +3 -0
  13. package/lib/public/app.js +27 -18
  14. package/lib/public/css/contextual-clay.css +1 -1
  15. package/lib/public/css/filebrowser.css +1 -1
  16. package/lib/public/css/mate-loading-scene.css +21 -0
  17. package/lib/public/css/overlays.css +64 -256
  18. package/lib/public/css/project-mate-creation.css +5 -6
  19. package/lib/public/css/project-start.css +108 -0
  20. package/lib/public/css/projects-hub.css +53 -14
  21. package/lib/public/css/settings-shell.css +103 -0
  22. package/lib/public/css/sidebar.css +7 -0
  23. package/lib/public/css/title-bar.css +83 -0
  24. package/lib/public/index.html +50 -63
  25. package/lib/public/modules/account-transport-settings.js +8 -2
  26. package/lib/public/modules/admin-settings-email.js +7 -2
  27. package/lib/public/modules/admin-settings-invitations.js +2 -2
  28. package/lib/public/modules/admin-settings-projects.js +2 -2
  29. package/lib/public/modules/admin-settings-ui.js +20 -4
  30. package/lib/public/modules/admin-settings-users.js +4 -4
  31. package/lib/public/modules/app-home-hub.js +2 -1
  32. package/lib/public/modules/app-messages.js +10 -2
  33. package/lib/public/modules/app-projects.js +1 -407
  34. package/lib/public/modules/app-rate-limit.js +12 -1
  35. package/lib/public/modules/clay-tool-presentation.js +1 -0
  36. package/lib/public/modules/contextual-clay.js +3 -1
  37. package/lib/public/modules/github-star.js +66 -0
  38. package/lib/public/modules/home-surface-boot.js +0 -1
  39. package/lib/public/modules/mate-loading-scene.js +17 -0
  40. package/lib/public/modules/pane-appearance-bridge.js +59 -0
  41. package/lib/public/modules/profile.js +11 -3
  42. package/lib/public/modules/project-mate-creation-workspace.js +13 -4
  43. package/lib/public/modules/project-session-visibility-settings.js +19 -16
  44. package/lib/public/modules/project-settings-environment.js +12 -1
  45. package/lib/public/modules/project-settings.js +119 -129
  46. package/lib/public/modules/project-start-dialog.js +130 -0
  47. package/lib/public/modules/project-start-folder-state.js +78 -0
  48. package/lib/public/modules/project-start-folder-view.js +190 -0
  49. package/lib/public/modules/project-start-forms.js +66 -0
  50. package/lib/public/modules/project-start-path.js +60 -0
  51. package/lib/public/modules/project-start-state.js +54 -0
  52. package/lib/public/modules/project-start-submit.js +67 -0
  53. package/lib/public/modules/projects-hub-view.js +32 -44
  54. package/lib/public/modules/projects-hub.js +39 -28
  55. package/lib/public/modules/server-settings.js +36 -91
  56. package/lib/public/modules/settings-project-target.js +56 -0
  57. package/lib/public/modules/settings-select.js +1 -1
  58. package/lib/public/modules/settings-shell-registry.js +88 -0
  59. package/lib/public/modules/settings-shell-view.js +104 -0
  60. package/lib/public/modules/settings-shell.js +342 -0
  61. package/lib/public/modules/sidebar-mobile.js +8 -30
  62. package/lib/public/modules/sidebar-projects.js +2 -1
  63. package/lib/public/modules/theme.js +31 -3
  64. package/lib/public/modules/user-settings-integrations.js +2 -1
  65. package/lib/public/modules/user-settings-layout.js +12 -34
  66. package/lib/public/modules/user-settings-profile.js +1 -1
  67. package/lib/public/modules/user-settings.js +18 -85
  68. package/lib/public/modules/vendor-usage-format.js +93 -0
  69. package/lib/public/modules/vendor-usage-list.js +45 -0
  70. package/lib/public/modules/vendor-usage-model.js +215 -0
  71. package/lib/public/modules/vendor-usage-overview.js +323 -0
  72. package/lib/public/style.css +10 -7
  73. package/lib/public/welcome-art/conversation-desk.png +0 -0
  74. package/lib/public/welcome-art/mate-clay.png +0 -0
  75. package/lib/public/welcome-art/project-clay.png +0 -0
  76. package/lib/sdk-bridge.js +6 -1
  77. package/lib/sdk-message-processor.js +9 -1
  78. package/lib/sdk-quota-snapshot.js +20 -2
  79. package/lib/server-github-stars.js +39 -0
  80. package/lib/server-global-ws.js +21 -17
  81. package/lib/server.js +11 -0
  82. package/lib/session-notes-mcp-server.js +26 -4
  83. package/lib/session-notes-writes.js +88 -0
  84. package/lib/session-visibility.js +20 -8
  85. package/lib/sessions.js +2 -0
  86. package/lib/vendor-usage-acp-readers.js +97 -0
  87. package/lib/vendor-usage-capabilities.js +88 -0
  88. package/lib/vendor-usage-http-readers.js +214 -0
  89. package/lib/vendor-usage-metrics.js +47 -0
  90. package/lib/vendor-usage-native-readers.js +106 -0
  91. package/lib/vendor-usage-overview.js +406 -0
  92. package/lib/vendor-usage-service.js +281 -0
  93. package/lib/ws-schema.js +4 -2
  94. package/lib/yoke/adapters/claude.js +12 -0
  95. package/lib/yoke/adapters/codex-query-contract.js +1 -0
  96. package/lib/yoke/adapters/codex.js +45 -12
  97. package/lib/yoke/claude-hook-installer.js +1 -0
  98. package/lib/yoke/codex-dynamic-tool-names.js +80 -0
  99. package/lib/yoke/codex-model-catalog.js +37 -3
  100. package/lib/yoke/muse-services.js +16 -3
  101. package/lib/yoke/providers/claude-model-descriptions.js +22 -2
  102. package/lib/yoke/vendor-registry.js +6 -2
  103. package/package.json +2 -1
  104. package/lib/public/modules/server-settings-navigation.js +0 -39
@@ -0,0 +1,130 @@
1
+ // project-start-dialog.js - One fixed-size dialog for choosing how to start a project
2
+
3
+ import { store } from './store.js';
4
+ import { iconHtml, refreshIcons } from './icons.js';
5
+ import { openState, closeState, selectView } from './project-start-state.js';
6
+ import { folderViewHtml, bindFolderView, renderFolderView, startFolderBrowse, resetFolderState, handleBrowseDirResult } from './project-start-folder-view.js';
7
+ import { formViewHtml, bindFormView, renderFormView } from './project-start-forms.js';
8
+ import { submitFolder, handleAddProjectResult, handleCloneProgress } from './project-start-submit.js';
9
+
10
+ export { handleBrowseDirResult, handleAddProjectResult, handleCloneProgress };
11
+
12
+ var TITLES = { choices: 'Start a project', open: 'Open a folder', create: 'Create a project', clone: 'Clone a repository' };
13
+ var CHOICES = [
14
+ { view: 'open', icon: 'folder-open', title: 'Open an existing folder', text: 'Use a directory on the machine running Clay.' },
15
+ { view: 'create', icon: 'plus', title: 'Create a new project', text: 'Start in a new, empty project directory.' },
16
+ { view: 'clone', icon: 'git-branch', title: 'Clone a repository', text: 'Bring an existing Git repository into Clay.' },
17
+ ];
18
+ var shownGen = -1;
19
+ var opener = null;
20
+
21
+ function modal() { return document.getElementById('project-start-modal'); }
22
+ function body() { return document.getElementById('project-start-body'); }
23
+ function current() { return store.get('projectStart'); }
24
+
25
+ function choicesHtml() {
26
+ var html = '<div class="project-start-view" data-view="choices"><div class="project-start-scroll"><ul class="project-start-rows project-start-choices" role="list">';
27
+ for (var i = 0; i < CHOICES.length; i++) {
28
+ var c = CHOICES[i];
29
+ html += '<li><button type="button" class="project-start-row project-start-choice" data-choice="' + c.view + '">' +
30
+ iconHtml(c.icon, 'project-start-choice-icon') + '<span class="project-start-choice-text"><strong>' + c.title + '</strong><span>' + c.text + '</span></span>' +
31
+ iconHtml('chevron-right') + '</button></li>';
32
+ }
33
+ return html + '</ul></div></div>';
34
+ }
35
+
36
+ function focusFirst(view) {
37
+ var target = null;
38
+ if (view === 'choices') target = body().querySelector('[data-choice]');
39
+ else if (view === 'open') target = body().querySelector('[data-ps="list"]');
40
+ else target = body().querySelector('[data-ps="field"]');
41
+ if (!target) target = modal().querySelector('.project-start-dialog');
42
+ target.focus({ preventScroll: true });
43
+ }
44
+
45
+ function renderShell(view) {
46
+ var container = body();
47
+ document.getElementById('project-start-title').textContent = TITLES[view];
48
+ document.getElementById('project-start-back').hidden = view === 'choices';
49
+ document.getElementById('project-start-subtitle').hidden = view !== 'choices';
50
+ if (view === 'choices') container.innerHTML = choicesHtml();
51
+ else if (view === 'open') { container.innerHTML = folderViewHtml(); bindFolderView(submitFolder); }
52
+ else { container.innerHTML = formViewHtml(view); bindFormView(view); }
53
+ refreshIcons();
54
+ }
55
+
56
+ function render(state, previous) {
57
+ var overlay = modal();
58
+ if (!overlay) return;
59
+ if (state.view === null) {
60
+ if (!overlay.classList.contains('hidden')) hide();
61
+ return;
62
+ }
63
+ if (state.gen !== shownGen || previous.view === null) {
64
+ shownGen = state.gen;
65
+ overlay.classList.remove('hidden');
66
+ renderShell(state.view);
67
+ if (state.view === 'open') { resetFolderState(); startFolderBrowse(); }
68
+ if (state.view === 'create' || state.view === 'clone') renderFormView(state.view);
69
+ focusFirst(state.view);
70
+ return;
71
+ }
72
+ if (state.view === 'open') renderFolderView();
73
+ else if (state.view === 'create' || state.view === 'clone') renderFormView(state.view);
74
+ }
75
+
76
+ function hide() {
77
+ modal().classList.add('hidden');
78
+ body().innerHTML = '';
79
+ shownGen = -1;
80
+ resetFolderState();
81
+ var target = opener;
82
+ opener = null;
83
+ if (target && document.contains(target) && typeof target.focus === 'function') target.focus({ preventScroll: true });
84
+ }
85
+
86
+ export function openAddProjectModal(mode) {
87
+ if (current().view === null) opener = document.activeElement;
88
+ store.set({ projectStart: openState(current(), mode) });
89
+ }
90
+
91
+ export function closeAddProjectModal() {
92
+ if (current().view === null) return;
93
+ store.set({ projectStart: closeState(current()) });
94
+ }
95
+
96
+ function trapTab(event, dialog) {
97
+ var items = dialog.querySelectorAll('button:not([disabled]):not([hidden]), input:not([disabled]):not([hidden]), [tabindex="-1"][data-ps="list"]');
98
+ var focusable = [];
99
+ for (var i = 0; i < items.length; i++) if (items[i].offsetParent !== null && items[i].tabIndex >= 0) focusable.push(items[i]);
100
+ if (!focusable.length) { event.preventDefault(); dialog.focus(); return; }
101
+ var first = focusable[0];
102
+ var last = focusable[focusable.length - 1];
103
+ var active = document.activeElement;
104
+ if (event.shiftKey && (active === first || active === dialog)) { event.preventDefault(); last.focus(); }
105
+ else if (!event.shiftKey && active === last) { event.preventDefault(); first.focus(); }
106
+ }
107
+
108
+ export function initProjectStart() {
109
+ var overlay = modal();
110
+ var dialog = overlay.querySelector('.project-start-dialog');
111
+ store.subscribe(function (state, previous) {
112
+ if (state.projectStart !== previous.projectStart) render(state.projectStart, previous.projectStart);
113
+ else if (state.projectStartFolder !== previous.projectStartFolder && state.projectStart.view === 'open') renderFolderView();
114
+ });
115
+ overlay.querySelector('.project-start-backdrop').addEventListener('click', closeAddProjectModal);
116
+ document.getElementById('project-start-close').addEventListener('click', closeAddProjectModal);
117
+ document.getElementById('project-start-back').addEventListener('click', function () {
118
+ store.set({ projectStart: selectView(current(), 'choices') });
119
+ });
120
+ body().addEventListener('click', function (event) {
121
+ var choice = event.target.closest('[data-choice]');
122
+ if (choice) store.set({ projectStart: selectView(current(), choice.dataset.choice) });
123
+ });
124
+ dialog.addEventListener('keydown', function (event) {
125
+ if (event.key === 'Escape') { event.preventDefault(); closeAddProjectModal(); }
126
+ else if (event.key === 'Tab') trapTab(event, dialog);
127
+ });
128
+ var addButton = document.getElementById('project-list-add');
129
+ if (addButton) addButton.addEventListener('click', function () { openAddProjectModal(); });
130
+ }
@@ -0,0 +1,78 @@
1
+ // project-start-folder-state.js - Pure state transitions for the open-folder browser.
2
+ // A reply is applied only when its requestId matches the single in-flight request.
3
+
4
+ export function emptyFolderState() {
5
+ return {
6
+ status: "idle", dir: null, parent: null, entries: [], error: null, errorKind: null,
7
+ requestId: null, requestedPath: null, candidates: [], candidateIdx: 0, initial: false, pathOpen: false,
8
+ };
9
+ }
10
+
11
+ export function beginInitial(candidates, requestId) {
12
+ var next = emptyFolderState();
13
+ next.status = "loading";
14
+ next.candidates = candidates.slice();
15
+ next.initial = true;
16
+ next.requestId = requestId;
17
+ next.requestedPath = candidates[0] || "~";
18
+ return next;
19
+ }
20
+
21
+ export function beginNavigate(prev, path, requestId) {
22
+ return Object.assign({}, prev, {
23
+ status: "loading", requestId: requestId, requestedPath: path, error: null, errorKind: null, initial: false,
24
+ });
25
+ }
26
+
27
+ function errorKindOf(message) {
28
+ return /not permitted|permission|EACCES|EPERM/i.test(message || "") ? "denied" : "failed";
29
+ }
30
+
31
+ function cleanEntries(entries) {
32
+ var out = [];
33
+ for (var i = 0; i < (entries || []).length; i++) {
34
+ var e = entries[i];
35
+ if (e && typeof e.name === "string" && typeof e.path === "string") out.push({ name: e.name, path: e.path });
36
+ }
37
+ return out;
38
+ }
39
+
40
+ // Returns { state, retryPath }. retryPath is set when the initial candidate failed and another remains.
41
+ export function applyBrowseResult(prev, msg, nextRequestId) {
42
+ if (!msg || prev.status !== "loading" || !prev.requestId || msg.requestId !== prev.requestId) {
43
+ return { state: prev, retryPath: null };
44
+ }
45
+ var failed = !!msg.error;
46
+ var wrongTarget = !failed && (typeof msg.dir !== "string" || (msg.prefix || "") !== "");
47
+ if (failed || wrongTarget) {
48
+ var idx = prev.candidateIdx + 1;
49
+ if (prev.initial && idx < prev.candidates.length) {
50
+ var retry = Object.assign({}, prev, { candidateIdx: idx, requestId: nextRequestId, requestedPath: prev.candidates[idx] });
51
+ return { state: retry, retryPath: prev.candidates[idx] };
52
+ }
53
+ return {
54
+ state: Object.assign({}, prev, {
55
+ status: "error",
56
+ error: failed ? msg.error : "That folder was not found.",
57
+ errorKind: failed ? errorKindOf(msg.error) : "missing",
58
+ }),
59
+ retryPath: null,
60
+ };
61
+ }
62
+ return {
63
+ state: Object.assign({}, prev, {
64
+ status: "ready", dir: msg.dir, parent: typeof msg.parent === "string" ? msg.parent : null,
65
+ entries: cleanEntries(msg.entries), error: null, errorKind: null, initial: false,
66
+ }),
67
+ retryPath: null,
68
+ };
69
+ }
70
+
71
+ export function failLocally(prev, message) {
72
+ if (prev.status !== "loading") return prev;
73
+ return Object.assign({}, prev, { status: "error", error: message, errorKind: "failed" });
74
+ }
75
+
76
+ export function canOpenFolder(state) {
77
+ return state.status === "ready" && typeof state.dir === "string" && state.dir.length > 0;
78
+ }
@@ -0,0 +1,190 @@
1
+ // project-start-folder-view.js - Open-folder step: browse_dir navigation and add_project submit
2
+
3
+ import { store } from './store.js';
4
+ import { getWs } from './ws-ref.js';
5
+ import { escapeHtml } from './utils.js';
6
+ import { iconHtml, refreshIcons } from './icons.js';
7
+ import { baseName, breadcrumbSegments, initialCandidates } from './project-start-path.js';
8
+ import {
9
+ beginInitial, beginNavigate, applyBrowseResult, failLocally, canOpenFolder, emptyFolderState,
10
+ } from './project-start-folder-state.js';
11
+
12
+ var seq = 0;
13
+ var onSubmit = null;
14
+
15
+ function nextId() { seq += 1; return "browse-" + seq; }
16
+ function folder() { return store.get('projectStartFolder'); }
17
+ function setFolder(next) { store.set({ projectStartFolder: next }); }
18
+ function root() { return document.querySelector('#project-start-body [data-view="open"]'); }
19
+
20
+ export function resetFolderState() { setFolder(emptyFolderState()); }
21
+
22
+ function send(path, requestId) {
23
+ var ws = getWs();
24
+ if (!ws || ws.readyState !== 1) {
25
+ setFolder(failLocally(folder(), "Not connected to the server."));
26
+ return;
27
+ }
28
+ ws.send(JSON.stringify({ type: "browse_dir", path: path, requestId: requestId }));
29
+ }
30
+
31
+ export function startFolderBrowse() {
32
+ var next = beginInitial(initialCandidates(store.snap()), nextId());
33
+ setFolder(next);
34
+ send(next.requestedPath, next.requestId);
35
+ }
36
+
37
+ export function navigateTo(path) {
38
+ if (!path) return;
39
+ var next = beginNavigate(folder(), path, nextId());
40
+ setFolder(next);
41
+ send(path, next.requestId);
42
+ }
43
+
44
+ export function handleBrowseDirResult(msg) {
45
+ if (store.get('projectStart').view !== 'open') return;
46
+ var out = applyBrowseResult(folder(), msg, nextId());
47
+ if (out.state === folder()) return;
48
+ setFolder(out.state);
49
+ if (out.retryPath) send(out.retryPath, out.state.requestId);
50
+ }
51
+
52
+ export function folderViewHtml() {
53
+ return '<div class="project-start-view" data-view="open">' +
54
+ '<div class="project-start-location">' +
55
+ '<button type="button" class="project-start-icon-btn" data-ps="up" aria-label="Up one folder">' + iconHtml('arrow-up') + '</button>' +
56
+ '<nav class="project-start-crumbs" aria-label="Current folder"></nav>' +
57
+ '<button type="button" class="project-start-link-btn" data-ps="toggle-path" aria-expanded="false" aria-controls="project-start-pathform">Enter path</button>' +
58
+ '</div>' +
59
+ '<form class="project-start-pathform" id="project-start-pathform" hidden>' +
60
+ '<input type="text" class="project-start-input" data-ps="path-input" aria-label="Folder path" placeholder="/path/to/folder" autocomplete="off" spellcheck="false">' +
61
+ '<button type="submit" class="project-start-secondary">Go</button>' +
62
+ '</form>' +
63
+ '<div class="project-start-scroll project-start-list" data-ps="list" tabindex="-1" aria-live="polite"></div>' +
64
+ '<div class="project-start-footer">' +
65
+ '<p class="project-start-error" data-ps="error" role="alert" hidden></p>' +
66
+ '<button type="button" class="project-start-primary" data-ps="open" disabled>Open</button>' +
67
+ '</div></div>';
68
+ }
69
+
70
+ function stateBlock(kind, title, text, actions) {
71
+ return '<div class="project-start-state project-start-state-' + kind + '">' +
72
+ (kind === 'loading' ? '<span class="project-start-spinner" aria-hidden="true"></span>' : '') +
73
+ '<strong>' + escapeHtml(title) + '</strong>' + (text ? '<span>' + escapeHtml(text) + '</span>' : '') +
74
+ (actions || '') + '</div>';
75
+ }
76
+
77
+ function errorTitle(kind) {
78
+ if (kind === 'denied') return "You can't open this folder";
79
+ if (kind === 'missing') return "Folder not found";
80
+ return "Couldn't load folders";
81
+ }
82
+
83
+ function listHtml(state) {
84
+ if (state.status === 'loading') return stateBlock('loading', 'Loading folders…');
85
+ if (state.status === 'error') {
86
+ var actions = '<div class="project-start-state-actions">' +
87
+ '<button type="button" class="project-start-secondary" data-ps="retry">Try again</button>' +
88
+ (state.dir ? '<button type="button" class="project-start-secondary" data-ps="back-to-dir">Back to ' + escapeHtml(baseName(state.dir)) + '</button>' : '') + '</div>';
89
+ return stateBlock('error', errorTitle(state.errorKind), state.error, actions);
90
+ }
91
+ if (state.status !== 'ready') return '';
92
+ if (state.entries.length === 0) return stateBlock('empty', 'No folders here', 'This folder has no sub-folders. You can still open it.');
93
+ var html = '<ul class="project-start-rows" role="list">';
94
+ for (var i = 0; i < state.entries.length; i++) {
95
+ html += '<li><button type="button" class="project-start-row project-start-folder-row" data-ps="enter" data-path="' + escapeHtml(state.entries[i].path) + '">' +
96
+ iconHtml('folder') + '<span class="project-start-row-name">' + escapeHtml(state.entries[i].name) + '</span>' + iconHtml('chevron-right') + '</button></li>';
97
+ }
98
+ return html + '</ul>';
99
+ }
100
+
101
+ function crumbsHtml(state) {
102
+ if (!state.dir) return '<span class="project-start-crumb-muted">' + (state.status === 'error' ? 'No folder selected' : 'Loading…') + '</span>';
103
+ var info = breadcrumbSegments(state.dir, 3);
104
+ var html = info.truncated ? '<span class="project-start-crumb-muted">…</span><span class="project-start-crumb-sep">/</span>' : '';
105
+ for (var i = 0; i < info.segments.length; i++) {
106
+ var last = i === info.segments.length - 1;
107
+ html += '<span class="' + (last ? 'project-start-crumb-current' : 'project-start-crumb') + '"' + (last ? ' aria-current="location"' : '') + '>' + escapeHtml(info.segments[i]) + '</span>';
108
+ if (!last) html += '<span class="project-start-crumb-sep">/</span>';
109
+ }
110
+ return html;
111
+ }
112
+
113
+ export function renderFolderView() {
114
+ var view = root();
115
+ if (!view) return;
116
+ var state = folder();
117
+ var crumbs = view.querySelector('.project-start-crumbs');
118
+ crumbs.innerHTML = crumbsHtml(state);
119
+ crumbs.title = state.dir || '';
120
+ view.querySelector('[data-ps="up"]').disabled = !(state.status === 'ready' && state.parent);
121
+ var list = view.querySelector('[data-ps="list"]');
122
+ var listHadFocus = list.contains(document.activeElement);
123
+ list.innerHTML = listHtml(state);
124
+ if (listHadFocus) list.focus({ preventScroll: true });
125
+ list.setAttribute('aria-busy', state.status === 'loading' ? 'true' : 'false');
126
+ var pathInput = view.querySelector('[data-ps="path-input"]');
127
+ if (state.status === 'ready' && document.activeElement !== pathInput) pathInput.value = state.dir;
128
+ var submit = store.get('projectStart').submit;
129
+ var busy = submit.status === 'busy';
130
+ var open = view.querySelector('[data-ps="open"]');
131
+ var name = state.dir ? baseName(state.dir) : '';
132
+ open.innerHTML = busy ? 'Opening…' : 'Open ' + (name ? '<span class="project-start-open-name">' + escapeHtml(name) + '</span>' : 'folder');
133
+ open.disabled = busy || !canOpenFolder(state);
134
+ open.title = state.dir && canOpenFolder(state) ? state.dir : '';
135
+ var error = view.querySelector('[data-ps="error"]');
136
+ error.textContent = submit.error || '';
137
+ error.hidden = !submit.error;
138
+ refreshIcons();
139
+ }
140
+
141
+ function togglePath(view, force) {
142
+ var form = view.querySelector('.project-start-pathform');
143
+ var open = typeof force === 'boolean' ? force : form.hidden;
144
+ form.hidden = !open;
145
+ view.querySelector('[data-ps="toggle-path"]').setAttribute('aria-expanded', open ? 'true' : 'false');
146
+ if (open) {
147
+ var input = form.querySelector('input');
148
+ input.value = folder().dir || '';
149
+ input.focus();
150
+ input.select();
151
+ }
152
+ }
153
+
154
+ export function bindFolderView(submitFolder) {
155
+ onSubmit = submitFolder;
156
+ var view = root();
157
+ view.addEventListener('click', function (event) {
158
+ var target = event.target.closest('[data-ps]');
159
+ if (!target || target.disabled) return;
160
+ var action = target.dataset.ps;
161
+ var state = folder();
162
+ if (action === 'up' && state.parent) navigateTo(state.parent);
163
+ else if (action === 'enter') navigateTo(target.dataset.path);
164
+ else if (action === 'retry') navigateTo(state.requestedPath);
165
+ else if (action === 'back-to-dir') navigateTo(state.dir);
166
+ else if (action === 'toggle-path') togglePath(view);
167
+ else if (action === 'open' && canOpenFolder(state) && onSubmit) onSubmit(state.dir);
168
+ });
169
+ view.querySelector('.project-start-pathform').addEventListener('submit', function (event) {
170
+ event.preventDefault();
171
+ var value = view.querySelector('[data-ps="path-input"]').value.trim();
172
+ if (value) navigateTo(value);
173
+ });
174
+ view.addEventListener('keydown', function (event) {
175
+ if (event.key === 'Escape' && !view.querySelector('.project-start-pathform').hidden) {
176
+ event.stopPropagation();
177
+ togglePath(view, false);
178
+ view.querySelector('[data-ps="toggle-path"]').focus();
179
+ return;
180
+ }
181
+ var rows = view.querySelectorAll('[data-ps="enter"]');
182
+ var index = Array.prototype.indexOf.call(rows, document.activeElement);
183
+ if (index < 0 || !rows.length) return;
184
+ var key = event.key;
185
+ if (key !== 'ArrowDown' && key !== 'ArrowUp' && key !== 'Home' && key !== 'End') return;
186
+ event.preventDefault();
187
+ var next = key === 'Home' ? 0 : key === 'End' ? rows.length - 1 : Math.max(0, Math.min(rows.length - 1, index + (key === 'ArrowDown' ? 1 : -1)));
188
+ rows[next].focus();
189
+ });
190
+ }
@@ -0,0 +1,66 @@
1
+ // project-start-forms.js - Create and clone steps (name or URL only)
2
+
3
+ import { store } from './store.js';
4
+ import { escapeHtml } from './utils.js';
5
+ import { submitCreate, submitClone } from './project-start-submit.js';
6
+
7
+ var FORMS = {
8
+ create: {
9
+ label: 'Project name', field: 'name', placeholder: 'my-project', action: 'Create project', busy: 'Creating…',
10
+ hint: 'Letters, numbers, dashes and underscores.', submit: submitCreate,
11
+ },
12
+ clone: {
13
+ label: 'GitHub repository URL', field: 'url', placeholder: 'https://github.com/user/repo', action: 'Clone repository', busy: 'Cloning…',
14
+ hint: 'Paste the repository address to bring it into Clay.', submit: submitClone,
15
+ },
16
+ };
17
+
18
+ export function formViewHtml(kind) {
19
+ var form = FORMS[kind];
20
+ return '<form class="project-start-view" data-view="' + kind + '" novalidate>' +
21
+ '<div class="project-start-scroll project-start-formbody">' +
22
+ '<label class="project-start-label" for="project-start-field">' + form.label + '</label>' +
23
+ '<input type="text" id="project-start-field" class="project-start-input" data-ps="field" placeholder="' + form.placeholder + '" autocomplete="off" spellcheck="false" aria-describedby="project-start-hint">' +
24
+ '<p class="project-start-hint" id="project-start-hint">' + form.hint + '</p>' +
25
+ '<p class="project-start-progress" data-ps="progress" role="status" hidden><span class="project-start-spinner" aria-hidden="true"></span>Cloning repository…</p>' +
26
+ '<p class="project-start-error" data-ps="error" role="alert" hidden></p>' +
27
+ '</div>' +
28
+ '<div class="project-start-footer"><button type="submit" class="project-start-primary" data-ps="submit">' + escapeHtml(form.action) + '</button></div>' +
29
+ '</form>';
30
+ }
31
+
32
+ function root(kind) { return document.querySelector('#project-start-body [data-view="' + kind + '"]'); }
33
+
34
+ export function renderFormView(kind) {
35
+ var view = root(kind);
36
+ if (!view) return;
37
+ var form = FORMS[kind];
38
+ var current = store.get('projectStart');
39
+ var submit = current.submit;
40
+ var busy = submit.status === 'busy';
41
+ var field = view.querySelector('[data-ps="field"]');
42
+ if (field.value !== current[form.field]) field.value = current[form.field];
43
+ field.readOnly = busy;
44
+ var button = view.querySelector('[data-ps="submit"]');
45
+ button.disabled = busy;
46
+ button.textContent = busy ? form.busy : form.action;
47
+ view.querySelector('[data-ps="progress"]').hidden = !(busy && kind === 'clone' && submit.cloning);
48
+ var error = view.querySelector('[data-ps="error"]');
49
+ error.textContent = submit.error || '';
50
+ error.hidden = !submit.error;
51
+ view.setAttribute('aria-busy', busy ? 'true' : 'false');
52
+ }
53
+
54
+ export function bindFormView(kind) {
55
+ var view = root(kind);
56
+ var form = FORMS[kind];
57
+ view.querySelector('[data-ps="field"]').addEventListener('input', function (event) {
58
+ var patch = {};
59
+ patch[form.field] = event.target.value;
60
+ store.set({ projectStart: Object.assign({}, store.get('projectStart'), patch) });
61
+ });
62
+ view.addEventListener('submit', function (event) {
63
+ event.preventDefault();
64
+ form.submit();
65
+ });
66
+ }
@@ -0,0 +1,60 @@
1
+ // project-start-path.js - Pure path helpers for the project-start dialog (no DOM, no store)
2
+
3
+ function trimTrailing(p) {
4
+ var out = String(p || "");
5
+ while (out.length > 1 && /[\\/]$/.test(out) && !/^[A-Za-z]:[\\/]$/.test(out)) out = out.slice(0, -1);
6
+ return out;
7
+ }
8
+
9
+ export function baseName(dir) {
10
+ var p = trimTrailing(dir);
11
+ if (p === "/" || /^[A-Za-z]:[\\/]$/.test(p)) return p;
12
+ var parts = p.split(/[\\/]/);
13
+ return parts[parts.length - 1] || p;
14
+ }
15
+
16
+ export function parentOf(dir) {
17
+ var p = trimTrailing(dir);
18
+ var idx = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
19
+ if (idx < 0) return null;
20
+ if (idx === 0) return p.length > 1 ? "/" : null;
21
+ if (/^[A-Za-z]:$/.test(p.slice(0, idx))) return p.slice(0, idx + 1);
22
+ return p.slice(0, idx);
23
+ }
24
+
25
+ // Last `max` segments of a directory, with a flag when earlier segments are hidden.
26
+ export function breadcrumbSegments(dir, max) {
27
+ var p = trimTrailing(dir);
28
+ var limit = max || 3;
29
+ var parts = p.split(/[\\/]/).filter(function (part) { return part.length > 0; });
30
+ if (parts.length === 0) return { truncated: false, segments: [p || "/"] };
31
+ return { truncated: parts.length > limit, segments: parts.slice(-limit) };
32
+ }
33
+
34
+ function ordinaryProjectParent(state) {
35
+ var projects = Array.isArray(state.projectsHubList) ? state.projectsHubList : [];
36
+ for (var i = 0; i < projects.length; i++) {
37
+ var project = projects[i];
38
+ if (project && project.slug === state.currentSlug && !project.isMate && typeof project.path === "string" && project.path) {
39
+ return parentOf(project.path);
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+
45
+ // Ordered starting directories to try. Each one is only a request; the server decides whether
46
+ // the signed-in user may list it, and the next candidate is tried when it cannot.
47
+ export function initialCandidates(state) {
48
+ var out = [];
49
+ var users = Array.isArray(state.cachedAllUsers) ? state.cachedAllUsers : [];
50
+ var me = null;
51
+ for (var i = 0; i < users.length; i++) if (users[i].id === state.myUserId) me = users[i];
52
+ if (state.isOsUsers && me && me.role !== "admin" && me.linuxUser) {
53
+ out.push("/home/" + me.linuxUser);
54
+ return out;
55
+ }
56
+ var near = ordinaryProjectParent(state);
57
+ if (near) out.push(near);
58
+ out.push("~");
59
+ return out;
60
+ }
@@ -0,0 +1,54 @@
1
+ // project-start-state.js - Pure state transitions for the project-start dialog
2
+
3
+ var VIEWS = { choices: true, open: true, create: true, clone: true };
4
+
5
+ export function idleSubmit() {
6
+ return { status: 'idle', kind: null, requestId: null, error: null, cloning: false };
7
+ }
8
+
9
+ export function initialProjectStart() {
10
+ return { view: null, gen: 0, name: '', url: '', submit: idleSubmit() };
11
+ }
12
+
13
+ // Deep links: "existing" lands on the folder step, "create"/"clone" on theirs, anything else on the choices.
14
+ export function viewForMode(mode) {
15
+ if (mode === 'existing') return 'open';
16
+ return mode === 'create' || mode === 'clone' ? mode : 'choices';
17
+ }
18
+
19
+ export function openState(prev, mode) {
20
+ return { view: viewForMode(mode), gen: prev.gen + 1, name: '', url: '', submit: idleSubmit() };
21
+ }
22
+
23
+ export function closeState(prev) {
24
+ return { view: null, gen: prev.gen + 1, name: '', url: '', submit: idleSubmit() };
25
+ }
26
+
27
+ export function selectView(prev, view) {
28
+ if (!VIEWS[view] || prev.view === null) return prev;
29
+ return { view: view, gen: prev.gen + 1, name: '', url: '', submit: idleSubmit() };
30
+ }
31
+
32
+ export function beginSubmit(prev, kind, requestId) {
33
+ if (prev.view === null || prev.submit.status === 'busy') return prev;
34
+ return Object.assign({}, prev, { submit: { status: 'busy', kind: kind, requestId: requestId, error: null, cloning: false } });
35
+ }
36
+
37
+ export function applyProgress(prev, msg) {
38
+ var s = prev.submit;
39
+ if (s.status !== 'busy' || s.kind !== 'clone' || !msg || msg.status !== 'cloning' || s.cloning || typeof msg.requestId !== 'string' || msg.requestId !== s.requestId) return prev;
40
+ return Object.assign({}, prev, { submit: Object.assign({}, s, { cloning: true }) });
41
+ }
42
+
43
+ // outcome: 'success' | 'error' for the submission this dialog is waiting on,
44
+ // 'orphan' for any other reply (the dialog was closed or moved on), which never touches the dialog.
45
+ export function resolveSubmit(prev, msg) {
46
+ var s = prev.submit;
47
+ var matches = prev.view !== null && s.status === 'busy' && msg && typeof msg.requestId === 'string' && msg.requestId === s.requestId;
48
+ if (!matches) return { state: prev, outcome: 'orphan', kind: null };
49
+ if (msg.ok) return { state: closeState(prev), outcome: 'success', kind: s.kind };
50
+ return {
51
+ state: Object.assign({}, prev, { submit: Object.assign({}, idleSubmit(), { error: msg.error || 'Could not add this project.' }) }),
52
+ outcome: 'error', kind: s.kind,
53
+ };
54
+ }
@@ -0,0 +1,67 @@
1
+ // project-start-submit.js - add_project / create_project / clone_project requests and replies
2
+
3
+ import { store } from './store.js';
4
+ import { getWs } from './ws-ref.js';
5
+ import { showToast } from './utils.js';
6
+ import { switchProject } from './app-projects.js';
7
+ import { beginSubmit, applyProgress, resolveSubmit, idleSubmit } from './project-start-state.js';
8
+
9
+ var seq = 0;
10
+
11
+ function state() { return store.get('projectStart'); }
12
+ function setState(next) { store.set({ projectStart: next }); }
13
+
14
+ function failSubmit(message) {
15
+ var current = state();
16
+ setState(Object.assign({}, current, { submit: Object.assign({}, idleSubmit(), { error: message }) }));
17
+ }
18
+
19
+ function submit(kind, payload) {
20
+ var begun = beginSubmit(state(), kind, 'start-' + (seq += 1));
21
+ if (begun === state()) return;
22
+ var ws = getWs();
23
+ if (!ws || ws.readyState !== 1) {
24
+ setState(begun);
25
+ failSubmit('Not connected to the server.');
26
+ return;
27
+ }
28
+ setState(begun);
29
+ ws.send(JSON.stringify(Object.assign({ requestId: begun.submit.requestId }, payload)));
30
+ }
31
+
32
+ export function submitFolder(dir) {
33
+ if (typeof dir !== 'string' || !dir) return;
34
+ submit('open', { type: 'add_project', path: dir });
35
+ }
36
+
37
+ export function submitCreate() {
38
+ var name = state().name.trim();
39
+ if (name) submit('create', { type: 'create_project', name: name });
40
+ }
41
+
42
+ export function submitClone() {
43
+ var url = state().url.trim();
44
+ if (url) submit('clone', { type: 'clone_project', url: url });
45
+ }
46
+
47
+ export function handleCloneProgress(msg) {
48
+ var next = applyProgress(state(), msg);
49
+ if (next !== state()) setState(next);
50
+ }
51
+
52
+ var SUCCESS_TOASTS = { open: 'Project added', create: 'Project created', clone: 'Project cloned' };
53
+
54
+ export function handleAddProjectResult(msg) {
55
+ var out = resolveSubmit(state(), msg);
56
+ if (out.outcome === 'orphan') {
57
+ // The dialog was closed or moved on; report the outcome without touching or reopening it.
58
+ if (msg.ok) showToast(msg.existing ? 'Project already registered' : 'Project added', msg.existing ? 'info' : 'success');
59
+ else showToast(msg.error || 'Failed to add project', 'error');
60
+ return;
61
+ }
62
+ setState(out.state);
63
+ if (out.outcome === 'success') {
64
+ showToast(msg.existing ? 'Project already registered' : SUCCESS_TOASTS[out.kind], msg.existing ? 'info' : 'success');
65
+ if (msg.slug) switchProject(msg.slug);
66
+ }
67
+ }