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

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 (98) 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 +25 -17
  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 +79 -0
  24. package/lib/public/index.html +50 -63
  25. package/lib/public/modules/admin-settings-invitations.js +2 -2
  26. package/lib/public/modules/admin-settings-projects.js +2 -2
  27. package/lib/public/modules/admin-settings-ui.js +20 -4
  28. package/lib/public/modules/admin-settings-users.js +4 -4
  29. package/lib/public/modules/app-home-hub.js +2 -1
  30. package/lib/public/modules/app-messages.js +10 -2
  31. package/lib/public/modules/app-projects.js +1 -407
  32. package/lib/public/modules/app-rate-limit.js +12 -1
  33. package/lib/public/modules/clay-tool-presentation.js +1 -0
  34. package/lib/public/modules/contextual-clay.js +3 -1
  35. package/lib/public/modules/github-star.js +66 -0
  36. package/lib/public/modules/mate-loading-scene.js +17 -0
  37. package/lib/public/modules/profile.js +11 -3
  38. package/lib/public/modules/project-mate-creation-workspace.js +13 -4
  39. package/lib/public/modules/project-session-visibility-settings.js +19 -16
  40. package/lib/public/modules/project-settings.js +119 -129
  41. package/lib/public/modules/project-start-dialog.js +130 -0
  42. package/lib/public/modules/project-start-folder-state.js +78 -0
  43. package/lib/public/modules/project-start-folder-view.js +190 -0
  44. package/lib/public/modules/project-start-forms.js +66 -0
  45. package/lib/public/modules/project-start-path.js +60 -0
  46. package/lib/public/modules/project-start-state.js +54 -0
  47. package/lib/public/modules/project-start-submit.js +67 -0
  48. package/lib/public/modules/projects-hub-view.js +33 -14
  49. package/lib/public/modules/projects-hub.js +10 -6
  50. package/lib/public/modules/server-settings.js +36 -91
  51. package/lib/public/modules/settings-project-target.js +56 -0
  52. package/lib/public/modules/settings-select.js +1 -1
  53. package/lib/public/modules/settings-shell-registry.js +88 -0
  54. package/lib/public/modules/settings-shell-view.js +104 -0
  55. package/lib/public/modules/settings-shell.js +342 -0
  56. package/lib/public/modules/sidebar-mobile.js +8 -30
  57. package/lib/public/modules/sidebar-projects.js +2 -1
  58. package/lib/public/modules/theme.js +10 -2
  59. package/lib/public/modules/user-settings-layout.js +8 -33
  60. package/lib/public/modules/user-settings-profile.js +1 -1
  61. package/lib/public/modules/user-settings.js +18 -85
  62. package/lib/public/modules/vendor-usage-format.js +93 -0
  63. package/lib/public/modules/vendor-usage-list.js +45 -0
  64. package/lib/public/modules/vendor-usage-model.js +215 -0
  65. package/lib/public/modules/vendor-usage-overview.js +312 -0
  66. package/lib/public/style.css +10 -7
  67. package/lib/public/welcome-art/conversation-desk.png +0 -0
  68. package/lib/public/welcome-art/mate-clay.png +0 -0
  69. package/lib/public/welcome-art/project-clay.png +0 -0
  70. package/lib/sdk-bridge.js +6 -1
  71. package/lib/sdk-message-processor.js +9 -1
  72. package/lib/sdk-quota-snapshot.js +20 -2
  73. package/lib/server-github-stars.js +39 -0
  74. package/lib/server-global-ws.js +21 -17
  75. package/lib/server.js +4 -0
  76. package/lib/session-notes-mcp-server.js +26 -4
  77. package/lib/session-notes-writes.js +88 -0
  78. package/lib/session-visibility.js +20 -8
  79. package/lib/sessions.js +2 -0
  80. package/lib/vendor-usage-acp-readers.js +97 -0
  81. package/lib/vendor-usage-capabilities.js +88 -0
  82. package/lib/vendor-usage-http-readers.js +214 -0
  83. package/lib/vendor-usage-metrics.js +47 -0
  84. package/lib/vendor-usage-native-readers.js +106 -0
  85. package/lib/vendor-usage-overview.js +406 -0
  86. package/lib/vendor-usage-service.js +281 -0
  87. package/lib/ws-schema.js +4 -2
  88. package/lib/yoke/adapters/claude.js +12 -0
  89. package/lib/yoke/adapters/codex-query-contract.js +1 -0
  90. package/lib/yoke/adapters/codex.js +45 -12
  91. package/lib/yoke/claude-hook-installer.js +1 -0
  92. package/lib/yoke/codex-dynamic-tool-names.js +80 -0
  93. package/lib/yoke/codex-model-catalog.js +37 -3
  94. package/lib/yoke/muse-services.js +16 -3
  95. package/lib/yoke/providers/claude-model-descriptions.js +22 -2
  96. package/lib/yoke/vendor-registry.js +6 -2
  97. package/package.json +2 -1
  98. package/lib/public/modules/server-settings-navigation.js +0 -39
