groove-dev 0.27.87 → 0.27.88

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 (53) hide show
  1. package/CLAUDE.md +3 -2
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +115 -7
  5. package/node_modules/@groove-dev/daemon/src/conversations.js +29 -3
  6. package/node_modules/@groove-dev/daemon/src/providers/codex.js +28 -10
  7. package/node_modules/@groove-dev/daemon/src/registry.js +30 -0
  8. package/node_modules/@groove-dev/daemon/src/validate.js +23 -0
  9. package/node_modules/@groove-dev/gui/dist/assets/index-BSqk8cbI.css +1 -0
  10. package/node_modules/@groove-dev/gui/dist/assets/index-B_igwWvq.js +8642 -0
  11. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  12. package/node_modules/@groove-dev/gui/package.json +1 -1
  13. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +254 -0
  14. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +177 -0
  15. package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +148 -0
  16. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +377 -0
  17. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +117 -40
  18. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +10 -13
  19. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +26 -1
  20. package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +14 -14
  21. package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +5 -0
  22. package/node_modules/@groove-dev/gui/src/stores/groove.js +132 -1
  23. package/node_modules/@groove-dev/gui/src/views/agents.jsx +22 -3
  24. package/package.json +1 -1
  25. package/packages/cli/package.json +1 -1
  26. package/packages/daemon/package.json +1 -1
  27. package/packages/daemon/src/api.js +115 -7
  28. package/packages/daemon/src/conversations.js +29 -3
  29. package/packages/daemon/src/providers/codex.js +28 -10
  30. package/packages/daemon/src/registry.js +30 -0
  31. package/packages/daemon/src/validate.js +23 -0
  32. package/packages/gui/dist/assets/index-BSqk8cbI.css +1 -0
  33. package/packages/gui/dist/assets/index-B_igwWvq.js +8642 -0
  34. package/packages/gui/dist/index.html +2 -2
  35. package/packages/gui/package.json +1 -1
  36. package/packages/gui/src/components/agents/agent-file-tree.jsx +254 -0
  37. package/packages/gui/src/components/agents/code-review.jsx +177 -0
  38. package/packages/gui/src/components/agents/diff-viewer.jsx +148 -0
  39. package/packages/gui/src/components/agents/workspace-mode.jsx +377 -0
  40. package/packages/gui/src/components/chat/chat-input.jsx +117 -40
  41. package/packages/gui/src/components/chat/chat-messages.jsx +10 -13
  42. package/packages/gui/src/components/chat/chat-view.jsx +26 -1
  43. package/packages/gui/src/components/chat/conversation-list.jsx +14 -14
  44. package/packages/gui/src/components/chat/model-picker.jsx +5 -0
  45. package/packages/gui/src/stores/groove.js +132 -1
  46. package/packages/gui/src/views/agents.jsx +22 -3
  47. package/test/doomsday-clock/index.html +55 -0
  48. package/test/doomsday-clock/script.js +66 -0
  49. package/test/doomsday-clock/style.css +315 -0
  50. package/node_modules/@groove-dev/gui/dist/assets/index-BCQY8ojz.css +0 -1
  51. package/node_modules/@groove-dev/gui/dist/assets/index-C5e7KVGN.js +0 -8637
  52. package/packages/gui/dist/assets/index-BCQY8ojz.css +0 -1
  53. package/packages/gui/dist/assets/index-C5e7KVGN.js +0 -8637
@@ -45,12 +45,20 @@ export function ChatView() {
45
45
  const conversationRoles = useGrooveStore((s) => s.conversationRoles);
46
46
  const setConversationRole = useGrooveStore((s) => s.setConversationRole);
47
47
 
48
+ const conversationReasoningEffort = useGrooveStore((s) => s.conversationReasoningEffort);
49
+ const setConversationReasoningEffort = useGrooveStore((s) => s.setConversationReasoningEffort);
50
+ const conversationVerbosity = useGrooveStore((s) => s.conversationVerbosity);
51
+ const setConversationVerbosity = useGrooveStore((s) => s.setConversationVerbosity);
52
+
48
53
  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
49
54
  const [replyContext, setReplyContext] = useState(null);
50
55
 
51
56
  const activeRole = activeConversationId ? (conversationRoles?.[activeConversationId] || null) : null;
57
+ const activeReasoningEffort = activeConversationId ? (conversationReasoningEffort?.[activeConversationId] || 'medium') : 'medium';
58
+ const activeVerbosity = activeConversationId ? (conversationVerbosity?.[activeConversationId] || 'medium') : 'medium';
52
59
 
53
60
  const activeConversation = conversations.find((c) => c.id === activeConversationId) || null;
61
+ const isCodexProvider = activeConversation?.provider === 'codex';
54
62
  const messages = activeConversationId ? (conversationMessages[activeConversationId] || []) : [];
55
63
  const isStreaming = streamingConversationId === activeConversationId && sendingMessage;
56
64
  const currentModelIsImage = activeConversation ? isImageModel(activeConversation.model) : false;
@@ -108,6 +116,16 @@ export function ChatView() {
108
116
  setReplyContext(msg);
109
117
  }, []);
