groove-dev 0.27.14 → 0.27.17

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 (169) hide show
  1. package/README.md +37 -1
  2. package/developerID_application.cer +0 -0
  3. package/node_modules/@groove-dev/daemon/src/api.js +587 -68
  4. package/node_modules/@groove-dev/daemon/src/classifier.js +24 -0
  5. package/node_modules/@groove-dev/daemon/src/credentials.js +12 -2
  6. package/node_modules/@groove-dev/daemon/src/federation/ambassador.js +204 -0
  7. package/node_modules/@groove-dev/daemon/src/federation/connection.js +359 -0
  8. package/node_modules/@groove-dev/daemon/src/federation/contracts.js +112 -0
  9. package/node_modules/@groove-dev/daemon/src/federation/whitelist.js +190 -0
  10. package/node_modules/@groove-dev/daemon/src/federation.js +166 -7
  11. package/node_modules/@groove-dev/daemon/src/index.js +172 -19
  12. package/node_modules/@groove-dev/daemon/src/introducer.js +52 -7
  13. package/node_modules/@groove-dev/daemon/src/journalist.js +46 -1
  14. package/node_modules/@groove-dev/daemon/src/memory.js +36 -16
  15. package/node_modules/@groove-dev/daemon/src/process.js +140 -23
  16. package/node_modules/@groove-dev/daemon/src/providers/base.js +1 -0
  17. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +1 -0
  18. package/node_modules/@groove-dev/daemon/src/providers/codex.js +124 -28
  19. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +104 -17
  20. package/node_modules/@groove-dev/daemon/src/providers/index.js +17 -0
  21. package/node_modules/@groove-dev/daemon/src/registry.js +10 -1
  22. package/node_modules/@groove-dev/daemon/src/rotator.js +93 -30
  23. package/node_modules/@groove-dev/daemon/src/skills.js +33 -3
  24. package/node_modules/@groove-dev/daemon/src/terminal-pty.js +9 -1
  25. package/node_modules/@groove-dev/daemon/src/tool-executor.js +11 -5
  26. package/node_modules/@groove-dev/daemon/src/toys.js +69 -0
  27. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +24 -5
  28. package/node_modules/@groove-dev/daemon/templates/toys-catalog.json +242 -0
  29. package/node_modules/@groove-dev/daemon/test/classifier.test.js +98 -0
  30. package/node_modules/@groove-dev/daemon/test/introducer.test.js +72 -1
  31. package/node_modules/@groove-dev/daemon/test/journalist.test.js +117 -0
  32. package/node_modules/@groove-dev/daemon/test/memory.test.js +37 -1
  33. package/node_modules/@groove-dev/daemon/test/rotator.test.js +183 -4
  34. package/node_modules/@groove-dev/gui/dist/assets/index-BglPgjlu.js +8607 -0
  35. package/node_modules/@groove-dev/gui/dist/assets/index-CGcwmmJv.css +1 -0
  36. package/node_modules/@groove-dev/gui/dist/index.html +3 -2
  37. package/node_modules/@groove-dev/gui/index.html +1 -0
  38. package/node_modules/@groove-dev/gui/src/app.css +7 -0
  39. package/node_modules/@groove-dev/gui/src/app.jsx +37 -10
  40. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +21 -31
  41. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +11 -6
  42. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
  43. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +42 -1
  44. package/node_modules/@groove-dev/gui/src/components/editor/breadcrumbs.jsx +30 -0
  45. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +33 -2
  46. package/node_modules/@groove-dev/gui/src/components/editor/editor-status-bar.jsx +26 -0
  47. package/node_modules/@groove-dev/gui/src/components/editor/editor-tabs.jsx +113 -34
  48. package/node_modules/@groove-dev/gui/src/components/editor/goto-line.jsx +35 -0
  49. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +12 -6
  50. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +13 -3
  51. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +0 -1
  52. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
  53. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +6 -2
  54. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +10 -9
  55. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +9 -1
  56. package/node_modules/@groove-dev/gui/src/components/onboarding/provider-card.jsx +134 -0
  57. package/node_modules/@groove-dev/gui/src/components/onboarding/setup-wizard.jsx +819 -0
  58. package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +12 -5
  59. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +15 -8
  60. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-modal.jsx +151 -0
  61. package/node_modules/@groove-dev/gui/src/components/settings/federation-activity.jsx +98 -0
  62. package/node_modules/@groove-dev/gui/src/components/settings/federation-panel.jsx +290 -0
  63. package/node_modules/@groove-dev/gui/src/components/settings/federation-peers.jsx +126 -0
  64. package/node_modules/@groove-dev/gui/src/components/settings/federation-wizard.jsx +293 -0
  65. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +110 -67
  66. package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +3 -3
  67. package/node_modules/@groove-dev/gui/src/components/settings/server-detail.jsx +310 -0
  68. package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +4 -1
  69. package/node_modules/@groove-dev/gui/src/components/settings/server-list.jsx +59 -0
  70. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +549 -0
  71. package/node_modules/@groove-dev/gui/src/components/toys/toy-card.jsx +78 -0
  72. package/node_modules/@groove-dev/gui/src/components/toys/toy-creator.jsx +144 -0
  73. package/node_modules/@groove-dev/gui/src/components/toys/toy-launcher.jsx +187 -0
  74. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +2 -2
  75. package/node_modules/@groove-dev/gui/src/lib/electron.js +15 -0
  76. package/node_modules/@groove-dev/gui/src/lib/format.js +1 -0
  77. package/node_modules/@groove-dev/gui/src/stores/groove.js +373 -58
  78. package/node_modules/@groove-dev/gui/src/views/agents.jsx +148 -42
  79. package/node_modules/@groove-dev/gui/src/views/editor.jsx +92 -2
  80. package/node_modules/@groove-dev/gui/src/views/federation.jsx +37 -0
  81. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +2 -42
  82. package/node_modules/@groove-dev/gui/src/views/settings.jsx +32 -132
  83. package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +327 -0
  84. package/node_modules/@groove-dev/gui/src/views/teams.jsx +3 -3
  85. package/node_modules/@groove-dev/gui/src/views/toys.jsx +162 -0
  86. package/package.json +1 -1
  87. package/packages/daemon/src/api.js +587 -68
  88. package/packages/daemon/src/classifier.js +24 -0
  89. package/packages/daemon/src/credentials.js +12 -2
  90. package/packages/daemon/src/federation/ambassador.js +204 -0
  91. package/packages/daemon/src/federation/connection.js +359 -0
  92. package/packages/daemon/src/federation/contracts.js +112 -0
  93. package/packages/daemon/src/federation/whitelist.js +190 -0
  94. package/packages/daemon/src/federation.js +166 -7
  95. package/packages/daemon/src/index.js +172 -19
  96. package/packages/daemon/src/introducer.js +52 -7
  97. package/packages/daemon/src/journalist.js +46 -1
  98. package/packages/daemon/src/memory.js +36 -16
  99. package/packages/daemon/src/process.js +140 -23
  100. package/packages/daemon/src/providers/base.js +1 -0
  101. package/packages/daemon/src/providers/claude-code.js +1 -0
  102. package/packages/daemon/src/providers/codex.js +124 -28
  103. package/packages/daemon/src/providers/gemini.js +104 -17
  104. package/packages/daemon/src/providers/index.js +17 -0
  105. package/packages/daemon/src/registry.js +10 -1
  106. package/packages/daemon/src/rotator.js +93 -30
  107. package/packages/daemon/src/skills.js +33 -3
  108. package/packages/daemon/src/terminal-pty.js +9 -1
  109. package/packages/daemon/src/tool-executor.js +11 -5
  110. package/packages/daemon/src/toys.js +69 -0
  111. package/packages/daemon/src/tunnel-manager.js +24 -5
  112. package/packages/daemon/templates/toys-catalog.json +242 -0
  113. package/packages/gui/dist/assets/index-BglPgjlu.js +8607 -0
  114. package/packages/gui/dist/assets/index-CGcwmmJv.css +1 -0
  115. package/packages/gui/dist/index.html +3 -2
  116. package/packages/gui/index.html +1 -0
  117. package/packages/gui/src/app.css +7 -0
  118. package/packages/gui/src/app.jsx +37 -10
  119. package/packages/gui/src/components/agents/agent-chat.jsx +21 -31
  120. package/packages/gui/src/components/agents/agent-config.jsx +11 -6
  121. package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
  122. package/packages/gui/src/components/agents/spawn-wizard.jsx +42 -1
  123. package/packages/gui/src/components/editor/breadcrumbs.jsx +30 -0
  124. package/packages/gui/src/components/editor/code-editor.jsx +33 -2
  125. package/packages/gui/src/components/editor/editor-status-bar.jsx +26 -0
  126. package/packages/gui/src/components/editor/editor-tabs.jsx +113 -34
  127. package/packages/gui/src/components/editor/goto-line.jsx +35 -0
  128. package/packages/gui/src/components/editor/terminal.jsx +12 -6
  129. package/packages/gui/src/components/layout/activity-bar.jsx +13 -3
  130. package/packages/gui/src/components/layout/app-shell.jsx +0 -1
  131. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
  132. package/packages/gui/src/components/layout/command-palette.jsx +6 -2
  133. package/packages/gui/src/components/layout/terminal-panel.jsx +10 -9
  134. package/packages/gui/src/components/marketplace/repo-import.jsx +9 -1
  135. package/packages/gui/src/components/onboarding/provider-card.jsx +134 -0
  136. package/packages/gui/src/components/onboarding/setup-wizard.jsx +819 -0
  137. package/packages/gui/src/components/pro/pro-gate.jsx +12 -5
  138. package/packages/gui/src/components/pro/upgrade-card.jsx +15 -8
  139. package/packages/gui/src/components/pro/upgrade-modal.jsx +151 -0
  140. package/packages/gui/src/components/settings/federation-activity.jsx +98 -0
  141. package/packages/gui/src/components/settings/federation-panel.jsx +290 -0
  142. package/packages/gui/src/components/settings/federation-peers.jsx +126 -0
  143. package/packages/gui/src/components/settings/federation-wizard.jsx +293 -0
  144. package/packages/gui/src/components/settings/quick-connect.jsx +110 -67
  145. package/packages/gui/src/components/settings/remote-server-card.jsx +3 -3
  146. package/packages/gui/src/components/settings/server-detail.jsx +310 -0
  147. package/packages/gui/src/components/settings/server-dialog.jsx +4 -1
  148. package/packages/gui/src/components/settings/server-list.jsx +59 -0
  149. package/packages/gui/src/components/settings/ssh-wizard.jsx +549 -0
  150. package/packages/gui/src/components/toys/toy-card.jsx +78 -0
  151. package/packages/gui/src/components/toys/toy-creator.jsx +144 -0
  152. package/packages/gui/src/components/toys/toy-launcher.jsx +187 -0
  153. package/packages/gui/src/components/ui/toast.jsx +2 -2
  154. package/packages/gui/src/lib/electron.js +15 -0
  155. package/packages/gui/src/lib/format.js +1 -0
  156. package/packages/gui/src/stores/groove.js +373 -58
  157. package/packages/gui/src/views/agents.jsx +148 -42
  158. package/packages/gui/src/views/editor.jsx +92 -2
  159. package/packages/gui/src/views/federation.jsx +37 -0
  160. package/packages/gui/src/views/marketplace.jsx +2 -42
  161. package/packages/gui/src/views/settings.jsx +32 -132
  162. package/packages/gui/src/views/subscription-panel.jsx +327 -0
  163. package/packages/gui/src/views/teams.jsx +3 -3
  164. package/packages/gui/src/views/toys.jsx +162 -0
  165. package/plans/chat-persistence-refactor.md +154 -0
  166. package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +0 -1
  167. package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +0 -677
  168. package/packages/gui/dist/assets/index-BE6lYcd7.css +0 -1
  169. package/packages/gui/dist/assets/index-zdzOLAZM.js +0 -677