@@ -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
+ }
@@ -7,8 +7,25 @@ export function ordinaryProjects(projects) {
7
7
  });
8
8
  }
9
9
 
10
- function button(action, label, extra) {
11
- return '<button type="button" class="projects-button ' + (extra || '') + '" data-projects-action="' + action + '">' + label + '</button>';
10
+ // Zero ordinary projects (once the list is known): the welcome renders inside the normal app shell.
11
+ export function isShellWelcome(state) {
12
+ return !!state.projectListLoaded && ordinaryProjects(state.projectsHubList).length === 0;
13
+ }
14
+
15
+ function button(action, label, extra, disabled) {
16
+ return '<button type="button" class="projects-button ' + (extra || '') + '" data-projects-action="' + action + '"' + (disabled ? ' disabled' : '') + '>' + label + '</button>';
17
+ }
18
+
19
+ // Mate creation starts with the built-in Clay Mate, so the CTA waits until it is in the loaded Mate list.
20
+ export function clayMateReady(state) {
21
+ var mates = Array.isArray(state.cachedMatesList) ? state.cachedMatesList : [];
22
+ for (var i = 0; i < mates.length; i++) if (mates[i] && mates[i].builtinKey === 'clay') return true;
23
+ return false;
24
+ }
25
+
26
+ function welcomeCard(label, title, text, action, artwork) {
27
+ return '<div class="projects-intro-card"><div class="projects-intro-art"><img src="/welcome-art/' + artwork + '.png" width="1536" height="1024" alt="" decoding="async"></div>' +
28
+ '<span class="projects-intro-label">' + label + '</span><h2>' + title + '</h2><p>' + text + '</p><div class="projects-intro-action">' + action + '</div></div>';
12
29
  }
13
30
 
14
31
  function setupAction(mode, icon, title, text, disabled) {
@@ -19,24 +36,27 @@ function setupAction(mode, icon, title, text, disabled) {
19
36
  export function projectsHubContent(state) {
20
37
  var projects = ordinaryProjects(state.projectsHubList);
21
38
  var ready = state.projectListLoaded;
22
- var welcome = ready && (state.projectsShowGuide || (!projects.length && (state.onboardingStep || 'welcome') === 'welcome'));
39
+ var welcome = ready && (state.projectsShowGuide || !projects.length);
23
40
  var allowed = state.projectsAccessLoaded && (!state.permissions || state.permissions.createProject === true);
24
41
  var available = allowed && state.connected && !state.pendingHomeProjectSlug;
25
- var html = '<header class="projects-header"><a class="projects-brand" href="/projects"><img src="/clay-studio-symbol.png" alt="">Clay Studio</a>' +
42
+ var html = isShellWelcome(state) ? '' : '<header class="projects-header"><a class="projects-brand" href="/projects"><img src="/clay-studio-symbol.png" alt="">Clay Studio</a>' +
26
43
  '<nav aria-label="Workspace navigation">' + button('settings', 'Settings', 'projects-button-quiet') + '</nav></header>';
27
44
  html += '<main class="projects-main" aria-labelledby="projects-title">';
28
45
  if (!ready) return html + '<div class="projects-loading" role="status"><h1 id="projects-title" tabindex="-1">Your projects</h1><p>Loading your workspace…</p></div></main>';
29
46
  if (welcome) {
30
- return html + '<section class="projects-welcome"><div class="projects-eyebrow">WELCOME TO CLAY</div>' +
31
- '<h1 id="projects-title" tabindex="-1">A place for<br>your next idea.</h1>' +
32
- '<p class="projects-lead">Bring a project. Describe what you want to build.<br>Your AI Driver helps you take it from there.</p>' +
33
- '<div class="projects-intro-grid"><div><span>01 / PROJECTS</span><h2>Work on something real.</h2><p>Open a folder, start fresh, or clone a repository. Your conversations and tools stay with the project.</p></div>' +
34
- '<div><span>02 / MATES</span><h2>Bring in the right perspective.</h2><p>Your Mates live beside projects and open their conversations in the same focused workspace.</p></div></div>' +
35
- '<div class="projects-welcome-actions">' + button('start', projects.length ? 'Explore your projects ' + iconHtml('arrow-right') : 'Set up your first project ' + iconHtml('arrow-right'), 'projects-button-primary') + '</div>' +
36
- '<p class="projects-footnote">You can revisit this guide from Projects at any time.</p></section></main>';
47
+ var mateReady = !!state.connected && clayMateReady(state);
48
+ return html + '<section class="projects-welcome"><div class="projects-eyebrow">WELCOME TO</div>' +
49
+ '<h1 id="projects-title" tabindex="-1"><span class="projects-wordmark" role="img" aria-label="Clay Studio"></span></h1>' +
50
+ '<p class="projects-lead">Where the minds work together</p>' +
51
+ '<div class="projects-intro-grid">' +
52
+ welcomeCard('01 / PROJECTS', 'Work together on a project.', 'Bring a codebase or a folder of files, and work through it together with Clay Studio.',
53
+ button(projects.length ? 'start' : 'create-project', projects.length ? 'Explore your projects ' + iconHtml('arrow-right') : 'Start a project ' + iconHtml('arrow-right'), 'projects-button-primary', !projects.length && !available), 'project-clay') +
54
+ welcomeCard('02 / MATES', 'Create your own specialist.', 'Create a Mate with the expertise you need, and work together on your ideas and tasks.',
55
+ button('mate', 'Create a Mate ' + iconHtml('arrow-right'), 'projects-button-secondary', !mateReady), 'mate-clay') +
56
+ '</div><p class="projects-footnote">You can revisit this guide from Projects at any time.</p></section></main>';
37
57
  }
38
- html += '<div class="projects-heading"><div><div class="projects-eyebrow">YOUR WORKSPACE</div><h1 id="projects-title" tabindex="-1">' + (projects.length ? 'Projects' : 'Start with a project.') + '</h1>' +
39
- '<p class="projects-lead">' + (projects.length ? 'Pick up where you left off, or begin something new.' : 'Choose where your work will live. You can add more projects later.') + '</p></div>' + button('guide', 'Getting started', 'projects-button-quiet') + '</div>';
58
+ html += '<div class="projects-heading"><div><div class="projects-eyebrow">YOUR WORKSPACE</div><h1 id="projects-title" tabindex="-1">Projects</h1>' +
59
+ '<p class="projects-lead">Pick up where you left off, or begin something new.</p></div>' + button('guide', 'Getting started', 'projects-button-quiet') + '</div>';
40
60
  if (state.pendingHomeProjectSlug) html += '<p class="projects-status" role="status">Opening your project…</p>';
41
61
  if (projects.length) {
42
62
  html += '<div class="projects-list" aria-label="Your projects">';
@@ -54,7 +74,6 @@ export function projectsHubContent(state) {
54
74
  setupAction('existing', 'folder-open', 'Open a folder', 'Use a directory on the machine running Clay.', !available) +
55
75
  setupAction('create', 'plus', 'Create a project', 'Start in a new, empty project directory.', !available) +
56
76
  setupAction('clone', 'git-branch', 'Clone a repository', 'Bring an existing Git repository into Clay.', !available) + '</div>';
57
- if (!projects.length) html += '<p class="projects-next-step"><span>UP NEXT</span> Choose your AI in the workspace, then tell your Driver what you want to build or fix.</p>';
58
77
  }
59
78
  if (!state.connected) html += '<p class="projects-status" role="status">Reconnecting… Project actions will be available when the connection returns.</p>';
60
79
  return html + '</main><footer class="projects-footer">Your projects, conversations, and tools. One workspace.</footer>';
@@ -1,9 +1,11 @@
1
1
  import { store } from './store.js';
2
2
  import { getWs } from './ws-ref.js';
3
- import { openAddProjectModal, switchProject } from './app-projects.js';
3
+ import { switchProject } from './app-projects.js';
4
+ import { openAddProjectModal } from './project-start-dialog.js';
5
+ import { startClayMateCreation } from './project-mate-creation-workspace.js';
4
6
  import { openUserSettings } from './user-settings.js';
5
7
  import { rememberHomePrimarySurface, updateHomeSurfacePreference } from './home-surface.js';
6
- import { projectsHubContent } from './projects-hub-view.js';
8
+ import { projectsHubContent, isShellWelcome, clayMateReady } from './projects-hub-view.js';
7
9
  import { refreshIcons } from './icons.js';
8
10
 
9
11
  function root() { return document.getElementById('projects-hub'); }
@@ -11,6 +13,7 @@ function root() { return document.getElementById('projects-hub'); }
11
13
  function render() {
12
14
  var element = root();
13
15
  if (!element || !store.get('projectsHubVisible')) return;
16
+ document.body.classList.toggle('projects-shell', isShellWelcome(store.snap()));
14
17
  var html = projectsHubContent(store.snap());
15
18
  if (store.get('projectsHubMarkup') === html) return;
16
19
  store.set({ projectsHubMarkup: html });
@@ -51,7 +54,7 @@ export function showProjectsHub(fromHistory) {
51
54
  export function hideProjectsHub() {
52
55
  if (!store.get('projectsHubVisible')) return;
53
56
  store.set({ projectsHubVisible: false, projectsShowGuide: false });
54
- document.body.classList.remove('projects-active');
57
+ document.body.classList.remove('projects-active', 'projects-shell');
55
58
  if (root()) root().classList.add('hidden');
56
59
  }
57
60
 
@@ -75,10 +78,11 @@ function handleClick(event) {
75
78
  }
76
79
  var ws = getWs();
77
80
  if (!ws || ws.readyState !== 1) return;
81
+ if (action === 'mate') { if (clayMateReady(store.snap())) startClayMateCreation(); return; }
78
82
  if (button.dataset.projectSlug) { switchProject(button.dataset.projectSlug); return; }
79
- if (button.dataset.projectsMode) {
83
+ if (action === 'create-project' || button.dataset.projectsMode) {
80
84
  var permissions = store.get('permissions');
81
- if (!store.get('projectsAccessLoaded') || (permissions && permissions.createProject !== true)) return;
85
+ if (!store.get('projectsAccessLoaded') || store.get('pendingHomeProjectSlug') || (permissions && permissions.createProject !== true)) return;
82
86
  openAddProjectModal(button.dataset.projectsMode);
83
87
  }
84
88
  }
@@ -96,6 +100,6 @@ export function initProjectsHub() {
96
100
  if (state.projectsHubList !== previous.projectsHubList || state.projectListLoaded !== previous.projectListLoaded ||
97
101
  state.homeSurfaceLoaded !== previous.homeSurfaceLoaded || state.onboardingStep !== previous.onboardingStep ||
98
102
  state.projectsAccessLoaded !== previous.projectsAccessLoaded || state.permissions !== previous.permissions ||
99
- state.connected !== previous.connected || state.pendingHomeProjectSlug !== previous.pendingHomeProjectSlug) render();
103
+ state.connected !== previous.connected || state.cachedMatesList !== previous.cachedMatesList || state.pendingHomeProjectSlug !== previous.pendingHomeProjectSlug) render();
100
104
  });
101
105
  }