groove-dev 0.27.15 → 0.27.18

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 (172) hide show
  1. package/CLAUDE.md +0 -10
  2. package/README.md +37 -1
  3. package/developerID_application.cer +0 -0
  4. package/node_modules/@groove-dev/daemon/src/api.js +586 -67
  5. package/node_modules/@groove-dev/daemon/src/classifier.js +24 -0
  6. package/node_modules/@groove-dev/daemon/src/credentials.js +12 -2
  7. package/node_modules/@groove-dev/daemon/src/federation/ambassador.js +204 -0
  8. package/node_modules/@groove-dev/daemon/src/federation/connection.js +359 -0
  9. package/node_modules/@groove-dev/daemon/src/federation/contracts.js +112 -0
  10. package/node_modules/@groove-dev/daemon/src/federation/whitelist.js +190 -0
  11. package/node_modules/@groove-dev/daemon/src/federation.js +166 -7
  12. package/node_modules/@groove-dev/daemon/src/index.js +172 -19
  13. package/node_modules/@groove-dev/daemon/src/introducer.js +52 -7
  14. package/node_modules/@groove-dev/daemon/src/journalist.js +46 -1
  15. package/node_modules/@groove-dev/daemon/src/memory.js +36 -16
  16. package/node_modules/@groove-dev/daemon/src/process.js +140 -23
  17. package/node_modules/@groove-dev/daemon/src/providers/base.js +1 -0
  18. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +14 -0
  19. package/node_modules/@groove-dev/daemon/src/providers/codex.js +124 -28
  20. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +104 -17
  21. package/node_modules/@groove-dev/daemon/src/providers/index.js +17 -0
  22. package/node_modules/@groove-dev/daemon/src/registry.js +10 -1
  23. package/node_modules/@groove-dev/daemon/src/rotator.js +93 -30
  24. package/node_modules/@groove-dev/daemon/src/skills.js +33 -3
  25. package/node_modules/@groove-dev/daemon/src/terminal-pty.js +9 -1
  26. package/node_modules/@groove-dev/daemon/src/tool-executor.js +11 -5
  27. package/node_modules/@groove-dev/daemon/src/toys.js +69 -0
  28. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +24 -5
  29. package/node_modules/@groove-dev/daemon/templates/toys-catalog.json +242 -0
  30. package/node_modules/@groove-dev/daemon/test/classifier.test.js +98 -0
  31. package/node_modules/@groove-dev/daemon/test/introducer.test.js +72 -1
  32. package/node_modules/@groove-dev/daemon/test/journalist.test.js +117 -0
  33. package/node_modules/@groove-dev/daemon/test/memory.test.js +37 -1
  34. package/node_modules/@groove-dev/daemon/test/rotator.test.js +183 -4
  35. package/node_modules/@groove-dev/gui/dist/assets/index-Bg6_D2xK.css +1 -0
  36. package/node_modules/@groove-dev/gui/dist/assets/index-D3rvwTHD.js +8607 -0
  37. package/node_modules/@groove-dev/gui/dist/index.html +3 -2
  38. package/node_modules/@groove-dev/gui/index.html +1 -0
  39. package/node_modules/@groove-dev/gui/src/app.css +7 -0
  40. package/node_modules/@groove-dev/gui/src/app.jsx +37 -10
  41. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +21 -31
  42. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +11 -6
  43. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
  44. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +42 -1
  45. package/node_modules/@groove-dev/gui/src/components/editor/breadcrumbs.jsx +30 -0
  46. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +33 -2
  47. package/node_modules/@groove-dev/gui/src/components/editor/editor-status-bar.jsx +26 -0
  48. package/node_modules/@groove-dev/gui/src/components/editor/editor-tabs.jsx +113 -34
  49. package/node_modules/@groove-dev/gui/src/components/editor/goto-line.jsx +35 -0
  50. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +12 -6
  51. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +15 -3
  52. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +0 -1
  53. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
  54. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +6 -2
  55. package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +11 -1
  56. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +10 -9
  57. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +9 -1
  58. package/node_modules/@groove-dev/gui/src/components/onboarding/provider-card.jsx +134 -0
  59. package/node_modules/@groove-dev/gui/src/components/onboarding/setup-wizard.jsx +819 -0
  60. package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +12 -5
  61. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +15 -8
  62. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-modal.jsx +151 -0
  63. package/node_modules/@groove-dev/gui/src/components/settings/federation-activity.jsx +98 -0
  64. package/node_modules/@groove-dev/gui/src/components/settings/federation-panel.jsx +290 -0
  65. package/node_modules/@groove-dev/gui/src/components/settings/federation-peers.jsx +126 -0
  66. package/node_modules/@groove-dev/gui/src/components/settings/federation-wizard.jsx +293 -0
  67. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +110 -67
  68. package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +3 -3
  69. package/node_modules/@groove-dev/gui/src/components/settings/server-detail.jsx +310 -0
  70. package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +4 -1
  71. package/node_modules/@groove-dev/gui/src/components/settings/server-list.jsx +59 -0
  72. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +549 -0
  73. package/node_modules/@groove-dev/gui/src/components/toys/toy-card.jsx +78 -0
  74. package/node_modules/@groove-dev/gui/src/components/toys/toy-creator.jsx +144 -0
  75. package/node_modules/@groove-dev/gui/src/components/toys/toy-launcher.jsx +187 -0
  76. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +2 -2
  77. package/node_modules/@groove-dev/gui/src/lib/electron.js +15 -0
  78. package/node_modules/@groove-dev/gui/src/lib/format.js +1 -0
  79. package/node_modules/@groove-dev/gui/src/stores/groove.js +388 -63
  80. package/node_modules/@groove-dev/gui/src/views/agents.jsx +148 -42
  81. package/node_modules/@groove-dev/gui/src/views/editor.jsx +92 -2
  82. package/node_modules/@groove-dev/gui/src/views/federation.jsx +37 -0
  83. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +2 -42
  84. package/node_modules/@groove-dev/gui/src/views/settings.jsx +35 -134
  85. package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +327 -0
  86. package/node_modules/@groove-dev/gui/src/views/teams.jsx +3 -3
  87. package/node_modules/@groove-dev/gui/src/views/toys.jsx +162 -0
  88. package/package.json +1 -1
  89. package/packages/daemon/src/api.js +586 -67
  90. package/packages/daemon/src/classifier.js +24 -0
  91. package/packages/daemon/src/credentials.js +12 -2
  92. package/packages/daemon/src/federation/ambassador.js +204 -0
  93. package/packages/daemon/src/federation/connection.js +359 -0
  94. package/packages/daemon/src/federation/contracts.js +112 -0
  95. package/packages/daemon/src/federation/whitelist.js +190 -0
  96. package/packages/daemon/src/federation.js +166 -7
  97. package/packages/daemon/src/index.js +172 -19
  98. package/packages/daemon/src/introducer.js +52 -7
  99. package/packages/daemon/src/journalist.js +46 -1
  100. package/packages/daemon/src/memory.js +36 -16
  101. package/packages/daemon/src/process.js +140 -23
  102. package/packages/daemon/src/providers/base.js +1 -0
  103. package/packages/daemon/src/providers/claude-code.js +14 -0
  104. package/packages/daemon/src/providers/codex.js +124 -28
  105. package/packages/daemon/src/providers/gemini.js +104 -17
  106. package/packages/daemon/src/providers/index.js +17 -0
  107. package/packages/daemon/src/registry.js +10 -1
  108. package/packages/daemon/src/rotator.js +93 -30
  109. package/packages/daemon/src/skills.js +33 -3
  110. package/packages/daemon/src/terminal-pty.js +9 -1
  111. package/packages/daemon/src/tool-executor.js +11 -5
  112. package/packages/daemon/src/toys.js +69 -0
  113. package/packages/daemon/src/tunnel-manager.js +24 -5
  114. package/packages/daemon/templates/toys-catalog.json +242 -0
  115. package/packages/gui/dist/assets/index-Bg6_D2xK.css +1 -0
  116. package/packages/gui/dist/assets/index-D3rvwTHD.js +8607 -0
  117. package/packages/gui/dist/index.html +3 -2
  118. package/packages/gui/index.html +1 -0
  119. package/packages/gui/src/app.css +7 -0
  120. package/packages/gui/src/app.jsx +37 -10
  121. package/packages/gui/src/components/agents/agent-chat.jsx +21 -31
  122. package/packages/gui/src/components/agents/agent-config.jsx +11 -6
  123. package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
  124. package/packages/gui/src/components/agents/spawn-wizard.jsx +42 -1
  125. package/packages/gui/src/components/editor/breadcrumbs.jsx +30 -0
  126. package/packages/gui/src/components/editor/code-editor.jsx +33 -2
  127. package/packages/gui/src/components/editor/editor-status-bar.jsx +26 -0
  128. package/packages/gui/src/components/editor/editor-tabs.jsx +113 -34
  129. package/packages/gui/src/components/editor/goto-line.jsx +35 -0
  130. package/packages/gui/src/components/editor/terminal.jsx +12 -6
  131. package/packages/gui/src/components/layout/activity-bar.jsx +15 -3
  132. package/packages/gui/src/components/layout/app-shell.jsx +0 -1
  133. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
  134. package/packages/gui/src/components/layout/command-palette.jsx +6 -2
  135. package/packages/gui/src/components/layout/status-bar.jsx +11 -1
  136. package/packages/gui/src/components/layout/terminal-panel.jsx +10 -9
  137. package/packages/gui/src/components/marketplace/repo-import.jsx +9 -1
  138. package/packages/gui/src/components/onboarding/provider-card.jsx +134 -0
  139. package/packages/gui/src/components/onboarding/setup-wizard.jsx +819 -0
  140. package/packages/gui/src/components/pro/pro-gate.jsx +12 -5
  141. package/packages/gui/src/components/pro/upgrade-card.jsx +15 -8
  142. package/packages/gui/src/components/pro/upgrade-modal.jsx +151 -0
  143. package/packages/gui/src/components/settings/federation-activity.jsx +98 -0
  144. package/packages/gui/src/components/settings/federation-panel.jsx +290 -0
  145. package/packages/gui/src/components/settings/federation-peers.jsx +126 -0
  146. package/packages/gui/src/components/settings/federation-wizard.jsx +293 -0
  147. package/packages/gui/src/components/settings/quick-connect.jsx +110 -67
  148. package/packages/gui/src/components/settings/remote-server-card.jsx +3 -3
  149. package/packages/gui/src/components/settings/server-detail.jsx +310 -0
  150. package/packages/gui/src/components/settings/server-dialog.jsx +4 -1
  151. package/packages/gui/src/components/settings/server-list.jsx +59 -0
  152. package/packages/gui/src/components/settings/ssh-wizard.jsx +549 -0
  153. package/packages/gui/src/components/toys/toy-card.jsx +78 -0
  154. package/packages/gui/src/components/toys/toy-creator.jsx +144 -0
  155. package/packages/gui/src/components/toys/toy-launcher.jsx +187 -0
  156. package/packages/gui/src/components/ui/toast.jsx +2 -2
  157. package/packages/gui/src/lib/electron.js +15 -0
  158. package/packages/gui/src/lib/format.js +1 -0
  159. package/packages/gui/src/stores/groove.js +388 -63
  160. package/packages/gui/src/views/agents.jsx +148 -42
  161. package/packages/gui/src/views/editor.jsx +92 -2
  162. package/packages/gui/src/views/federation.jsx +37 -0
  163. package/packages/gui/src/views/marketplace.jsx +2 -42
  164. package/packages/gui/src/views/settings.jsx +35 -134
  165. package/packages/gui/src/views/subscription-panel.jsx +327 -0
  166. package/packages/gui/src/views/teams.jsx +3 -3
  167. package/packages/gui/src/views/toys.jsx +162 -0
  168. package/plans/chat-persistence-refactor.md +154 -0
  169. package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +0 -1
  170. package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +0 -677
  171. package/packages/gui/dist/assets/index-BE6lYcd7.css +0 -1
  172. package/packages/gui/dist/assets/index-zdzOLAZM.js +0 -677
