clay-server 4.7.0-beta.1 → 4.7.0-beta.3

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 (43) hide show
  1. package/lib/project-connection.js +0 -14
  2. package/lib/project-sessions.js +0 -18
  3. package/lib/public/app.js +16 -14
  4. package/lib/public/css/input.css +27 -0
  5. package/lib/public/css/pane.css +0 -1
  6. package/lib/public/css/projects-hub.css +75 -0
  7. package/lib/public/css/tui-attention.css +0 -407
  8. package/lib/public/index.html +6 -14
  9. package/lib/public/modules/app-connection.js +23 -2
  10. package/lib/public/modules/app-favicon.js +1 -0
  11. package/lib/public/modules/app-home-hub.js +8 -3
  12. package/lib/public/modules/app-messages.js +2 -16
  13. package/lib/public/modules/app-projects.js +16 -11
  14. package/lib/public/modules/app-rendering.js +16 -10
  15. package/lib/public/modules/generation-stop.js +24 -0
  16. package/lib/public/modules/home-surface-boot.js +10 -3
  17. package/lib/public/modules/home-surface.js +13 -1
  18. package/lib/public/modules/projects-hub-view.js +62 -0
  19. package/lib/public/modules/projects-hub.js +105 -0
  20. package/lib/public/style.css +2 -0
  21. package/lib/sdk-bridge.js +17 -3
  22. package/lib/server.js +1 -1
  23. package/lib/sessions.js +2 -0
  24. package/lib/users-home-surface-preferences.js +4 -2
  25. package/lib/users-preferences.js +0 -36
  26. package/lib/users.js +0 -4
  27. package/lib/ws-schema.js +0 -3
  28. package/lib/yoke/acp-process-manager.js +25 -15
  29. package/lib/yoke/acp-query-handle.js +52 -40
  30. package/lib/yoke/adapters/antigravity.js +25 -1
  31. package/lib/yoke/adapters/claude.js +25 -7
  32. package/lib/yoke/adapters/codex.js +55 -24
  33. package/lib/yoke/adapters/kiro.js +30 -19
  34. package/lib/yoke/codex-turn-events.js +32 -0
  35. package/lib/yoke/codex-turn-stop.js +59 -0
  36. package/lib/yoke/query-abort.js +40 -0
  37. package/lib/yoke/user-input-guidance.js +11 -0
  38. package/lib/yoke/user-input.js +1 -1
  39. package/package.json +1 -1
  40. package/lib/public/modules/whats-new-article.js +0 -70
  41. package/lib/public/modules/whats-new.js +0 -264
  42. package/lib/whats-new-content.js +0 -84
  43. package/lib/whats-new.js +0 -57
@@ -124,6 +124,7 @@
124
124
 
125
125
  <!-- === Main Area (sidebar + resize-handle + main-column) === -->
126
126
  <div id="main-area">
127
+ <section id="projects-hub" class="hidden" aria-label="Projects"></section>
127
128
  <div id="home-hub" class="hidden">
128
129
  <div class="home-hub-inner">
129
130
  <button id="home-sidebar-backdrop" class="home-sidebar-backdrop" type="button" aria-label="Close Home navigation"></button>
@@ -136,6 +137,7 @@
136
137
  </div>
137
138
  </div>
138
139
  <nav class="home-sidebar-primary-actions" aria-label="Home actions">
140
+ <button id="home-nav-projects" class="home-sidebar-action" type="button" title="Projects" aria-label="Projects"><span class="home-sidebar-action-icon" aria-hidden="true"><i data-lucide="folder"></i></span><span>Projects</span></button>
139
141
  <button id="home-sidebar-new" class="home-sidebar-action home-sidebar-new" type="button" title="New Chat" aria-label="Start a new chat with the current Mate" disabled><span class="home-sidebar-action-icon" aria-hidden="true"><i data-lucide="message-square-plus"></i></span><span>New Chat</span></button>
140
142
  <button id="home-sidebar-debate" class="home-sidebar-action" type="button" title="Browse debates" aria-label="Browse debates" aria-pressed="false"><span class="home-sidebar-action-icon" aria-hidden="true"><i data-lucide="messages-square"></i></span><span>Debates</span></button>
141
143
  <button id="home-tools-btn" class="home-sidebar-action" type="button" title="Capsules" aria-label="Capsules" aria-expanded="false" aria-controls="home-tool-workbench"><span class="home-sidebar-action-icon" aria-hidden="true"><i data-lucide="box"></i></span><span id="home-tools-label">Capsules</span><span id="home-tools-activity" class="home-tools-activity" aria-hidden="true"></span></button>