@@ -0,0 +1,30 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { ChevronRight } from 'lucide-react';
3
+
4
+ export function Breadcrumbs({ path }) {
5
+ if (!path) return null;
6
+
7
+ const segments = path.split('/').filter(Boolean);
8
+
9
+ return (
10
+ <div className="flex items-center h-7 px-3 bg-surface-2 border-b border-border-subtle text-2xs font-sans text-text-3 overflow-hidden flex-shrink-0 select-none">
11
+ {segments.map((segment, i) => {
12
+ const isLast = i === segments.length - 1;
13
+ return (
14
+ <span key={i} className="flex items-center min-w-0">
15
+ {i > 0 && <ChevronRight size={10} className="mx-0.5 flex-shrink-0 text-text-4" />}
16
+ <span
17
+ className={
18
+ isLast
19
+ ? 'text-text-1 font-medium truncate'
20
+ : 'hover:text-text-1 cursor-pointer truncate transition-colors'
21
+ }
22
+ >
23
+ {segment}
24
+ </span>
25
+ </span>
26
+ );
27
+ })}
28
+ </div>
29
+ );
30
+ }
@@ -34,16 +34,41 @@ const grooveTheme = EditorView.theme({
34
34
  '.cm-activeLineGutter': { backgroundColor: '#2c313a' },
35
35
  '.cm-activeLine': { backgroundColor: 'rgba(44, 49, 58, 0.5)' },
36
36
  '&.cm-focused .cm-selectionBackground, .cm-selectionBackground': { backgroundColor: 'rgba(51, 175, 188, 0.15)' },
37
+ // Search panel styling
38
+ '.cm-panels': { backgroundColor: '#24282f', borderBottom: '1px solid #3e4451' },
39
+ '.cm-panels.cm-panels-top': { borderBottom: '1px solid #3e4451' },
40
+ '.cm-panels.cm-panels-bottom': { borderTop: '1px solid #3e4451' },
41
+ '.cm-search': { padding: '6px 8px', gap: '4px', fontFamily: 'var(--font-sans)', fontSize: '12px', display: 'flex', flexWrap: 'wrap', alignItems: 'center' },
42
+ '.cm-search label': { display: 'flex', alignItems: 'center', gap: '4px', color: '#8b929e', fontSize: '11px' },
43
+ '.cm-search input, .cm-search .cm-textfield': {
44
+ backgroundColor: '#1a1e25', border: '1px solid #2c313a', borderRadius: '4px', color: '#e6e6e6',
45
+ padding: '2px 6px', fontSize: '12px', fontFamily: 'var(--font-mono)', outline: 'none',
46
+ },
47
+ '.cm-search input:focus, .cm-search .cm-textfield:focus': { borderColor: '#33afbc' },
48
+ '.cm-search .cm-button, .cm-button': {
49
+ backgroundColor: '#2c313a', border: '1px solid #3e4451', borderRadius: '4px', color: '#bcc2cd',
50
+ padding: '2px 8px', fontSize: '11px', fontFamily: 'var(--font-sans)', cursor: 'pointer',
51
+ backgroundImage: 'none',
52
+ },
53
+ '.cm-search .cm-button:hover, .cm-button:hover': { backgroundColor: '#333842', color: '#e6e6e6' },
54
+ '.cm-search .cm-button:active': { backgroundColor: '#3a3f4b' },
55
+ '.cm-search br': { display: 'none' },
56
+ '.cm-panel.cm-search [name=close]': { color: '#6e7681', cursor: 'pointer', padding: '0 4px' },
57
+ '.cm-panel.cm-search [name=close]:hover': { color: '#e6e6e6' },
58
+ '.cm-searchMatch': { backgroundColor: 'rgba(51, 175, 188, 0.2)', outline: '1px solid rgba(51, 175, 188, 0.4)' },
59
+ '.cm-searchMatch-selected': { backgroundColor: 'rgba(51, 175, 188, 0.35)' },
37
60
  }, { dark: true });