@@ -1,7 +1,11 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useRef, useState, useEffect, useCallback } from 'react';
2
3
  import { useGrooveStore } from '../../stores/groove';
3
4
  import { cn } from '../../lib/cn';
4
- import { X } from 'lucide-react';
5
+ import { X, ChevronLeft, ChevronRight, Copy, XCircle } from 'lucide-react';
6
+ import {
7
+ ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuSeparator,
8
+ } from '../ui/context-menu';
5
9
 
6
10
  export function EditorTabs() {
7
11
  const openTabs = useGrooveStore((s) => s.editorOpenTabs);
@@ -10,42 +14,117 @@ export function EditorTabs() {
10
14
  const setActiveFile = useGrooveStore((s) => s.setActiveFile);
11
15
  const closeFile = useGrooveStore((s) => s.closeFile);
12
16
 
17
+ const scrollRef = useRef(null);
18
+ const [overflows, setOverflows] = useState(false);
19
+
20
+ const checkOverflow = useCallback(() => {
21
+ const el = scrollRef.current;
22
+ if (el) setOverflows(el.scrollWidth > el.clientWidth);
23
+ }, []);
24
+
25
+ useEffect(() => {
26
+ checkOverflow();
27
+ const el = scrollRef.current;
28
+ if (!el) return;
29
+ const ro = new ResizeObserver(checkOverflow);
30
+ ro.observe(el);
31
+ return () => ro.disconnect();
32
+ }, [checkOverflow, openTabs.length]);
33
+
34
+ function scrollLeft() {
35
+ scrollRef.current?.scrollBy({ left: -120, behavior: 'smooth' });
36
+ }
37
+ function scrollRight() {
38
+ scrollRef.current?.scrollBy({ left: 120, behavior: 'smooth' });
39
+ }
40
+
41
+ function closeOthers(path) {
42
+ openTabs.filter((t) => t !== path).forEach((t) => closeFile(t));
43
+ }
44
+ function closeAll() {
45
+ [...openTabs].forEach((t) => closeFile(t));
46
+ }
47
+ function closeToRight(path) {
48
+ const idx = openTabs.indexOf(path);
49
+ openTabs.slice(idx + 1).forEach((t) => closeFile(t));
50
+ }
51
+ function copyPath(path) {
52
+ navigator.clipboard?.writeText(path);
53
+ }
54
+
13
55
  if (openTabs.length === 0) return null;
14
56
 
15
57
  return (
16
- <div className="flex items-center h-8 bg-surface-3 border-b border-border overflow-x-auto flex-shrink-0">
17
- {openTabs.map((path) => {
18
- const isActive = path === activeFile;
19
- const file = files[path];
20
- const isDirty = file && file.content !== file.originalContent;
21
- const name = path.split('/').pop();
22
-
23
- return (
24
- <div
25
- key={path}
26
- className={cn(
27
- 'flex items-center gap-1.5 h-full px-3 text-xs font-sans cursor-pointer select-none',
28
- 'border-r border-border-subtle',
29
- 'transition-colors duration-75',
30
- isActive
31
- ? 'bg-surface-2 text-text-0 border-b-2 border-b-accent'
32
- : 'bg-surface-3 text-text-3 hover:text-text-1 hover:bg-surface-4',
33
- )}
34
- onClick={() => setActiveFile(path)}
35
- >
36
- <span className="truncate max-w-[120px]">{name}</span>
37
- {isDirty && (
38
- <span className="w-1.5 h-1.5 rounded-full bg-warning flex-shrink-0" />
39
- )}
40
- <button
41
- onClick={(e) => { e.stopPropagation(); closeFile(path); }}
42
- className="p-0.5 rounded hover:bg-surface-5 text-text-4 hover:text-text-1 transition-colors cursor-pointer ml-0.5"
43
- >
44
- <X size={12} />
45
- </button>
46
- </div>
47
- );
48
- })}
58
+ <div className="flex items-center h-8 bg-surface-3 border-b border-border flex-shrink-0">
59
+ {overflows && (
60
+ <button onClick={scrollLeft} className="flex-shrink-0 px-1 h-full text-text-4 hover:text-text-1 hover:bg-surface-4 transition-colors cursor-pointer">
61
+ <ChevronLeft size={14} />
62
+ </button>
63
+ )}
64
+
65
+ <div ref={scrollRef} className="flex items-center flex-1 min-w-0 overflow-x-auto scrollbar-none scroll-smooth" style={{ scrollSnapType: 'x mandatory' }}>
66
+ {openTabs.map((path) => {
67
+ const isActive = path === activeFile;
68
+ const file = files[path];
69
+ const isDirty = file && file.content !== file.originalContent;
70
+ const name = path.split('/').pop();
71
+
72
+ return (
73
+ <ContextMenu key={path}>
74
+ <ContextMenuTrigger asChild>
75
+ <div
76
+ className={cn(
77
+ 'flex items-center gap-1.5 h-full px-3 text-xs font-sans cursor-pointer select-none',
78
+ 'border-r border-border-subtle',
79
+ 'transition-colors duration-75 flex-shrink-0',
80
+ isActive
81
+ ? 'bg-surface-2 text-text-0 border-b-2 border-b-accent'
82
+ : 'bg-surface-3 text-text-3 hover:text-text-1 hover:bg-surface-4',
83
+ )}
84
+ style={{ scrollSnapAlign: 'start' }}
85
+ onClick={() => setActiveFile(path)}
86
+ onAuxClick={(e) => { if (e.button === 1) { e.preventDefault(); closeFile(path); } }}
87
+ >
88
+ <span className="truncate max-w-[120px]">{name}</span>
89
+ {isDirty && (
90
+ <span className="w-1.5 h-1.5 rounded-full bg-warning flex-shrink-0" />
91
+ )}
92
+ <button
93
+ onClick={(e) => { e.stopPropagation(); closeFile(path); }}
94
+ className="p-0.5 rounded hover:bg-surface-5 text-text-4 hover:text-text-1 transition-colors cursor-pointer ml-0.5"
95
+ >
96
+ <X size={12} />
97
+ </button>
98
+ </div>
99
+ </ContextMenuTrigger>
100
+ <ContextMenuContent>
101
+ <ContextMenuItem onSelect={() => closeFile(path)}>
102
+ <X size={12} className="text-text-3" /> Close
103
+ </ContextMenuItem>
104
+ <ContextMenuItem onSelect={() => closeOthers(path)}>
105
+ <XCircle size={12} className="text-text-3" /> Close Others
106
+ </ContextMenuItem>
107
+ <ContextMenuItem onSelect={() => closeAll()}>
108
+ <XCircle size={12} className="text-text-3" /> Close All
109
+ </ContextMenuItem>
110
+ <ContextMenuItem onSelect={() => closeToRight(path)}>
111
+ <ChevronRight size={12} className="text-text-3" /> Close to the Right
112
+ </ContextMenuItem>
113
+ <ContextMenuSeparator />
114
+ <ContextMenuItem onSelect={() => copyPath(path)}>
115
+ <Copy size={12} className="text-text-3" /> Copy Path
116
+ </ContextMenuItem>
117
+ </ContextMenuContent>
118
+ </ContextMenu>
119
+ );
120
+ })}
121
+ </div>
122
+
123
+ {overflows && (
124
+ <button onClick={scrollRight} className="flex-shrink-0 px-1 h-full text-text-4 hover:text-text-1 hover:bg-surface-4 transition-colors cursor-pointer">
125
+ <ChevronRight size={14} />
126
+ </button>
127
+ )}
49
128
  </div>
50
129
  );
51
130
  }
@@ -0,0 +1,35 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState, useEffect, useRef } from 'react';
3
+
4
+ export function GotoLine({ currentLine, onGoto, onClose }) {
5
+ const [value, setValue] = useState('');
6
+ const inputRef = useRef(null);
7
+
8
+ useEffect(() => {
9
+ inputRef.current?.focus();
10
+ }, []);
11
+
12
+ function handleKeyDown(e) {
13
+ if (e.key === 'Enter') {
14
+ const line = parseInt(value, 10);
15
+ if (line > 0) onGoto(line);
16
+ onClose();
17
+ }
18
+ if (e.key === 'Escape') onClose();
19
+ }
20
+
21
+ return (
22
+ <div className="absolute top-2 left-1/2 -translate-x-1/2 z-20 flex items-center gap-2 bg-surface-2 border border-border rounded-lg shadow-xl p-2">
23
+ <label className="text-2xs font-sans text-text-3 whitespace-nowrap">Go to Line:</label>
24
+ <input
25
+ ref={inputRef}
26
+ value={value}
27
+ onChange={(e) => setValue(e.target.value)}
28
+ onKeyDown={handleKeyDown}
29
+ onBlur={onClose}
30
+ placeholder={String(currentLine)}
31
+ className="w-20 h-6 px-2 text-xs bg-surface-0 border border-border-subtle rounded text-text-0 font-mono focus:outline-none focus:border-accent"
32
+ />
33
+ </div>
34
+ );
35
+ }
@@ -23,6 +23,7 @@ let tabCounter = 0;
23
23
  function TerminalInstance({ tabId, visible }) {
24
24
  const containerRef = useRef(null);
25
25
  const termRef = useRef(null);
26
+ const fitRef = useRef(null);
26
27
  const termIdRef = useRef(null);
27
28
  const handlerRef = useRef(null);
28
29
  const mountedRef = useRef(false);
@@ -47,13 +48,18 @@ function TerminalInstance({ tabId, visible }) {
47
48
  term.loadAddon(new WebLinksAddon());
48
49
  term.open(containerRef.current);
49
50
  termRef.current = term;
51
+ fitRef.current = fitAddon;
50
52
 
51
- requestAnimationFrame(() => fitAddon.fit());
53
+ requestAnimationFrame(() => {
54
+ try { fitAddon.fit(); } catch {}
55
+ });
52
56
 
57
+ let spawnAttempts = 0;
53
58
  function trySpawn() {
59
+ spawnAttempts++;
54
60
  const ws = useGrooveStore.getState().ws;
55
61
  if (!ws || ws.readyState !== WebSocket.OPEN) {
56
- setTimeout(trySpawn, 500);
62
+ if (spawnAttempts < 20) setTimeout(trySpawn, 500);
57
63
  return;
58
64
  }
59
65
 
@@ -108,15 +114,15 @@ function TerminalInstance({ tabId, visible }) {
108
114
  handlerRef.current.ws.removeEventListener('message', handlerRef.current.handler);
109
115
  }
110
116
  term.dispose();
117
+ fitRef.current = null;
111
118
  mountedRef.current = false;
112
119
  };
113
120
  }, []);
