clay-server 4.7.0-beta.1 → 4.7.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/project-connection.js +0 -14
- package/lib/project-sessions.js +0 -18
- package/lib/public/app.js +14 -14
- package/lib/public/css/pane.css +0 -1
- package/lib/public/css/projects-hub.css +75 -0
- package/lib/public/css/tui-attention.css +0 -407
- package/lib/public/index.html +3 -14
- package/lib/public/modules/app-home-hub.js +8 -3
- package/lib/public/modules/app-messages.js +0 -15
- package/lib/public/modules/app-projects.js +16 -11
- package/lib/public/modules/home-surface-boot.js +10 -3
- package/lib/public/modules/home-surface.js +13 -1
- package/lib/public/modules/projects-hub-view.js +62 -0
- package/lib/public/modules/projects-hub.js +105 -0
- package/lib/public/style.css +2 -0
- package/lib/server.js +1 -1
- package/lib/users-home-surface-preferences.js +4 -2
- package/lib/users-preferences.js +0 -36
- package/lib/users.js +0 -4
- package/lib/ws-schema.js +0 -3
- package/package.json +1 -1
- package/lib/public/modules/whats-new-article.js +0 -70
- package/lib/public/modules/whats-new.js +0 -264
- package/lib/whats-new-content.js +0 -84
- package/lib/whats-new.js +0 -57
|
@@ -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.
|
|
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",
|
|
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 (!
|
|
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 });
|
|
@@ -69,8 +69,6 @@ import { clearPairResultStatus, handlePairResultStatus } from './pair-result-sta
|
|
|
69
69
|
import { scrollToBottom, addToMessages, addUserMessage, addSystemMessage, removeMatePreThinking, 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';
|
|
@@ -837,7 +835,6 @@ export function processMessage(msg) {
|
|
|
837
835
|
case "session_switched":
|
|
838
836
|
clearPairResultStatus();
|
|
839
837
|
syncAutonomousRunForSession(msg.autonomousRun || null);
|
|
840
|
-
closeWhatsNewArticle();
|
|
841
838
|
// Save draft from outgoing session
|
|
842
839
|
var _prevSid = store.get('activeSessionId');
|
|
843
840
|
if (_prevSid && inputEl.value) {
|
|
@@ -2127,18 +2124,6 @@ export function processMessage(msg) {
|
|
|
2127
2124
|
handleAutoContinueChanged(msg);
|
|
2128
2125
|
break;
|
|
2129
2126
|
|
|
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
2127
|
case "set_claude_open_mode_result":
|
|
2143
2128
|
case "claude_open_mode_changed":
|
|
2144
2129
|
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
|
|
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
|
|
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
|
-
|
|
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");
|
|
@@ -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.
|
|
21
|
-
|
|
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,
|
|
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
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { hideHomeHub, showHomeHub } from './app-home-hub.js';
|
|
4
|
+
import { openAddProjectModal, switchProject } from './app-projects.js';
|
|
5
|
+
import { openUserSettings } from './user-settings.js';
|
|
6
|
+
import { rememberHomePrimarySurface, updateHomeSurfacePreference } from './home-surface.js';
|
|
7
|
+
import { projectsHubContent } from './projects-hub-view.js';
|
|
8
|
+
import { refreshIcons } from './icons.js';
|
|
9
|
+
|
|
10
|
+
function root() { return document.getElementById('projects-hub'); }
|
|
11
|
+
|
|
12
|
+
function render() {
|
|
13
|
+
var element = root();
|
|
14
|
+
if (!element || !store.get('projectsHubVisible')) return;
|
|
15
|
+
var html = projectsHubContent(store.snap());
|
|
16
|
+
if (store.get('projectsHubMarkup') === html) return;
|
|
17
|
+
store.set({ projectsHubMarkup: html });
|
|
18
|
+
var active = element.contains(document.activeElement) ? document.activeElement : null;
|
|
19
|
+
var action = active && (active.dataset.projectsAction || active.dataset.projectsMode || active.dataset.projectSlug);
|
|
20
|
+
element.innerHTML = html;
|
|
21
|
+
refreshIcons();
|
|
22
|
+
if (action) {
|
|
23
|
+
var buttons = element.querySelectorAll('button, a');
|
|
24
|
+
for (var i = 0; i < buttons.length; i++) {
|
|
25
|
+
if ((buttons[i].dataset.projectsAction || buttons[i].dataset.projectsMode || buttons[i].dataset.projectSlug) === action && !buttons[i].disabled) {
|
|
26
|
+
buttons[i].focus({ preventScroll: true });
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function focusHeading() {
|
|
34
|
+
if (root()) root().scrollTop = 0;
|
|
35
|
+
var title = document.getElementById('projects-title');
|
|
36
|
+
if (title) title.focus({ preventScroll: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function showProjectsHub(fromHistory) {
|
|
40
|
+
hideHomeHub();
|
|
41
|
+
store.set({ projectsHubVisible: true, pendingHomeProjectSlug: null });
|
|
42
|
+
document.body.classList.add('projects-active');
|
|
43
|
+
root().classList.remove('hidden');
|
|
44
|
+
rememberHomePrimarySurface('projects');
|
|
45
|
+
if (!fromHistory && location.pathname !== '/projects') {
|
|
46
|
+
var method = document.documentElement.classList.contains('pwa-standalone') ? 'replaceState' : 'pushState';
|
|
47
|
+
history[method](null, '', '/projects');
|
|
48
|
+
}
|
|
49
|
+
render();
|
|
50
|
+
focusHeading();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function hideProjectsHub() {
|
|
54
|
+
if (!store.get('projectsHubVisible')) return;
|
|
55
|
+
store.set({ projectsHubVisible: false, projectsShowGuide: false });
|
|
56
|
+
document.body.classList.remove('projects-active');
|
|
57
|
+
if (root()) root().classList.add('hidden');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function completeProjectsOnboarding() {
|
|
61
|
+
if (store.get('onboardingStep') !== 'project') return;
|
|
62
|
+
updateHomeSurfacePreference({ onboardingStep: 'complete' });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function handleClick(event) {
|
|
66
|
+
var button = event.target.closest('[data-projects-action], [data-projects-mode], [data-project-slug]');
|
|
67
|
+
if (!button || button.disabled) return;
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
var action = button.dataset.projectsAction;
|
|
70
|
+
if (action === 'home') { showHomeHub(); return; }
|
|
71
|
+
if (action === 'settings') { openUserSettings(); return; }
|
|
72
|
+
if (action === 'guide') { store.set({ projectsShowGuide: true }); render(); focusHeading(); return; }
|
|
73
|
+
if (action === 'start' || action === 'skip') {
|
|
74
|
+
store.set({ projectsShowGuide: false });
|
|
75
|
+
updateHomeSurfacePreference({ onboardingStep: 'project' });
|
|
76
|
+
if (action === 'skip') showHomeHub();
|
|
77
|
+
else { render(); focusHeading(); }
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
var ws = getWs();
|
|
81
|
+
if (!ws || ws.readyState !== 1) return;
|
|
82
|
+
if (button.dataset.projectSlug) { switchProject(button.dataset.projectSlug); return; }
|
|
83
|
+
if (button.dataset.projectsMode) {
|
|
84
|
+
var permissions = store.get('permissions');
|
|
85
|
+
if (!store.get('projectsAccessLoaded') || (permissions && permissions.createProject !== true)) return;
|
|
86
|
+
openAddProjectModal(button.dataset.projectsMode);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function initProjectsHub() {
|
|
91
|
+
root().addEventListener('click', handleClick);
|
|
92
|
+
store.subscribe(function (state, previous) {
|
|
93
|
+
if (state.projectsHubRestoreRequested && !previous.projectsHubRestoreRequested) {
|
|
94
|
+
showProjectsHub(true);
|
|
95
|
+
if (location.pathname === '/') history.replaceState(null, '', '/projects');
|
|
96
|
+
store.set({ projectsHubRestoreRequested: false });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (!state.projectsHubVisible) return;
|
|
100
|
+
if (state.projectsHubList !== previous.projectsHubList || state.projectListLoaded !== previous.projectListLoaded ||
|
|
101
|
+
state.homeSurfaceLoaded !== previous.homeSurfaceLoaded || state.onboardingStep !== previous.onboardingStep ||
|
|
102
|
+
state.projectsAccessLoaded !== previous.projectsAccessLoaded || state.permissions !== previous.permissions ||
|
|
103
|
+
state.connected !== previous.connected || state.pendingHomeProjectSlug !== previous.pendingHomeProjectSlug) render();
|
|
104
|
+
});
|
|
105
|
+
}
|
package/lib/public/style.css
CHANGED
package/lib/server.js
CHANGED
|
@@ -814,7 +814,7 @@ function createServer(opts) {
|
|
|
814
814
|
|
|
815
815
|
// Root path — render the default project workspace while keeping Home
|
|
816
816
|
// available as an explicit client-side surface.
|
|
817
|
-
if (fullUrl === "/" && req.method === "GET") {
|
|
817
|
+
if ((fullUrl === "/" || fullUrl === "/projects" || fullUrl === "/projects/") && req.method === "GET") {
|
|
818
818
|
if (!isRequestAuthed(req)) {
|
|
819
819
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
820
820
|
res.end(auth.getAuthPage());
|
|
@@ -31,8 +31,8 @@ function attachHomeSurfacePreferences(deps) {
|
|
|
31
31
|
if (!validId(mateIds[i]) || !validSessionReference(sessions[mateIds[i]])) continue;
|
|
32
32
|
normalizedSessions[mateIds[i]] = sessions[mateIds[i]];
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
surface: source.surface === "home" || source.surface === "project" ? source.surface : null,
|
|
34
|
+
var result = {
|
|
35
|
+
surface: source.surface === "home" || source.surface === "project" || source.surface === "projects" ? source.surface : null,
|
|
36
36
|
projectSlug: validProjectSlug(source.projectSlug) ? source.projectSlug : null,
|
|
37
37
|
activeMateId: validId(source.activeMateId) ? source.activeMateId : null,
|
|
38
38
|
activeSessionByMate: normalizedSessions,
|
|
@@ -41,6 +41,8 @@ function attachHomeSurfacePreferences(deps) {
|
|
|
41
41
|
chatScope: source.chatScope === "current" ? "current" : "all",
|
|
42
42
|
subSurface: source.subSurface === "debates" ? "debates" : "chat",
|
|
43
43
|
};
|
|
44
|
+
if (["welcome", "project", "complete"].indexOf(source.onboardingStep) !== -1) result.onboardingStep = source.onboardingStep;
|
|
45
|
+
return result;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
function loadSingleUserPreference() {
|
package/lib/users-preferences.js
CHANGED
|
@@ -484,40 +484,6 @@ function attachPreferences(deps) {
|
|
|
484
484
|
return { error: "User not found" };
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
-
// --- What's New seen ids ---
|
|
488
|
-
//
|
|
489
|
-
// Per-user list of dismissed "What's New" entry ids. The whats-new
|
|
490
|
-
// service uses this to filter unseen entries on connect. Ids are stable
|
|
491
|
-
// strings authored in lib/whats-new-content.js.
|
|
492
|
-
|
|
493
|
-
function getWhatsNewSeenIds(userId) {
|
|
494
|
-
var data = loadUsers();
|
|
495
|
-
for (var i = 0; i < data.users.length; i++) {
|
|
496
|
-
if (data.users[i].id === userId) {
|
|
497
|
-
var ids = data.users[i].whatsNewSeenIds;
|
|
498
|
-
return Array.isArray(ids) ? ids.slice() : [];
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
return [];
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function markWhatsNewSeen(userId, entryId) {
|
|
505
|
-
if (!entryId || typeof entryId !== "string") return { error: "bad_id" };
|
|
506
|
-
var data = loadUsers();
|
|
507
|
-
for (var i = 0; i < data.users.length; i++) {
|
|
508
|
-
if (data.users[i].id === userId) {
|
|
509
|
-
var ids = Array.isArray(data.users[i].whatsNewSeenIds) ? data.users[i].whatsNewSeenIds.slice() : [];
|
|
510
|
-
if (ids.indexOf(entryId) === -1) {
|
|
511
|
-
ids.push(entryId);
|
|
512
|
-
data.users[i].whatsNewSeenIds = ids;
|
|
513
|
-
saveUsers(data);
|
|
514
|
-
}
|
|
515
|
-
return { ok: true, seenIds: ids };
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
return { error: "User not found" };
|
|
519
|
-
}
|
|
520
|
-
|
|
521
487
|
// --- Mate onboarding ---
|
|
522
488
|
|
|
523
489
|
function setMateOnboarded(userId) {
|
|
@@ -556,8 +522,6 @@ function attachPreferences(deps) {
|
|
|
556
522
|
setMatesEnabled: setMatesEnabled,
|
|
557
523
|
getToolPalettes: getToolPalettes,
|
|
558
524
|
setToolPalette: setToolPalette,
|
|
559
|
-
getWhatsNewSeenIds: getWhatsNewSeenIds,
|
|
560
|
-
markWhatsNewSeen: markWhatsNewSeen,
|
|
561
525
|
getTerminalFont: getTerminalFont,
|
|
562
526
|
setTerminalFont: setTerminalFont,
|
|
563
527
|
setMateOnboarded: setMateOnboarded,
|
package/lib/users.js
CHANGED
|
@@ -453,8 +453,6 @@ var getMatesEnabled = preferences.getMatesEnabled;
|
|
|
453
453
|
var setMatesEnabled = preferences.setMatesEnabled;
|
|
454
454
|
var getToolPalettes = preferences.getToolPalettes;
|
|
455
455
|
var setToolPalette = preferences.setToolPalette;
|
|
456
|
-
var getWhatsNewSeenIds = preferences.getWhatsNewSeenIds;
|
|
457
|
-
var markWhatsNewSeen = preferences.markWhatsNewSeen;
|
|
458
456
|
var getTerminalFont = preferences.getTerminalFont;
|
|
459
457
|
var setTerminalFont = preferences.setTerminalFont;
|
|
460
458
|
var setMateOnboarded = preferences.setMateOnboarded;
|
|
@@ -541,8 +539,6 @@ module.exports = {
|
|
|
541
539
|
setCapsulesEnabled: setCapsulesEnabled,
|
|
542
540
|
getToolPalettes: getToolPalettes,
|
|
543
541
|
setToolPalette: setToolPalette,
|
|
544
|
-
getWhatsNewSeenIds: getWhatsNewSeenIds,
|
|
545
|
-
markWhatsNewSeen: markWhatsNewSeen,
|
|
546
542
|
getTerminalFont: getTerminalFont,
|
|
547
543
|
setTerminalFont: setTerminalFont,
|
|
548
544
|
getHomeDockPreference: getHomeDockPreference,
|
package/lib/ws-schema.js
CHANGED
|
@@ -332,9 +332,6 @@ var schema = {
|
|
|
332
332
|
// -----------------------------------------------------------------------
|
|
333
333
|
// What's New
|
|
334
334
|
// -----------------------------------------------------------------------
|
|
335
|
-
"whats_new_state": { direction: "s2c", handler: "lib/public/modules/whats-new.js", description: "Unseen What's New entries for the connecting user" },
|
|
336
|
-
"whats_new_seen": { direction: "c2s", handler: "lib/project-sessions.js", description: "Mark a What's New entry as dismissed by the current user" },
|
|
337
|
-
"whats_new_seen_result": { direction: "s2c", handler: "lib/public/modules/whats-new.js", description: "Result of marking a What's New entry seen" },
|
|
338
335
|
"home_dock_get": { direction: "c2s", handler: "lib/server-home-preferences.js", description: "Request the current user's home dock preference" },
|
|
339
336
|
"home_dock_set": { direction: "c2s", handler: "lib/server-home-preferences.js", description: "Persist the current user's home dock preference" },
|
|
340
337
|
"home_dock_state": { direction: "s2c", handler: "lib/public/modules/home-dock.js", description: "Current user home dock preference" },
|
package/package.json
CHANGED