38
61
 
39
- export function CodeEditor({ content, language, onChange, onSave }) {
62
+ export function CodeEditor({ content, language, onChange, onSave, onCursorChange, viewRef: externalViewRef }) {
40
63
  const containerRef = useRef(null);
41
64
  const viewRef = useRef(null);
42
65
  const langCompartment = useRef(new Compartment());
43
66
  const onChangeRef = useRef(onChange);
44
67
  const onSaveRef = useRef(onSave);
68
+ const onCursorChangeRef = useRef(onCursorChange);
45
69
  onChangeRef.current = onChange;
46
70
  onSaveRef.current = onSave;
71
+ onCursorChangeRef.current = onCursorChange;
47
72
 
48
73
  useEffect(() => {
49
74
  if (!containerRef.current) return;
@@ -74,14 +99,20 @@ export function CodeEditor({ content, language, onChange, onSave }) {
74
99
  if (update.docChanged) {
75
100
  onChangeRef.current?.(update.state.doc.toString());
76
101
  }
102
+ if (update.selectionSet || update.docChanged) {
103
+ const pos = update.state.selection.main.head;
104
+ const line = update.state.doc.lineAt(pos);
105
+ onCursorChangeRef.current?.({ line: line.number, col: pos - line.from + 1 });
106
+ }
77
107
  }),
78
108
  ],
79
109
  });