114
121
 
115
- // Refit when visibility changes
116
122
  useEffect(() => {
117
- if (visible && termRef.current) {
123
+ if (visible && fitRef.current) {
118
124
  requestAnimationFrame(() => {
119
- try { termRef.current._addonManager?._addons?.[0]?.instance?.fit?.(); } catch {}
125
+ try { fitRef.current.fit(); } catch {}
120
126
  });
121
127
  }
122
128
  }, [visible]);
@@ -124,7 +130,7 @@ function TerminalInstance({ tabId, visible }) {
124
130
  return (
125
131
  <div
126
132
  ref={containerRef}
127
- className="w-full h-full"
133
+ className="w-full h-full overflow-hidden"
128
134
  style={{ display: visible ? 'block' : 'none' }}
129
135
  />
130
136
  );
@@ -1,15 +1,18 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { Network, Code2, ChartSpline, Puzzle, Users, Box, Newspaper, Settings } from 'lucide-react';
2
+ import { Network, Code2, ChartSpline, Puzzle, Gamepad2, Users, Box, Globe, Newspaper, Settings } from 'lucide-react';
3
3
  import { cn } from '../../lib/cn';
4
4
  import { Tooltip } from '../ui/tooltip';
5
+ import { isElectron, getPlatform } from '../../lib/electron';
5
6
 
6
7
  const NAV_ITEMS = [
7
8
  { id: 'agents', icon: Network, label: 'Agents' },
8
9
  { id: 'editor', icon: Code2, label: 'Editor' },
9
10
  { id: 'dashboard', icon: ChartSpline, label: 'Dashboard' },
10
11
  { id: 'marketplace', icon: Puzzle, label: 'Marketplace' },
12
+ { id: 'toys', icon: Gamepad2, label: 'Toys' },
11
13
  { id: 'models', icon: Box, label: 'Models' },
12
14
  { id: 'teams', icon: Users, label: 'Teams' },
15
+ { id: 'federation', icon: Globe, label: 'Federation' },
13
16
  ];
14
17
 
15
18
  const UTIL_ITEMS = [
@@ -18,10 +21,19 @@ const UTIL_ITEMS = [
18
21
  ];
19
22
 
20
23
  export function ActivityBar({ activeView, detailPanel, onNavigate, onTogglePanel }) {
24
+ const darwinTrafficLights = isElectron() && getPlatform() === 'darwin';
25
+
21
26
  return (
22
27
  <nav className="w-12 flex-shrink-0 flex flex-col bg-surface-3 border-r border-border">
28
+ {/* Sidebar header — aligns with BreadcrumbBar */}
29
+ {darwinTrafficLights && (
30
+ <div className="h-11 flex-shrink-0 flex items-end justify-center pb-1.5 border-b border-border">
31
+ <img src="/favicon.png" alt="Groove" className="h-6 w-6 rounded-full" />
32
+ </div>
33
+ )}
34
+
23
35
  {/* Main nav */}
24
- <div className="flex flex-col items-center gap-0.5 pt-2">
36
+ <div className="flex flex-col items-center gap-1.5 pt-2.5">
25
37
  {NAV_ITEMS.map((item) => (
26
38
  <Tooltip key={item.id} content={item.label} side="right">
27
39
  <button
@@ -43,7 +55,7 @@ export function ActivityBar({ activeView, detailPanel, onNavigate, onTogglePanel
43
55
  <div className="flex-1" />
44
56
 
45
57
  {/* Utility nav */}
46
- <div className="flex flex-col items-center gap-0.5 pb-2">
58
+ <div className="flex flex-col items-center gap-1.5 pb-3">
47
59
  {UTIL_ITEMS.map((item) => {
48
60
  const isActive = item.panel
49
61
  ? detailPanel?.type === item.id
@@ -67,7 +67,6 @@ export function AppShell({ children, detailContent, terminalContent }) {
67
67
  daemonHost={daemonHost}
68
68
  editorActiveFile={editorActiveFile}
69
69
  onOpenCommandPalette={toggleCommandPalette}
70
- onSpawn={() => openDetail({ type: 'spawn' })}
71
70
  />
72
71
 
73
72
  <div className="flex-1 flex min-h-0">
@@ -1,8 +1,124 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { Search, Plus, ChevronRight } from 'lucide-react';
2
+ import { useState, useEffect, useRef } from 'react';
3
+ import { Search, ChevronRight, LogIn, LogOut, User, ExternalLink, BookOpen, ChevronDown } from 'lucide-react';
3
4
  import { cn } from '../../lib/cn';
5
+ import { useGrooveStore } from '../../stores/groove';
4
6
  import { isElectron, getPlatform } from '../../lib/electron';
5
7
 
8
+ function ProfilePic({ user, size = 24 }) {
9
+ const [broken, setBroken] = useState(false);
10
+ const src = user?.avatar || user?.picture || user?.photoURL || user?.photo;
11
+
12
+ if (src && !broken) {
13
+ return (
14
+ <img
15
+ src={src}
16
+ alt=""
17
+ className="rounded-full"
18
+ style={{ width: size, height: size }}
19
+ referrerPolicy="no-referrer"
20
+ crossOrigin="anonymous"
21
+ onError={() => setBroken(true)}
22
+ />
23
+ );
24
+ }
25
+
26
+ return (
27
+ <div
28
+ className="rounded-full bg-accent/10 flex items-center justify-center"
29
+ style={{ width: size, height: size }}
30
+ >
31
+ <User size={Math.round(size * 0.5)} className="text-accent" />
32
+ </div>
33
+ );
34
+ }
35
+
36
+ function UserMenu() {
37
+ const authenticated = useGrooveStore((s) => s.marketplaceAuthenticated);
38
+ const user = useGrooveStore((s) => s.marketplaceUser);
39
+ const login = useGrooveStore((s) => s.marketplaceLogin);
40
+ const logout = useGrooveStore((s) => s.marketplaceLogout);
41
+ const [open, setOpen] = useState(false);
42
+ const ref = useRef(null);
43
+
44
+ useEffect(() => {
45
+ if (!open) return;
46
+ function handleClick(e) {
47
+ if (ref.current && !ref.current.contains(e.target)) setOpen(false);
48
+ }
49
+ document.addEventListener('mousedown', handleClick, true);
50
+ return () => document.removeEventListener('mousedown', handleClick, true);
51
+ }, [open]);
52
+
53
+ if (!authenticated) {
54
+ return (
55
+ <button
56
+ onClick={login}
57
+ className="flex items-center gap-1.5 h-7 px-3 rounded-md bg-surface-1 border border-border-subtle text-xs font-semibold font-sans text-text-2 hover:text-text-0 hover:border-border transition-colors cursor-pointer select-none flex-shrink-0"
58
+ >
59
+ <LogIn size={12} />
60
+ Sign in
61
+ </button>
62
+ );
63
+ }
64
+
65
+ return (
66
+ <div ref={ref} className="relative flex-shrink-0">
67
+ <button
68
+ onClick={() => setOpen(!open)}
69
+ className={cn(
70
+ 'flex items-center gap-2 h-7 pl-1 pr-2 rounded-md transition-colors cursor-pointer select-none',
71
+ open ? 'bg-surface-1 border border-border' : 'hover:bg-surface-1 border border-transparent',
72
+ )}
73
+ >
74
+ <ProfilePic user={user} size={20} />
75
+ <span className="text-xs text-text-1 font-sans font-medium max-w-[100px] truncate">
76
+ {user?.displayName || user?.id || 'Account'}
77
+ </span>
78
+ <ChevronDown size={10} className={cn('text-text-4 transition-transform', open && 'rotate-180')} />
79
+ </button>
80
+
81
+ {open && (
82
+ <div className="absolute right-0 top-full mt-1 w-48 py-1 rounded-md bg-surface-1 border border-border shadow-lg z-50">
83
+ <div className="px-3 py-2 border-b border-border-subtle">
84
+ <p className="text-xs font-medium text-text-0 font-sans truncate">{user?.displayName || 'Account'}</p>
85
+ {user?.email && <p className="text-2xs text-text-4 font-sans truncate">{user.email}</p>}
86
+ </div>
87
+ <a
88
+ href="https://docs.groovedev.ai"
89
+ target="_blank"
90
+ rel="noopener noreferrer"
91
+ onClick={() => setOpen(false)}
92
+ className="flex items-center gap-2 px-3 py-1.5 text-xs text-text-2 hover:text-text-0 hover:bg-surface-3 font-sans cursor-pointer transition-colors"
93
+ >
94
+ <BookOpen size={12} />
95
+ Docs
96
+ <ExternalLink size={9} className="ml-auto text-text-4" />
97
+ </a>
98
+ <a
99
+ href="https://groovedev.ai"
100
+ target="_blank"
101
+ rel="noopener noreferrer"
102
+ onClick={() => setOpen(false)}
103
+ className="flex items-center gap-2 px-3 py-1.5 text-xs text-text-2 hover:text-text-0 hover:bg-surface-3 font-sans cursor-pointer transition-colors"
104
+ >
105
+ <ExternalLink size={12} />
106
+ groovedev.ai
107
+ </a>
108
+ <div className="my-1 h-px bg-border-subtle" />
109
+ <button
110
+ onClick={() => { setOpen(false); logout(); }}
111
+ className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-text-3 hover:text-danger hover:bg-surface-3 font-sans cursor-pointer transition-colors"
112
+ >
113
+ <LogOut size={12} />
114
+ Sign out
115
+ </button>
116
+ </div>
117
+ )}
118
+ </div>
119
+ );
120
+ }
121
+
6
122
  const VIEW_LABELS = {
7
123
  agents: 'Agents',
8
124
  editor: 'Editor',
@@ -17,7 +133,6 @@ export function BreadcrumbBar({
17
133
  daemonHost,
18
134
  editorActiveFile,
19
135
  onOpenCommandPalette,
20
- onSpawn,
21
136
  }) {
22
137
  const crumbs = ['Groove', VIEW_LABELS[activeView] || activeView];
23
138
  if (activeView === 'editor' && editorActiveFile) {
@@ -27,48 +142,45 @@ export function BreadcrumbBar({
27
142
  const electron = isElectron();
28
143
  const darwinDrag = electron && getPlatform() === 'darwin';
29
144
 
145
+ const [instanceName, setInstanceName] = useState(null);
146
+
147
+ useEffect(() => {
148
+ if (window.groove?.getInstanceInfo) {
149
+ window.groove.getInstanceInfo().then(info => {
150
+ if (info?.name) setInstanceName(info.name);
151
+ });
152
+ } else {
153
+ const param = new URLSearchParams(window.location.search).get('instance');
154
+ if (param) setInstanceName(param);
155
+ }
156
+ }, []);
157
+
30
158
  return (
31
159
  <header
32
160
  className={cn(
33
- 'h-11 flex-shrink-0 flex items-center gap-3 px-4 bg-surface-3 border-b border-border',
34
- darwinDrag && 'pl-20 electron-drag electron-no-drag-children',
161
+ 'h-11 flex-shrink-0 flex items-center gap-3 px-4 bg-surface-3 border-b border-border relative',
162
+ darwinDrag && 'pl-24 electron-drag electron-no-drag-children',
35
163
  )}
36
164
  >
37
- {/* Logo */}
38
- <img src="/favicon.png" alt="Groove" className="h-7 w-7 rounded-full flex-shrink-0" />
39
-
40
- {/* Host badge — show instance name from ?instance= or raw host */}
41
- {(() => {
42
- const instance = new URLSearchParams(window.location.search).get('instance');
43
- if (instance) return (
44
- <span className="text-2xs font-mono font-semibold text-accent bg-accent/10 px-1.5 py-0.5 rounded flex-shrink-0">
45
- {instance}
46
- </span>
47
- );
48
- if (daemonHost) return (
49
- <span className="text-2xs font-mono font-semibold text-text-3 bg-surface-5 px-1.5 py-0.5 rounded flex-shrink-0">
50
- {daemonHost}
51
- </span>
52
- );
53
- return null;
54
- })()}
165
+ {/* Logo — web only (Electron shows it in the sidebar) */}
166
+ {!darwinDrag && <img src="/favicon.png" alt="Groove" className="h-7 w-7 rounded-full flex-shrink-0" />}
55
167
 
56
- <div className="flex-1 min-w-4" />
168
+ {/* Project name badge — clickable to open folder */}
169
+ {instanceName && (
170
+ <button
171
+ onClick={() => window.groove?.openFolder?.()}
172
+ className="text-2xs font-mono font-semibold text-accent bg-accent/10 px-1.5 py-0.5 rounded flex-shrink-0 hover:bg-accent/20 transition-colors cursor-pointer"
173
+ >
174
+ {instanceName}
175
+ </button>
176
+ )}
57
177
 
58
- {/* Command palettepill style */}
59
- <button
60
- onClick={onOpenCommandPalette}
61
- className={cn(
62
- 'flex items-center gap-2.5 h-8 px-4 rounded-full w-full max-w-md',
63
- 'bg-surface-1 border border-border-subtle',
64
- 'text-xs text-text-4 font-sans',
65
- 'hover:border-border hover:text-text-3 transition-colors cursor-pointer',
66
- )}
67
- >
68
- <Search size={14} className="flex-shrink-0" />
69
- <span className="flex-1 text-left">Search commands...</span>
70
- <kbd className="text-2xs font-mono bg-surface-4 px-1.5 py-0.5 rounded-full text-text-4">Cmd+K</kbd>
71
- </button>
178
+ {/* Host badgeshow raw host when no instance */}
179
+ {!instanceName && daemonHost && (
180
+ <span className="text-2xs font-mono font-semibold text-text-3 bg-surface-5 px-1.5 py-0.5 rounded flex-shrink-0">
181
+ {daemonHost}
182
+ </span>
183
+ )}
72
184
 
73
185
  <div className="flex-1 min-w-4" />
74
186
 
@@ -84,16 +196,22 @@ export function BreadcrumbBar({
84
196
  ))}
85
197
  </div>
86
198
 
87
- {/* Spawn button */}
88
- {connected && (
89
- <button
90
- onClick={onSpawn}
91
- className="ml-1 flex items-center gap-1 h-7 px-3.5 rounded-full bg-accent/15 text-accent text-xs font-semibold font-sans hover:bg-accent/25 transition-colors cursor-pointer select-none flex-shrink-0"
92
- >
93
- <Plus size={14} />
94
- Spawn
95
- </button>
96
- )}
199
+ <UserMenu />
200
+
201
+ {/* Command palette — absolutely centered to header */}
202
+ <button
203
+ onClick={onOpenCommandPalette}
204
+ className={cn(
205
+ 'absolute left-1/2 -translate-x-1/2 flex items-center gap-2.5 h-8 px-4 rounded-md w-full max-w-md',
206
+ 'bg-surface-1 border border-border-subtle',
207
+ 'text-xs text-text-4 font-sans',
208
+ 'hover:border-border hover:text-text-3 transition-colors cursor-pointer',
209
+ )}
210
+ >
211
+ <Search size={14} className="flex-shrink-0" />
212
+ <span className="flex-1 text-left">Search commands...</span>
213
+ <kbd className="text-2xs font-mono bg-surface-4 px-1.5 py-0.5 rounded text-text-4 ml-1">Cmd+K</kbd>
214
+ </button>
97
215
  </header>
98
216
  );
99
217
  }
@@ -4,7 +4,7 @@ import { useGrooveStore } from '../../stores/groove';
4
4
  import {
5
5
  Network, Code2, ChartSpline, Puzzle, Users, Plus,
6
6
  RotateCw, Skull, MessageSquare, Terminal, Newspaper,
7
- Search, Radio, ExternalLink,
7
+ Search, Radio, ExternalLink, FolderOpen, Globe,
8
8
  } from 'lucide-react';
9
9
  import { cn } from '../../lib/cn';
10
10
  import { AnimatePresence, motion } from 'framer-motion';
@@ -15,6 +15,7 @@ const STATIC_COMMANDS = [
15
15
  { id: 'nav:dashboard', label: 'Go to Dashboard', icon: ChartSpline, category: 'Navigation', action: (s) => { s.setActiveView('dashboard'); } },
16
16
  { id: 'nav:marketplace', label: 'Go to Marketplace', icon: Puzzle, category: 'Navigation', action: (s) => { s.setActiveView('marketplace'); } },
17
17
  { id: 'nav:teams', label: 'Go to Teams', icon: Users, category: 'Navigation', action: (s) => { s.setActiveView('teams'); } },
18
+ { id: 'nav:federation', label: 'Go to Federation', icon: Globe, category: 'Navigation', action: (s) => { s.setActiveView('federation'); } },
18
19
  { id: 'action:spawn', label: 'Spawn Agent', icon: Plus, category: 'Actions', action: (s) => { s.openDetail({ type: 'spawn' }); } },
19
20
  { id: 'action:terminal', label: 'Toggle Terminal', icon: Terminal, category: 'Actions', action: (s) => { s.setTerminalVisible(!s.terminalVisible); }, shortcut: 'Cmd+J' },
20
21
  { id: 'action:journalist', label: 'Toggle Journalist', icon: Newspaper, category: 'Actions', action: (s) => {
@@ -51,7 +52,10 @@ export function CommandPalette() {
51
52
  : { id: `tunnel:connect:${t.id}`, label: `Connect to ${t.name}`, icon: Radio, category: 'Remote', action: (s) => { s.connectTunnel(t.id); } }
52
53
  ),
53
54
  ];
54
- return [...STATIC_COMMANDS, ...agentCommands, ...tunnelCommands];
55
+ const windowCommands = window.groove?.openFolder ? [
56
+ { id: 'action:openfolder', label: 'Open Folder', icon: FolderOpen, category: 'Window', shortcut: 'Cmd+O', action: () => window.groove.openFolder() },
57
+ ] : [];
58
+ return [...STATIC_COMMANDS, ...windowCommands, ...agentCommands, ...tunnelCommands];
55
59
  }, [agents, savedTunnels]);
56
60
 
57
61
  // Filter
@@ -1,5 +1,5 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { Terminal, BookOpen, Radio, Plug } from 'lucide-react';
2
+ import { Terminal, BookOpen, Radio, Plug, Globe } from 'lucide-react';
3
3
  import { cn } from '../../lib/cn';
4
4
  import { StatusDot } from '../ui/status-dot';
5
5
  import { fmtUptime } from '../../lib/format';
@@ -28,6 +28,16 @@ export function StatusBar({
28
28
  {connected ? (electron ? 'Desktop' : 'Connected') : 'Offline'}
29
29
  </span>
30
30
  </div>
31
+ {electron && connected && (
32
+ <button
33
+ onClick={() => openExternal(window.location.href)}
34
+ className="flex items-center gap-1 text-text-4 hover:text-text-1 cursor-pointer transition-colors"
35
+ title="Open this workspace in your browser"
36
+ >
37
+ <Globe size={10} />
38
+ <span>Browser</span>
39
+ </button>
40
+ )}
31
41
  {connected && uptime > 0 && (
32
42
  <span className="text-text-4">Up {fmtUptime(uptime)}</span>
33
43
  )}
@@ -63,26 +63,27 @@ export function TerminalPanel({
63
63
  )}
64
64
 
65
65
  {/* Header bar */}
66
- <div className="flex items-center h-9 bg-surface-1 border-b border-border-subtle flex-shrink-0 pl-3 pr-1.5">
66
+ <div className="flex items-center h-9 bg-surface-1 border-b border-border flex-shrink-0 px-3">
67
67
  {/* Tabs */}
68
- <div className="flex items-center gap-0 flex-1 min-w-0 overflow-x-auto scrollbar-none">
68
+ <div className="flex items-center gap-0 flex-1 min-w-0 overflow-x-auto scrollbar-none h-full">
69
69
  {tabList.map((tab) => (
70
70
  <button
71
71
  key={tab.id}
72
72
  onClick={() => onSelectTab?.(tab.id)}
73
73
  className={cn(
74
- 'flex items-center gap-1.5 pl-2.5 pr-1 h-7 text-2xs font-sans cursor-pointer select-none transition-colors flex-shrink-0 rounded-t',
74
+ 'inline-flex items-center gap-1.5 px-3 h-full text-xs font-medium font-sans cursor-pointer select-none transition-colors duration-100 flex-shrink-0',
75
+ 'border-t',
75
76
  tab.id === activeTab
76
- ? 'text-text-0 bg-surface-0'
77
- : 'text-text-3 hover:text-text-1 hover:bg-surface-0/50',
77
+ ? 'text-text-0 border-accent'
78
+ : 'text-text-2 border-transparent hover:text-text-0 hover:bg-surface-5/50',
78
79
  )}
79
80
  >
80
- <Terminal size={10} />
81
- <span className="truncate max-w-[80px]">{tab.label}</span>
81
+ <Terminal size={11} />
82
+ <span className="truncate max-w-[100px]">{tab.label}</span>
82
83
  {tabList.length > 1 && (
83
84
  <button
84
85
  onClick={(e) => { e.stopPropagation(); onCloseTab?.(tab.id); }}
85
- className="ml-0.5 p-0.5 rounded hover:bg-surface-5 text-text-4 hover:text-text-1 cursor-pointer"
86
+ className="ml-1 p-0.5 rounded hover:bg-surface-5 text-text-4 hover:text-text-1 cursor-pointer"
86
87
  >
87
88
  <X size={9} />
88
89
  </button>
@@ -91,7 +92,7 @@ export function TerminalPanel({
91
92
  ))}
92
93
  <button
93
94
  onClick={onAddTab}
94
- className="flex items-center justify-center w-6 h-6 text-text-4 hover:text-text-1 hover:bg-surface-0/50 rounded cursor-pointer transition-colors flex-shrink-0 ml-0.5"
95
+ className="flex items-center justify-center w-6 h-6 text-text-3 hover:text-text-0 hover:bg-surface-5/50 rounded cursor-pointer transition-colors flex-shrink-0 ml-1"
95
96
  title="New terminal"
96
97
  >
97
98
  <Plus size={11} />