@@ -157,7 +159,7 @@
157
159
  </aside>
158
160
  <div id="home-workspace" class="home-workspace">
159
161
  <button id="home-sidebar-expand" class="home-sidebar-expand" type="button" title="Show sidebar" aria-label="Show Home sidebar" aria-describedby="home-sidebar-expand-brand-label"><i data-lucide="panel-left-open"></i><span id="home-sidebar-expand-brand-label" class="home-sidebar-brand-wordmark home-sidebar-expand-wordmark">Clay Studio</span></button>
160
- <button id="home-close-control" class="home-close-control hidden" type="button" title="Close Home" aria-label="Close Home"><i data-lucide="x" aria-hidden="true"></i><span>Close</span></button>
162
+ <button id="home-close-control" class="home-close-control" type="button" title="Projects" aria-label="Projects"><i data-lucide="folder" aria-hidden="true"></i><span>Projects</span></button>
161
163
  <div id="home-conversation-region" class="home-conversation-region">
162
164
  <div id="home-mate-chat" class="home-mate-chat">
163
165
  <div class="home-mate-chat-stage">
@@ -216,19 +218,6 @@
216
218
  </div>
217
219
  </div>
218
220
 
219
- <div id="whats-new-article" class="hidden">
220
- <header class="wna-toolbar">
221
- <button type="button" class="wna-back" id="wna-back">
222
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>
223
- <span>Back</span>
224
- </button>
225
- </header>
226
- <article class="wna-content">
227
- <div class="wna-date" id="wna-date"></div>
228
- <h1 class="wna-title" id="wna-title"></h1>
229
- <div class="wna-body" id="wna-body"></div>
230
- </article>
231
- </div>
232
221
  <div id="sidebar-column">
233
222
  <div class="title-bar-sidebar">
234
223
  <button class="title-bar-project-dropdown" id="title-bar-project-dropdown">
@@ -646,6 +635,9 @@
646
635
  </div>
647
636
  </div>
648
637
  </div>
638
+ <div id="generation-stop-row" class="hidden">
639
+ <button id="generation-stop" type="button" aria-label="Stop generating"><i data-lucide="square"></i><span>Stop</span></button>
640
+ </div>
649
641
  </div>
650
642
  </div>
651
643
  <!-- Mobile input-more bottom sheet: attach / image + context sources -->
@@ -151,7 +151,19 @@ function suspendSocketWatchdogs() {
151
151
  watchdog.suspend();
152
152
  }
153
153
 
154
+ function refreshBackgroundPane() {
155
+ if (document.hidden || !store.get('paneNeedsSnapshot')) return false;
156
+ store.set({ paneNeedsSnapshot: false });
157
+ if (!store.get('paneMode') || !store.get('paneSessionId') || lifecycle.isOffline()) return false;
158
+ // A fresh socket discards browser-queued deltas and re-pins the exact pane.
159
+ // Server replay supplies current history, status and pending interactions.
160
+ watchdog.resume();
161
+ connect();
162
+ return true;
163
+ }
164
+
154
165
  function resumeSocketWatchdogs(reason) {
166
+ if (refreshBackgroundPane()) return;
155
167
  var resumed = watchdog.resume();
156
168
  var socket = getWs();
157
169
  if (socket && socket.readyState === 1 && resumed) startHeartbeat(socket, lifecycle.getEpoch());
@@ -178,10 +190,13 @@ export function initConnection() {
178
190
  });
179
191
  });
180
192
  document.addEventListener("visibilitychange", function () {
181
- if (document.hidden) suspendSocketWatchdogs();
182
- else resumeSocketWatchdogs("visible");
193
+ if (document.hidden) {
194
+ if (store.get('paneMode') && store.get('paneSessionId')) store.set({ paneNeedsSnapshot: true });
195
+ suspendSocketWatchdogs();
196
+ } else resumeSocketWatchdogs("visible");
183
197
  });
184
198
  document.addEventListener("freeze", function () {
199
+ if (store.get('paneMode') && store.get('paneSessionId')) store.set({ paneNeedsSnapshot: true });
185
200
  suspendSocketWatchdogs();
186
201
  });