110
118
 
119
+ const handleReasoningEffortChange = useCallback((effort) => {
120
+ if (!activeConversationId) return;
121
+ setConversationReasoningEffort(activeConversationId, effort);
122
+ }, [activeConversationId, setConversationReasoningEffort]);
123
+
124
+ const handleVerbosityChange = useCallback((verbosity) => {
125
+ if (!activeConversationId) return;
126
+ setConversationVerbosity(activeConversationId, verbosity);
127
+ }, [activeConversationId, setConversationVerbosity]);
128
+
111
129
  const currentModel = activeConversation
112
130
  ? { provider: activeConversation.provider, model: activeConversation.model }
113
131
  : null;
@@ -116,7 +134,7 @@ export function ChatView() {
116
134
  <div className="flex h-full bg-surface-0">
117
135
  {/* Conversation sidebar */}
118
136
  <div className={cn(
119
- 'flex-shrink-0 border-r border-accent/12 bg-surface-1 transition-all duration-200 overflow-hidden',
137
+ 'flex-shrink-0 border-r border-accent/12 bg-accent transition-all duration-200 overflow-hidden',
120
138
  sidebarCollapsed ? 'w-0' : 'w-64',
121
139
  )}>
122
140
  <ConversationList onNewChat={() => handleNewChat()} />
@@ -146,6 +164,13 @@ export function ChatView() {
146
164
  replyContext={replyContext}
147
165
  onClearReply={() => setReplyContext(null)}
148
166
  role={activeRole}
167
+ isCodex={isCodexProvider}
168
+ reasoningEffort={activeReasoningEffort}
169
+ onReasoningEffortChange={handleReasoningEffortChange}
170
+ verbosity={activeVerbosity}
171
+ onVerbosityChange={handleVerbosityChange}
172
+ mode={activeConversation?.mode || 'api'}
173
+ onModeChange={handleModeChange}
149
174
  />
150
175
  </>
151
176
  ) : (
@@ -31,7 +31,7 @@ function groupByDate(conversations) {
31
31
  function GroupLabel({ label }) {
32
32
  return (
33
33
  <div className="px-3 pt-4 pb-1.5">
34
- <span className="text-2xs font-semibold text-text-4 uppercase tracking-wider font-sans">{label}</span>
34
+ <span className="text-2xs font-semibold text-white/60 uppercase tracking-wider font-sans">{label}</span>
35
35
  </div>
36
36
  );
37
37
  }
@@ -45,23 +45,23 @@ function ConversationItem({ conv, isActive, onSelect, onRename, onPin, onDelete
45
45
  className={cn(
46
46
  'w-full flex items-center gap-2 px-3 py-2 text-left rounded-md transition-colors cursor-pointer group',
47
47
  isActive
48
- ? 'bg-accent/10 text-text-0'
49
- : 'text-text-2 hover:bg-surface-4 hover:text-text-1',
48
+ ? 'bg-white/15 text-white'
49
+ : 'text-white/80 hover:bg-white/10 hover:text-white',
50
50
  )}
51
51
  >
52
- <MessageCircle size={13} className={cn('flex-shrink-0', isActive ? 'text-accent' : 'text-text-4 group-hover:text-text-3')} />
52
+ <MessageCircle size={13} className={cn('flex-shrink-0', isActive ? 'text-white' : 'text-white/50 group-hover:text-white/70')} />
53
53
  <div className="flex-1 min-w-0">
54
- <div className="text-xs font-medium font-sans truncate">{conv.title || 'New Chat'}</div>
54
+ <div className="text-xs font-medium font-sans truncate text-white">{conv.title || 'New Chat'}</div>
55
55
  <div className="flex items-center gap-1.5 mt-0.5">
56
56
  {conv.mode === 'agent'
57
- ? <Bot size={9} className="text-purple flex-shrink-0" />
58
- : <Zap size={9} className="text-accent flex-shrink-0" />
57
+ ? <Bot size={9} className="text-white/70 flex-shrink-0" />
58
+ : <Zap size={9} className="text-white/70 flex-shrink-0" />
59
59
  }
60
60
  {conv.model && <Badge variant="default" className="text-[8px] px-1 py-0">{formatModelName(conv.model)}</Badge>}
61
- <span className="text-2xs text-text-4 font-sans">{timeAgo(conv.updatedAt || conv.createdAt)}</span>
61
+ <span className="text-2xs text-white/50 font-sans">{timeAgo(conv.updatedAt || conv.createdAt)}</span>
62
62
  </div>
63
63
  </div>
64
- {conv.pinned && <Pin size={10} className="text-accent flex-shrink-0" />}
64
+ {conv.pinned && <Pin size={10} className="text-white flex-shrink-0" />}
65
65
  </button>
66
66
  </ContextMenuTrigger>
67
67
  <ContextMenuContent>
@@ -125,9 +125,9 @@ export function ConversationList({ onNewChat }) {
125
125
  <div className="flex-1 overflow-y-auto px-1.5 pt-3 pb-3 space-y-0.5">
126
126
  {conversations.length === 0 ? (
127
127
  <div className="flex flex-col items-center justify-center py-16 text-center px-4">
128
- <MessageCircle size={24} className="text-text-4 mb-3" />
129
- <p className="text-xs text-text-3 font-sans">No conversations yet</p>
130
- <p className="text-2xs text-text-4 font-sans mt-1">Start a new chat to begin</p>
128
+ <MessageCircle size={24} className="text-white/50 mb-3" />
129
+ <p className="text-xs text-white/80 font-sans">No conversations yet</p>
130
+ <p className="text-2xs text-white/60 font-sans mt-1">Start a new chat to begin</p>
131
131
  </div>
132
132
  ) : (
133
133
  <>
@@ -140,10 +140,10 @@ export function ConversationList({ onNewChat }) {
140
140
  )}
141
141
  </div>
142
142
 
143
- <div className="p-3 border-t border-border-subtle">
143
+ <div className="p-3 border-t border-white/15">
144
144
  <button
145
145
  onClick={onNewChat}
146
- className="w-full flex items-center justify-center gap-2 h-9 rounded-lg bg-accent/15 text-accent text-xs font-semibold font-sans hover:bg-accent/25 transition-colors cursor-pointer border border-accent/20"
146
+ className="w-full flex items-center justify-center gap-2 h-9 rounded-lg bg-white/15 text-white text-xs font-semibold font-sans hover:bg-white/25 transition-colors cursor-pointer border border-white/20"
147
147
  >
148
148
  <Plus size={14} />
149
149
  New Chat
@@ -23,6 +23,10 @@ const TIER_CONFIG = {
23
23
 
24
24
  function getTier(model) {
25
25
  const name = (model || '').toLowerCase();
26
+ if (name.includes('gpt-5.5') || name.includes('gpt-5.4-pro')) return 'frontier';
27
+ if (name.includes('gpt-5.4-mini') || name.includes('gpt-5-mini')) return 'mid';
28
+ if (name.includes('gpt-5.4-nano') || name.includes('gpt-5-nano')) return 'fast';
29
+ if (name.includes('gpt-5.4')) return 'frontier';
26
30
  if (name.includes('opus') || name.includes('pro') || name.includes('o3') || name.includes('gpt-4o')) return 'frontier';
27
31
  if (name.includes('sonnet') || name.includes('flash') || name.includes('o4-mini')) return 'mid';
28
32
  return 'fast';
@@ -30,6 +34,7 @@ function getTier(model) {
30
34
 
31
35
  function getContextSize(model) {
32
36
  const name = (model || '').toLowerCase();
37
+ if (name.startsWith('gpt-5')) return '200k';
33
38
  if (name.includes('opus') || name.includes('sonnet')) return '200k';
34
39
  if (name.includes('haiku')) return '200k';
35
40
  if (name.includes('pro')) return '1M';
@@ -92,6 +92,8 @@ export const useGrooveStore = create((set, get) => ({
92
92
  sendingMessage: false,
93
93
  streamingConversationId: null,
94
94
  conversationRoles: loadJSON('groove:conversationRoles'),
95
+ conversationReasoningEffort: loadJSON('groove:conversationReasoningEffort'),
96
+ conversationVerbosity: loadJSON('groove:conversationVerbosity'),
95
97
 
96
98
  // ── Approvals ─────────────────────────────────────────────
97
99
  pendingApprovals: [],
@@ -173,6 +175,13 @@ export const useGrooveStore = create((set, get) => ({
173
175
  editorRecentSaves: {},
174
176
  editorSidebarWidth: Number(localStorage.getItem('groove:editorSidebarWidth')) || 240,
175
177
 
178
+ // ── Workspace Mode ────────────────────────────────────────
179
+ workspaceMode: localStorage.getItem('groove:workspaceMode') === 'true',
180
+ workspaceAgentId: null,
181
+ workspaceSnapshots: {},
182
+ workspaceReviewMode: false,
183
+ workspaceReviewFiles: [],
184
+
176
185
  // ── Onboarding ────────────────────────────────────────────
177
186
  onboardingComplete: localStorage.getItem('groove:onboardingComplete') === 'true',
178
187
 
@@ -451,6 +460,15 @@ export const useGrooveStore = create((set, get) => ({
451
460
  if (msg.error && msg.agentId) {
452
461
  get().addChatMessage(msg.agentId, 'system', `Crashed: ${msg.error}`);
453
462
  }
463
+ // Clear workspace if the exiting agent was the workspace target
464
+ if (get().workspaceAgentId === msg.agentId) {
465
+ const teamAgents = get().agents.filter(
466
+ (a) => a.id !== msg.agentId && a.teamId === get().activeTeamId,
467
+ );
468
+ const next = teamAgents.find((a) => a.status === 'running') || teamAgents[0];
469
+ set({ workspaceAgentId: next?.id || null });
470
+ }
471
+
454
472
  // Check for recommended team when planner completes
455
473
  if (agent?.role === 'planner' && msg.status === 'completed') {
456
474
  setTimeout(() => get().checkRecommendedTeam(), 1000);
@@ -567,6 +585,13 @@ export const useGrooveStore = create((set, get) => ({
567
585
  const savedAt = get().editorRecentSaves[msg.path];
568
586
  if (savedAt && Date.now() - savedAt < 2000) break;
569
587
  set((s) => ({ editorChangedFiles: { ...s.editorChangedFiles, [msg.path]: msg.timestamp } }));
588
+ // Auto-capture workspace snapshot for diff viewer
589
+ if (get().workspaceMode && msg.path && !get().workspaceSnapshots[msg.path]) {
590
+ const existing = get().editorFiles[msg.path];
591
+ if (existing?.content) {
592
+ get().captureSnapshot(msg.path, existing.content);
593
+ }
594
+ }
570
595
  break;
571
596
  }
572
597
 
@@ -2050,7 +2075,13 @@ export const useGrooveStore = create((set, get) => ({
2050
2075
  const conversationRoles = { ...s.conversationRoles };
2051
2076
  delete conversationRoles[id];
2052
2077
  persistJSON('groove:conversationRoles', conversationRoles);
2053
- return { conversations, conversationMessages, conversationRoles, activeConversationId };
2078
+ const conversationReasoningEffort = { ...s.conversationReasoningEffort };
2079
+ delete conversationReasoningEffort[id];
2080
+ persistJSON('groove:conversationReasoningEffort', conversationReasoningEffort);
2081
+ const conversationVerbosity = { ...s.conversationVerbosity };
2082
+ delete conversationVerbosity[id];
2083
+ persistJSON('groove:conversationVerbosity', conversationVerbosity);
2084
+ return { conversations, conversationMessages, conversationRoles, conversationReasoningEffort, conversationVerbosity, activeConversationId };
2054
2085
  });
2055
2086
  } catch (err) {
2056
2087
  get().addToast('error', 'Delete failed', err.message);
@@ -2093,6 +2124,24 @@ export const useGrooveStore = create((set, get) => ({
2093
2124
  });
2094
2125
  },
2095
2126
 
2127
+ setConversationReasoningEffort(id, effort) {
2128
+ set((s) => {
2129
+ const map = { ...s.conversationReasoningEffort };
2130
+ map[id] = effort || 'medium';
2131
+ persistJSON('groove:conversationReasoningEffort', map);
2132
+ return { conversationReasoningEffort: map };
2133
+ });
2134
+ },
2135
+
2136
+ setConversationVerbosity(id, verbosity) {
2137
+ set((s) => {
2138
+ const map = { ...s.conversationVerbosity };
2139
+ map[id] = verbosity || 'medium';
2140
+ persistJSON('groove:conversationVerbosity', map);
2141
+ return { conversationVerbosity: map };
2142
+ });
2143
+ },
2144
+
2096
2145
  async sendChatMessage(conversationId, message) {
2097
2146
  const conv = get().conversations.find((c) => c.id === conversationId);
2098
2147
  if (!conv) return;
@@ -2122,6 +2171,12 @@ export const useGrooveStore = create((set, get) => ({
2122
2171
  ...body.history,
2123
2172
  ];
2124
2173
  }
2174
+ const effort = get().conversationReasoningEffort?.[conversationId] || 'medium';
2175
+ const verbosity = get().conversationVerbosity?.[conversationId] || 'medium';
2176
+ if (conv.provider === 'codex') {
2177
+ body.reasoning_effort = effort;
2178
+ body.verbosity = verbosity;
2179
+ }
2125
2180
  await api.post(`/conversations/${encodeURIComponent(conversationId)}/message`, body);
2126
2181
  } catch (err) {
2127
2182
  set((s) => {
@@ -2303,6 +2358,82 @@ export const useGrooveStore = create((set, get) => ({
2303
2358
  }
2304
2359
  },
2305
2360
 
2361
+ // ── Workspace Mode ────────────────────────────────────────
2362
+
2363
+ setWorkspaceMode(on) {
2364
+ set({ workspaceMode: on });
2365
+ localStorage.setItem('groove:workspaceMode', String(on));
2366
+ if (on && !get().workspaceAgentId) {
2367
+ const teamAgents = get().agents.filter((a) => a.teamId === get().activeTeamId);
2368
+ const selected = get().detailPanel?.type === 'agent' ? get().detailPanel.agentId : null;
2369
+ const running = teamAgents.find((a) => a.status === 'running');
2370
+ set({ workspaceAgentId: selected || running?.id || teamAgents[0]?.id || null });
2371
+ }
2372
+ },
2373
+
2374
+ setWorkspaceAgent(id) {
2375
+ set({ workspaceAgentId: id });
2376
+ },
2377
+
2378
+ captureSnapshot(path, content) {
2379
+ set((s) => {
2380
+ if (s.workspaceSnapshots[path]) return s;
2381
+ const next = { ...s.workspaceSnapshots, [path]: content };
2382
+ const keys = Object.keys(next);
2383
+ if (keys.length > 200) {
2384
+ delete next[keys[0]];
2385
+ }
2386
+ return { workspaceSnapshots: next };
2387
+ });
2388
+ },
2389
+
2390
+ toggleReviewMode() {
2391
+ const st = get();
2392
+ if (st.workspaceReviewMode) {
2393
+ set({ workspaceReviewMode: false, workspaceReviewFiles: [] });
2394
+ return;
2395
+ }
2396
+ const agentId = st.workspaceAgentId;
2397
+ const log = st.activityLog[agentId] || [];
2398
+ const seen = new Set();
2399
+ const files = [];
2400
+ for (const entry of log) {
2401
+ const t = (entry.text || '').toLowerCase();
2402
+ if (!(t.includes('writ') || t.includes('edit') || t.includes('creat'))) continue;
2403
+ const match = entry.text.match(/(?:Write|Edit|Create|wrote|editing|writing)\S*\s+(\S+)/i);
2404
+ if (!match) continue;
2405
+ const path = match[1];
2406
+ if (seen.has(path)) continue;
2407
+ seen.add(path);
2408
+ files.push({ path, status: 'pending', comment: '' });
2409
+ }
2410
+ set({ workspaceReviewMode: true, workspaceReviewFiles: files });
2411
+ },
2412
+
2413
+ approveFile(path) {
2414
+ set((s) => ({
2415
+ workspaceReviewFiles: s.workspaceReviewFiles.map((f) =>
2416
+ f.path === path ? { ...f, status: 'approved' } : f,
2417
+ ),
2418
+ }));
2419
+ },
2420
+
2421
+ rejectFile(path) {
2422
+ set((s) => ({
2423
+ workspaceReviewFiles: s.workspaceReviewFiles.map((f) =>
2424
+ f.path === path ? { ...f, status: 'rejected' } : f,
2425
+ ),
2426
+ }));
2427
+ },
2428
+
2429
+ commentFile(path, comment) {
2430
+ set((s) => ({
2431
+ workspaceReviewFiles: s.workspaceReviewFiles.map((f) =>
2432
+ f.path === path ? { ...f, comment } : f,
2433
+ ),
2434
+ }));
2435
+ },
2436
+
2306
2437
  // ── Federation ────────────────────────────────────────────
2307
2438
 
2308
2439
  async fetchFederationStatus() {
@@ -12,6 +12,7 @@ import { Button } from '../components/ui/button';
12
12
  import { Badge } from '../components/ui/badge';
13
13
  import { Plus, Users, Zap, X, Check, Rocket, Server, Monitor, Code2, TestTube, Shield, Pencil, Copy, Trash2, ChevronDown, ChevronLeft, ChevronRight, FolderOpen, Radio, Eye } from 'lucide-react';
14
14
  import { PreviewWorkspace } from '../components/preview/preview-workspace';
15
+ import { WorkspaceMode } from '../components/agents/workspace-mode';
15
16
  import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuSeparator } from '../components/ui/context-menu';
16
17
 
17
18
  const NODE_TYPES = { agentNode: AgentNode, rootNode: RootNode };
@@ -799,6 +800,8 @@ export default function AgentsView() {
799
800
  const showPreviewInAgents = useGrooveStore((s) => s.showPreviewInAgents);
800
801
  const previewState = useGrooveStore((s) => s.previewState);
801
802
  const togglePreviewInAgents = useGrooveStore((s) => s.togglePreviewInAgents);
803
+ const workspaceMode = useGrooveStore((s) => s.workspaceMode);
804
+ const setWorkspaceMode = useGrooveStore((s) => s.setWorkspaceMode);
802
805
 
803
806
  // Poll for recommended team while a planner is running
804
807
  useEffect(() => {
@@ -849,6 +852,8 @@ export default function AgentsView() {
849
852
  </div>
850
853
  ) : teamAgents.length === 0 ? (
851
854
  <EmptyState onPlanner={launchPlanner} onSpawn={() => openDetail({ type: 'spawn' })} />
855
+ ) : workspaceMode ? (
856
+ <WorkspaceMode />
852
857
  ) : showPreviewInAgents && previewState.url ? (
853
858
  <PreviewWorkspace embedded />
854
859
  ) : (
@@ -857,8 +862,8 @@ export default function AgentsView() {
857
862
  </ReactFlowProvider>
858
863
  )}
859
864
  </div>
860
- <RecommendedTeamCard />
861
- {!isLoading && teamAgents.length > 0 && (
865
+ {!workspaceMode && <RecommendedTeamCard />}
866
+ {!isLoading && teamAgents.length > 0 && !workspaceMode && (
862
867
  <button
863
868
  onClick={() => openDetail({ type: 'spawn' })}
864
869
  className="absolute bottom-4 left-4 z-40 flex items-center gap-1.5 h-8 px-4 rounded-md bg-accent/15 text-accent text-xs font-semibold font-sans hover:bg-accent/25 transition-colors cursor-pointer select-none shadow-lg shadow-black/10"
@@ -867,7 +872,21 @@ export default function AgentsView() {
867
872
  Spawn
868
873
  </button>
869
874
  )}
870
- {!isLoading && teamAgents.length > 0 && previewState.url && (
875
+ {!isLoading && teamAgents.length > 0 && (
876
+ <button
877
+ onClick={() => setWorkspaceMode(!workspaceMode)}
878
+ className={cn(
879
+ 'absolute bottom-4 z-40 flex items-center gap-1.5 h-8 px-4 rounded-md text-xs font-semibold font-sans transition-colors cursor-pointer select-none shadow-lg shadow-black/10',
880
+ previewState.url && !workspaceMode ? 'right-32' : 'right-4',
881
+ workspaceMode
882
+ ? 'bg-accent/15 text-accent hover:bg-accent/25'
883
+ : 'bg-purple/15 text-purple hover:bg-purple/25',
884
+ )}
885
+ >
886
+ {workspaceMode ? <><Users size={14} /> Tree</> : <><Code2 size={14} /> Workspace</>}
887
+ </button>
888
+ )}
889
+ {!isLoading && teamAgents.length > 0 && !workspaceMode && previewState.url && (
871
890
  <button
872
891
  onClick={togglePreviewInAgents}
873
892
  className="absolute bottom-4 right-4 z-40 flex items-center gap-1.5 h-8 px-4 rounded-md bg-info/15 text-info text-xs font-semibold font-sans hover:bg-info/25 transition-colors cursor-pointer select-none shadow-lg shadow-black/10"
@@ -0,0 +1,55 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Doomsday Clock</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Nosifer&family=Special+Elite&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="style.css">
11
+ </head>
12
+ <body>
13
+ <div class="particles">
14
+ <span class="particle"></span>
15
+ <span class="particle"></span>
16
+ <span class="particle"></span>
17
+ <span class="particle"></span>
18
+ <span class="particle"></span>
19
+ <span class="particle"></span>
20
+ <span class="particle"></span>
21
+ <span class="particle"></span>
22
+ </div>
23
+
24
+ <main class="container">
25
+ <p class="tagline" id="tagline">TIME IS RUNNING OUT</p>
26
+
27
+ <div class="clock-ring">
28
+ <div class="glow"></div>
29
+ <div class="countdown" id="countdown">
30
+ <div class="unit">
31
+ <span class="digit" id="days">00</span>
32
+ <span class="label">Days</span>
33
+ </div>
34
+ <div class="separator">:</div>
35
+ <div class="unit">
36
+ <span class="digit" id="hours">00</span>
37
+ <span class="label">Hours</span>
38
+ </div>
39
+ <div class="separator">:</div>
40
+ <div class="unit">
41
+ <span class="digit" id="minutes">00</span>
42
+ <span class="label">Minutes</span>
43
+ </div>
44
+ <div class="separator">:</div>
45
+ <div class="unit">
46
+ <span class="digit" id="seconds">00</span>
47
+ <span class="label">Seconds</span>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </main>
52
+
53
+ <script src="script.js"></script>
54
+ </body>
55
+ </html>
@@ -0,0 +1,66 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ (function () {
3
+ var STORAGE_KEY = 'doomsday-target';
4
+ var FIVE_DAYS_MS = 5 * 24 * 60 * 60 * 1000;
5
+
6
+ var daysEl = document.getElementById('days');
7
+ var hoursEl = document.getElementById('hours');
8
+ var minutesEl = document.getElementById('minutes');
9
+ var secondsEl = document.getElementById('seconds');
10
+ var taglineEl = document.getElementById('tagline');
11
+
12
+ var target = localStorage.getItem(STORAGE_KEY);
13
+ if (!target) {
14
+ target = Date.now() + FIVE_DAYS_MS;
15
+ localStorage.setItem(STORAGE_KEY, target);
16
+ } else {
17
+ target = Number(target);
18
+ }
19
+
20
+ var done = false;
21
+
22
+ function pad(n) {
23
+ return n < 10 ? '0' + n : String(n);
24
+ }
25
+
26
+ function tick() {
27
+ if (done) return;
28
+
29
+ var remaining = target - Date.now();
30
+
31
+ if (remaining <= 0) {
32
+ done = true;
33
+ daysEl.textContent = '00';
34
+ hoursEl.textContent = '00';
35
+ minutesEl.textContent = '00';
36
+ secondsEl.textContent = '00';
37
+
38
+ taglineEl.textContent = 'IT IS DONE';
39
+ taglineEl.classList.add('done');
40
+
41
+ document.querySelectorAll('.digit').forEach(function (el) {
42
+ el.classList.add('stopped');
43
+ });
44
+ document.querySelectorAll('.separator').forEach(function (el) {
45
+ el.classList.add('stopped');
46
+ });
47
+
48
+ document.body.classList.add('flash');
49
+ return;
50
+ }
51
+
52
+ var totalSeconds = Math.floor(remaining / 1000);
53
+ var d = Math.floor(totalSeconds / 86400);
54
+ var h = Math.floor((totalSeconds % 86400) / 3600);
55
+ var m = Math.floor((totalSeconds % 3600) / 60);
56
+ var s = totalSeconds % 60;
57
+
58
+ daysEl.textContent = pad(d);
59
+ hoursEl.textContent = pad(h);
60
+ minutesEl.textContent = pad(m);
61
+ secondsEl.textContent = pad(s);
62
+ }
63
+
64
+ tick();
65
+ setInterval(tick, 1000);
66
+ })();