80
110
 
81
111
  const view = new EditorView({ state, parent: containerRef.current });
82
112
  viewRef.current = view;
113
+ if (externalViewRef) externalViewRef.current = view;
83
114
 
84
- return () => { view.destroy(); viewRef.current = null; };
115
+ return () => { view.destroy(); viewRef.current = null; if (externalViewRef) externalViewRef.current = null; };
85
116
  }, []); // mount once
86
117
 
87
118
  // Update content when file changes externally
@@ -0,0 +1,26 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ const LANG_LABELS = {
4
+ javascript: 'JavaScript',
5
+ typescript: 'TypeScript',
6
+ css: 'CSS',
7
+ html: 'HTML',
8
+ json: 'JSON',
9
+ markdown: 'Markdown',
10
+ python: 'Python',
11
+ };
12
+
13
+ export function EditorStatusBar({ cursorPos, language }) {
14
+ return (
15
+ <div className="flex items-center justify-between h-6 px-3 bg-surface-1 border-t border-border-subtle text-2xs font-sans text-text-3 flex-shrink-0 select-none">
16
+ <div className="flex items-center gap-3">
17
+ <span>Ln {cursorPos.line}, Col {cursorPos.col}</span>
18
+ </div>
19
+ <div className="flex items-center gap-3">
20
+ <span className="cursor-default">{LANG_LABELS[language] || language || 'Plain Text'}</span>
21
+ <span>Spaces: 2</span>
22
+ <span>UTF-8</span>
23
+ </div>
24
+ </div>
25
+ );
26
+ }
@@ -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,17 @@ 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">
23
28
  {/* Main nav */}
24
- <div className="flex flex-col items-center gap-0.5 pt-2">
29
+ <div className="flex flex-col items-center gap-1.5 pt-3">
30
+ {darwinTrafficLights && (
31
+ <div className="w-full h-[44px] flex-shrink-0 flex items-end justify-center pb-1.5">
32
+ <img src="/favicon.png" alt="Groove" className="h-7 w-7 rounded-full" />
33
+ </div>
34
+ )}
25
35
  {NAV_ITEMS.map((item) => (
26
36
  <Tooltip key={item.id} content={item.label} side="right">
27
37
  <button
@@ -43,7 +53,7 @@ export function ActivityBar({ activeView, detailPanel, onNavigate, onTogglePanel
43
53
  <div className="flex-1" />
44
54
 
45
55
  {/* Utility nav */}
46
- <div className="flex flex-col items-center gap-0.5 pb-2">
56
+ <div className="flex flex-col items-center gap-1.5 pb-3">
47
57
  {UTIL_ITEMS.map((item) => {
48
58
  const isActive = item.panel
49
59
  ? 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">