187
202
  document.addEventListener("resume", function () {
@@ -387,6 +402,12 @@ export function connect() {
387
402
 
388
403
  newWs.onmessage = function (event) {
389
404
  if (getWs() !== newWs || !lifecycle.current(attemptEpoch)) return;
405
+ // Hidden pane deltas are replaced by a current server snapshot on return.
406
+ if (store.get('paneMode') && store.get('paneSessionId') && document.hidden) {
407
+ store.set({ paneNeedsSnapshot: true });
408
+ return;
409
+ }
410
+ if (refreshBackgroundPane()) return;
390
411
  // Backup: if we're receiving messages, we're connected
391
412
  if (!store.get('connected')) {
392
413
  setStatus("connected");
@@ -83,6 +83,7 @@ export function drawFaviconAnimFrame() {
83
83
 
84
84
  export function setSendBtnMode(mode) {
85
85
  var sendBtn = getSendBtn();
86
+ sendBtn.setAttribute("aria-label", mode === "stop" ? "Stop generating" : "Send");
86
87
  if (mode === "stop") {
87
88
  sendBtn.disabled = false;
88
89
  sendBtn.classList.add("stop");
@@ -1,5 +1,6 @@
1
1
  // app-home-hub.js - Home work hub navigation and first-depth Mate list.
2
2
 
3
+ import { showProjectsHub, hideProjectsHub } from './projects-hub.js';
3
4
  import { store } from './store.js';
4
5
  import { getWs } from './ws-ref.js';
5
6
  import { getCachedProjects, switchProject } from './app-projects.js';
@@ -42,7 +43,7 @@ function syncHomePresentation() {
42
43
  function syncHomeCloseControl() {
43
44
  var close = document.getElementById("home-close-control");
44
45
  if (!close) return;
45
- close.classList.toggle("hidden", !getHomeReturnSlug());
46
+ close.classList.remove("hidden");
46
47
  }
47
48
 
48
49
  function getHomeReturnSlug() {
@@ -245,7 +246,8 @@ export function initHomeHub() {
245
246
  initHomeDock();
246
247
  initHomeDebatesArchive();
247
248
  document.getElementById("home-mate-list").addEventListener("keydown", handleMateListKeydown);
248
- document.getElementById("home-close-control").addEventListener("click", minimizeHomeHub);
249
+ document.getElementById("home-close-control").addEventListener("click", function () { showProjectsHub(); });
250
+ document.getElementById("home-nav-projects").addEventListener("click", function () { showProjectsHub(); });
249
251
  window.addEventListener("clay:home-debate", function () { hideHomeHub(); });
250
252
  document.addEventListener("visibilitychange", syncHomePresentation);
251
253
 
@@ -349,6 +351,8 @@ export function updateHomeIconBadge() {
349
351
  }
350
352
 
351
353
  export function showHomeHub(fromHistory) {
354
+ hideProjectsHub();
355
+ store.set({ pendingHomeProjectSlug: null });
352
356
  if (store.get('dmMode')) exitDmMode();
353
357
  var activeMateId = store.get('homeChatMateId');
354
358
  var resume = homeHubSuspended || homeHubVisible;
@@ -399,7 +403,8 @@ export function hideHomeHub() {
399
403
 
400
404
  export function minimizeHomeHub() {
401
405
  var slug = getHomeReturnSlug();
402
- if (!slug || !homeHubVisible) return;
406
+ if (!homeHubVisible) return;
407
+ if (!slug) { showProjectsHub(); return; }
403
408
  switchProject(slug);
404
409
  var projectInput = document.getElementById("input");
405
410
  if (projectInput && !projectInput.disabled) projectInput.focus({ preventScroll: true });
@@ -66,11 +66,9 @@ import { renderGeneratedImage, renderImageGenerationProgress, clearImageGenerati
66
66
  import { clearPairResultStatus, handlePairResultStatus } from './pair-result-status.js';
67
67
 
68
68
  // --- App module imports ---
69
- import { scrollToBottom, addToMessages, addUserMessage, addSystemMessage, removeMatePreThinking, appendDelta, finalizeAssistantBlock, addConflictMessage, addContextOverflowMessage, showSuggestionChips, armStickyBottom, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
69
+ import { scrollToBottom, addToMessages, addUserMessage, addSystemMessage, removeMatePreThinking, resetStreamBuffer, appendDelta, finalizeAssistantBlock, addConflictMessage, addContextOverflowMessage, showSuggestionChips, armStickyBottom, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
70
70
  import { setActivity, startUrgentBlink, stopUrgentBlink, blinkSessionDot, updateCrossProjectBlink } from './app-favicon.js';
71
71
  import { setStatus } from './app-connection.js';
72
- import { handleWhatsNewState, handleWhatsNewSeenResult, setKnownEntries as setWhatsNewKnownEntries } from './whats-new.js';
73
- import { closeArticle as closeWhatsNewArticle } from './whats-new-article.js';
74
72
  import { resolvePaneSession, resolveSwitchedVendor } from './pane-session.js';
75
73
  import { markProjectSessionListHydrated, applyProjectSplitGroups, clearProjectSplitState } from './split-session-boundary.js';
76
74
  import { selectDefaultVendorForBlankSession } from './vendor-selection.js';
@@ -836,8 +834,8 @@ export function processMessage(msg) {
836
834
 
837
835
  case "session_switched":
838
836
  clearPairResultStatus();
837
+ resetStreamBuffer();
839
838
  syncAutonomousRunForSession(msg.autonomousRun || null);
840
- closeWhatsNewArticle();
841
839
  // Save draft from outgoing session
842
840
  var _prevSid = store.get('activeSessionId');
843
841
  if (_prevSid && inputEl.value) {
@@ -2127,18 +2125,6 @@ export function processMessage(msg) {
2127
2125
  handleAutoContinueChanged(msg);
2128
2126
  break;
2129
2127
 
2130
- case "whats_new_state":
2131
- // Keep a known-entries cache for the home page feed (which shows
2132
- // both seen and unseen entries), then queue unseen ones for the
2133
- // carousel.
2134
- if (msg && Array.isArray(msg.entries)) setWhatsNewKnownEntries(msg.entries);
2135
- handleWhatsNewState(msg);
2136
- break;
2137
-
2138
- case "whats_new_seen_result":
2139
- handleWhatsNewSeenResult(msg);
2140
- break;
2141
-
2142
2128
  case "set_claude_open_mode_result":
2143
2129
  case "claude_open_mode_changed":
2144
2130
  if (msg.claudeOpenMode === "tui" || msg.claudeOpenMode === "gui") {
@@ -1,6 +1,7 @@
1
1
  // app-projects.js - Project list, switching, add/remove project modals
2
2
  // Extracted from app.js (PR-29)
3
3
 
4
+ import { showProjectsHub, hideProjectsHub, completeProjectsOnboarding } from './projects-hub.js';
4
5
  import { escapeHtml, showToast } from './utils.js';
5
6
  import { refreshIcons } from './icons.js';
6
7
  import { parseEmojis } from './markdown.js';
@@ -17,7 +18,6 @@ import { spawnDustParticles } from './sidebar.js';
17
18
  import { isSearchOpen, closeSearch } from './session-search.js';
18
19
  import { exitDmMode } from './app-dm.js';
19
20
  import { isHomeHubVisible, hideHomeHub, showHomeHub } from './app-home-hub.js';
20
- import { closeArticle as closeWhatsNewArticle } from './whats-new-article.js';
21
21
  import { resetFileBrowser } from './filebrowser.js';
22
22
  import { closeNotesBrowser } from './sticky-notes-browser.js';
23
23
  import { hideMemory } from './mate-memory.js';
@@ -203,7 +203,7 @@ export function initProjects() {
203
203
  // --- State accessors ---
204
204
 
205
205
  export function getCachedProjects() { return cachedProjects; }
206
- export function setCachedProjects(v) { cachedProjects = v; }
206
+ export function setCachedProjects(v) { cachedProjects = v; store.set({ projectsHubList: v, projectListLoaded: true }); }
207
207
  export function getCachedProjectCount() { return cachedProjectCount; }
208
208
  export function setCachedProjectCount(v) { cachedProjectCount = v; }
209
209
  export function getCachedRemovedProjects() { return cachedRemovedProjects; }
@@ -223,6 +223,7 @@ export function updateProjectList(msg) {
223
223
  _lastProjectsJson = projectsJson;
224
224
  }
225
225
  cachedProjects = msg.projects;
226
+ store.set({ projectsHubList: cachedProjects, projectListLoaded: true });
226
227
  }
227
228
  if (msg.removedProjects) cachedRemovedProjects = msg.removedProjects;
228
229
  else if (msg.removedProjects === undefined) { /* keep cached */ }
@@ -424,15 +425,16 @@ export function switchProject(slug) {
424
425
  var st = store.snap();
425
426
  var ws = getWs();
426
427
  var targetWsPath = projectWsPath(slug);
427
- var homeVisible = isHomeHubVisible();
428
+ var homeVisible = isHomeHubVisible() || store.get('projectsHubVisible');
428
429
  var alreadyInProject = isProjectActivated(st, slug, ws);
429
430
  var wasDm = st.dmMode;
430
431
  var wasMate = st.dmMode && st.dmTargetUser && st.dmTargetUser.isMate;
431
432
  if (st.dmMode) exitDmMode(wasMate);
432
- closeWhatsNewArticle();
433
433
  if (homeVisible && alreadyInProject) {
434
434
  rememberHomePrimarySurface("project", slug);
435
435
  hideHomeHub();
436
+ hideProjectsHub();
437
+ completeProjectsOnboarding();
436
438
  if (document.documentElement.classList.contains("pwa-standalone")) {
437
439
  history.replaceState(null, "", "/p/" + slug + "/");
438
440
  } else {
@@ -473,6 +475,8 @@ export function completePendingProjectActivation() {
473
475
  var slug = store.get('pendingHomeProjectSlug');
474
476
  if (!slug || !isProjectActivated(store.snap(), slug, getWs())) return false;
475
477
  store.set({ pendingHomeProjectSlug: null });
478
+ hideProjectsHub();
479
+ completeProjectsOnboarding();
476
480
  rememberHomePrimarySurface("project", slug);
477
481
  if (document.documentElement.classList.contains("pwa-standalone")) {
478
482
  history.replaceState(null, "", "/p/" + slug + "/");
@@ -492,7 +496,7 @@ export function confirmCurrentProjectSessionActivation() {
492
496
  export function finishProjectSessionActivation() {
493
497
  var confirmed = confirmCurrentProjectSessionActivation();
494
498
  if (!confirmed) return false;
495
- if (!store.get('pendingHomeProjectSlug')) return true;
499
+ if (!store.get('pendingHomeProjectSlug')) return !isHomeHubVisible() && !store.get('projectsHubVisible');
496
500
  return completePendingProjectActivation();
497
501
  }
498
502
 
@@ -639,7 +643,7 @@ export function handleRemoveProjectResult(msg) {
639
643
  });
640
644
  }
641
645
  }
642
- cachedProjects = cachedProjects.filter(function (p) { return p.slug !== msg.slug; });
646
+ setCachedProjects(cachedProjects.filter(function (p) { return p.slug !== msg.slug; }));
643
647
  cachedProjectCount = cachedProjects.length;
644
648
  store.set({ currentSlug: null });
645
649
  renderProjectList();
@@ -648,7 +652,9 @@ export function handleRemoveProjectResult(msg) {
648
652
  if (targetSlug) {
649
653
  switchProject(targetSlug);
650
654
  } else {
651
- showHomeHub();
655
+ store.set({ wsPath: "/ws", basePath: "/", activeProjectSlug: null });
656
+ showProjectsHub();
657
+ connect();
652
658
  }
653
659
  } else {
654
660
  showToast(msg.slug.indexOf("--") !== -1 ? "Worktree removed" : "Project removed", "success");
@@ -698,7 +704,7 @@ function switchAddProjectMode(mode) {
698
704
  }, 50);
699
705
  }
700
706
 
701
- export function openAddProjectModal() {
707
+ export function openAddProjectModal(mode) {
702
708
  addProjectModal.classList.remove("hidden");
703
709
  addProjectCreateInput.value = "";
704
710
  addProjectCloneInput.value = "";
@@ -738,6 +744,7 @@ export function openAddProjectModal() {
738
744
  existingBtn.disabled = false;
739
745
  switchAddProjectMode("existing");
740
746
  }
747
+ if (mode === "create" || mode === "clone") switchAddProjectMode(mode);
741
748
  renderRemovedProjectsList();
742
749
  }
743
750
 
@@ -855,10 +862,8 @@ export function handleAddProjectResult(msg) {
855
862
  } else {
856
863
  var toastMsg = addProjectMode === "create" ? "Project created" : addProjectMode === "clone" ? "Project cloned" : "Project added";
857
864
  showToast(toastMsg, "success");
858
- if (msg.slug) {
859
- switchProject(msg.slug);
860
- }
861
865
  }
866
+ if (msg.slug) switchProject(msg.slug);
862
867
  } else {
863
868
  addProjectError.textContent = msg.error || "Failed to add project";
864
869
  addProjectError.classList.remove("hidden");
@@ -45,6 +45,11 @@ export function addCopyHandler(msgEl, rawText) {
45
45
  export function appendDelta(text) {
46
46
  ensureAssistantBlock();
47
47
  streamBuffer += text;
48
+ if (store.get('replayingHistory')) {
49
+ store.set({ currentFullText: store.get('currentFullText') + streamBuffer });
50
+ streamBuffer = "";
51
+ return;
52
+ }
48
53
  if (!streamDrainTimer) {
49
54
  streamDrainTimer = requestAnimationFrame(drainStreamTick);
50
55
  }
@@ -55,16 +60,9 @@ function drainStreamTick() {
55
60
  var _s = store.snap();
56
61
  if (!_s.currentMsgEl || streamBuffer.length === 0) return;
57
62
 
58
- var n;
59
- var len = streamBuffer.length;
60
- if (len > 200) { n = Math.ceil(len / 4); }
61
- else if (len > 80) { n = 8; }
62
- else if (len > 30) { n = 5; }
63
- else if (len > 10) { n = 2; }
64
- else { n = 1; }
65
-
66
- var chunk = streamBuffer.slice(0, n);
67
- streamBuffer = streamBuffer.slice(n);
63
+ // Render everything received by this frame; never replay a typing backlog.
64
+ var chunk = streamBuffer;
65
+ streamBuffer = "";
68
66
  var newText = _s.currentFullText + chunk;
69
67
  store.set({ currentFullText: newText });
70
68
 
@@ -82,6 +80,14 @@ function drainStreamTick() {
82
80
  }
83
81
  }
84
82
 
83
+ export function resetStreamBuffer() {
84
+ if (streamDrainTimer) cancelAnimationFrame(streamDrainTimer);
85
+ if (highlightTimer) clearTimeout(highlightTimer);
86
+ streamDrainTimer = null;
87
+ highlightTimer = null;
88
+ streamBuffer = "";
89
+ }
90
+
85
91
  export function flushStreamBuffer() {
86
92
  if (streamDrainTimer) { cancelAnimationFrame(streamDrainTimer); streamDrainTimer = null; }
87
93
  if (streamBuffer.length > 0) {
@@ -0,0 +1,24 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+
4
+ // This control lives outside the composer so drafts and Worker locks cannot
5
+ // take away the human's ability to stop the displayed session.
6
+ export function initGenerationStop() {
7
+ var row = document.getElementById('generation-stop-row');
8
+ var button = document.getElementById('generation-stop');
9
+ if (!row || !button) return;
10
+ function sync() {
11
+ row.classList.toggle('hidden', !store.get('processing'));
12
+ button.disabled = !store.get('connected');
13
+ button.title = store.get('connected') ? 'Stop generating' : 'Reconnect to stop generating';
14
+ }
15
+ button.addEventListener('click', function () {
16
+ var ws = getWs();
17
+ if (!store.get('processing') || !store.get('connected') || !ws || ws.readyState !== 1) return;
18
+ ws.send(JSON.stringify({ type: 'stop' }));
19
+ });
20
+ store.subscribe(function (state, previous) {
21
+ if (state.processing !== previous.processing || state.connected !== previous.connected) sync();
22
+ });
23
+ sync();
24
+ }
@@ -12,13 +12,18 @@ function explicitProjectPath(pathname) {
12
12
  export function resolveHomeBootDestination(options) {
13
13
  if (!options) return "wait";
14
14
  if (options.paneMode || explicitProjectPath(options.pathname)) return "project";
15
+ if (/^\/projects\/?$/.test(options.pathname || "")) return "projects";
15
16
  // The server-selected project embedded in the root shell is authoritative.
16
17
  // Home is entered only through an explicit in-app action; reloading `/`
17
18
  // returns to the project workspace even if Home was the last open surface.
18
19
  if (options.currentSlug) return "project";
19
20
  if (options.surfaceLoaded !== true) return "wait";
20
- if (options.dockLoaded !== true) return "wait";
21
- return "home";
21
+ if (options.projectsLoaded !== true) return "wait";
22
+ if (options.surface === "home") {
23
+ if (options.dockLoaded !== true) return "wait";
24
+ return "home";
25
+ }
26
+ return "projects";
22
27
  }
23
28
 
24
29
  function finishPendingShell() {
@@ -35,6 +40,7 @@ function settleBoot(state) {
35
40
  if (resolved) return;
36
41
  var destination = resolveHomeBootDestination({
37
42
  surfaceLoaded: state.homeSurfaceLoaded,
43
+ projectsLoaded: state.projectListLoaded,
38
44
  dockLoaded: state.homeDockPreferenceLoaded,
39
45
  surface: state.homePrimarySurface,
40
46
  currentSlug: state.currentSlug,
@@ -55,7 +61,8 @@ function settleBoot(state) {
55
61
  if (input && !input.disabled) input.focus({ preventScroll: true });
56
62
  return;
57
63
  }
58
- store.set({ homeSurfaceBootResolved: true, homeSurfaceRestoreRequested: true });
64
+ if (destination === "projects") store.set({ homeSurfaceBootResolved: true, projectsHubRestoreRequested: true });
65
+ else store.set({ homeSurfaceBootResolved: true, homeSurfaceRestoreRequested: true });
59
66
  finishPendingShell();
60
67
  }
61
68
 
@@ -21,7 +21,7 @@ function normalizeSessions(value) {
21
21
  }
22
22
 
23
23
  function normalizePrimarySurface(value) {
24
- return value === "home" || value === "project" ? value : null;
24
+ return value === "home" || value === "project" || value === "projects" ? value : null;
25
25
  }
26
26
 
27
27
  function normalizeProjectSlug(value) {
@@ -51,6 +51,7 @@ export function updateHomeSurfacePreference(patch) {
51
51
  matesCollapsed: state.homeMatesCollapsed === true,
52
52
  chatScope: normalizeChatScope(state.homeChatScope),
53
53
  subSurface: normalizeSubSurface(state.homeSubSurface),
54
+ onboardingStep: state.onboardingStep || "welcome",
54
55
  };
55
56
  if (patch && Object.prototype.hasOwnProperty.call(patch, "surface")) next.surface = normalizePrimarySurface(patch.surface);
56
57
  if (patch && Object.prototype.hasOwnProperty.call(patch, "projectSlug")) next.projectSlug = normalizeProjectSlug(patch.projectSlug);
@@ -60,7 +61,12 @@ export function updateHomeSurfacePreference(patch) {
60
61
  if (patch && Object.prototype.hasOwnProperty.call(patch, "matesCollapsed")) next.matesCollapsed = patch.matesCollapsed === true;
61
62
  if (patch && Object.prototype.hasOwnProperty.call(patch, "chatScope")) next.chatScope = normalizeChatScope(patch.chatScope);
62
63
  if (patch && Object.prototype.hasOwnProperty.call(patch, "subSurface")) next.subSurface = normalizeSubSurface(patch.subSurface);
64
+ if (patch && ["welcome", "project", "complete"].indexOf(patch.onboardingStep) !== -1) {
65
+ next.onboardingStep = patch.onboardingStep;
66
+ store.set({ onboardingIntent: patch.onboardingStep });
67
+ }
63
68
  store.set({
69
+ onboardingStep: next.onboardingStep,
64
70
  homePrimarySurface: next.surface,
65
71
  homeSurfaceProjectSlug: next.projectSlug,
66
72
  homeActiveSessionByMate: normalizeSessions(next.activeSessionByMate),
@@ -78,6 +84,7 @@ export function updateHomeSurfacePreference(patch) {
78
84
  if (patch && Object.prototype.hasOwnProperty.call(patch, "matesCollapsed")) outgoing.matesCollapsed = next.matesCollapsed;
79
85
  if (patch && Object.prototype.hasOwnProperty.call(patch, "chatScope")) outgoing.chatScope = next.chatScope;
80
86
  if (patch && Object.prototype.hasOwnProperty.call(patch, "subSurface")) outgoing.subSurface = next.subSurface;
87
+ if (patch && Object.prototype.hasOwnProperty.call(patch, "onboardingStep")) outgoing.onboardingStep = next.onboardingStep;
81
88
  return send({ type: "home_surface_set", preference: outgoing });
82
89
  }
83
90
 
@@ -127,7 +134,11 @@ export function handleHomeSurfaceState(msg) {
127
134
  var savedSurface = normalizePrimarySurface(preference.surface);
128
135
  var savedProject = normalizeProjectSlug(preference.projectSlug);
129
136
  var intentConfirmed = !!intent && intent === savedSurface && intendedProject === savedProject;
137
+ var onboardingIntent = store.get('onboardingIntent');
138
+ var savedStep = ["welcome", "project", "complete"].indexOf(preference.onboardingStep) !== -1 ? preference.onboardingStep : "welcome";
130
139
  var update = {
140
+ onboardingStep: onboardingIntent || savedStep,
141
+ onboardingIntent: onboardingIntent === savedStep ? null : onboardingIntent,
131
142
  homeSurfaceLoaded: true,
132
143
  homePrimarySurface: intent || savedSurface,
133
144
  homeSurfaceProjectSlug: intent ? intendedProject : savedProject,
@@ -142,5 +153,6 @@ export function handleHomeSurfaceState(msg) {
142
153
  update.homePreferredMateId = store.get('homePreferredMateId') || preference.activeMateId || store.get('homeChatMateId') || null;
143
154
  }
144
155
  store.set(update);
156
+ if (onboardingIntent && onboardingIntent !== savedStep) send({ type: "home_surface_set", preference: { onboardingStep: onboardingIntent } });
145
157
  if (intent && !intentConfirmed) send({ type: "home_surface_set", preference: { surface: intent, projectSlug: intendedProject } });
146
158
  }
@@ -0,0 +1,62 @@
1
+ import { escapeHtml } from './utils.js';
2
+ import { iconHtml } from './icons.js';
3
+
4
+ export function ordinaryProjects(projects) {
5
+ return (Array.isArray(projects) ? projects : []).filter(function (project) {
6
+ return project && !project.isMate && project.slug;
7
+ });
8
+ }
9
+
10
+ function button(action, label, extra) {
11
+ return '<button type="button" class="projects-button ' + (extra || '') + '" data-projects-action="' + action + '">' + label + '</button>';
12
+ }
13
+
14
+ function setupAction(mode, icon, title, text, disabled) {
15
+ return '<button type="button" class="projects-setup-action" data-projects-mode="' + mode + '"' + (disabled ? ' disabled' : '') + '>' +
16
+ '<span class="projects-action-icon">' + iconHtml(icon) + '</span><span><strong>' + title + '</strong><span>' + text + '</span></span>' + iconHtml('arrow-up-right') + '</button>';
17
+ }
18
+
19
+ export function projectsHubContent(state) {
20
+ var projects = ordinaryProjects(state.projectsHubList);
21
+ var ready = state.projectListLoaded && state.homeSurfaceLoaded;
22
+ var welcome = ready && (state.projectsShowGuide || (!projects.length && (state.onboardingStep || 'welcome') === 'welcome'));
23
+ var allowed = state.projectsAccessLoaded && (!state.permissions || state.permissions.createProject === true);
24
+ var available = allowed && state.connected && !state.pendingHomeProjectSlug;
25
+ var html = '<header class="projects-header"><a class="projects-brand" href="/" data-projects-action="home"><img src="/clay-studio-symbol.png" alt="">Clay Studio</a>' +
26
+ '<nav aria-label="Workspace navigation">' + button('home', 'Home', 'projects-button-quiet') + button('settings', 'Settings', 'projects-button-quiet') + '</nav></header>';
27
+ html += '<main class="projects-main" aria-labelledby="projects-title">';
28
+ 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
+ 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 / HOME</span><h2>Make room to think.</h2><p>Talk with Clay and your Mates, explore ideas, and return to your projects whenever you’re ready.</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') +
36
+ button('skip', 'Explore Home first', 'projects-button-quiet') + '</div>' +
37
+ '<p class="projects-footnote">You can revisit this guide from Projects at any time.</p></section></main>';
38
+ }
39
+ 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>' +
40
+ '<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>';
41
+ if (state.pendingHomeProjectSlug) html += '<p class="projects-status" role="status">Opening your project…</p>';
42
+ if (projects.length) {
43
+ html += '<div class="projects-list" aria-label="Your projects">';
44
+ projects.forEach(function (project) {
45
+ html += '<button type="button" class="projects-list-item" data-project-slug="' + escapeHtml(project.slug) + '"' + (!state.connected || state.pendingHomeProjectSlug ? ' disabled' : '') + '>' +
46
+ '<span class="projects-action-icon">' + iconHtml(project.isWorktree ? 'git-branch' : 'folder') + '</span><span><strong>' + escapeHtml(project.title || project.project || project.slug) + '</strong>' +
47
+ '<span>' + escapeHtml(project.branch || project.path || project.slug) + '</span></span>' + iconHtml('arrow-right') + '</button>';
48
+ });
49
+ html += '</div><h2 class="projects-section-title">Add a project</h2>';
50
+ }
51
+ if (!state.projectsAccessLoaded) html += '<p class="projects-status" role="status">Checking project access…</p>';
52
+ else if (!allowed) html += '<p class="projects-status">Ask an administrator to create or share a project with you.</p>';
53
+ else {
54
+ html += '<div class="projects-setup-actions">' +
55
+ setupAction('existing', 'folder-open', 'Open a folder', 'Use a directory on the machine running Clay.', !available) +
56
+ setupAction('create', 'plus', 'Create a project', 'Start in a new, empty project directory.', !available) +
57
+ setupAction('clone', 'git-branch', 'Clone a repository', 'Bring an existing Git repository into Clay.', !available) + '</div>';
58
+ 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>';
59
+ }
60
+ if (!state.connected) html += '<p class="projects-status" role="status">Reconnecting… Project actions will be available when the connection returns.</p>';
61
+ return html + '</main><footer class="projects-footer">Your projects, conversations, and tools. One workspace.</footer>';
62
+ }