groove-dev 0.17.8 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/node_modules/@groove-dev/cli/package.json +4 -3
  2. package/node_modules/@groove-dev/daemon/google-oauth.json +5 -0
  3. package/node_modules/@groove-dev/daemon/integrations-registry.json +0 -40
  4. package/node_modules/@groove-dev/daemon/package.json +4 -3
  5. package/node_modules/@groove-dev/daemon/src/api.js +212 -21
  6. package/node_modules/@groove-dev/daemon/src/index.js +68 -1
  7. package/node_modules/@groove-dev/daemon/src/integrations.js +59 -20
  8. package/node_modules/@groove-dev/daemon/src/process.js +83 -11
  9. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +4 -0
  10. package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
  11. package/node_modules/@groove-dev/gui/.groove/audit.log +1 -0
  12. package/node_modules/@groove-dev/gui/.groove/codebase-index.json +64 -0
  13. package/node_modules/@groove-dev/gui/.groove/config.json +10 -0
  14. package/node_modules/@groove-dev/gui/.groove/coordination.md +5 -0
  15. package/node_modules/@groove-dev/gui/.groove/credentials.json +6 -0
  16. package/node_modules/@groove-dev/gui/.groove/daemon.port +1 -0
  17. package/node_modules/@groove-dev/gui/.groove/federation/identity.key +3 -0
  18. package/node_modules/@groove-dev/gui/.groove/federation/identity.pub +3 -0
  19. package/node_modules/@groove-dev/gui/.groove/integrations/package.json +6 -0
  20. package/node_modules/@groove-dev/gui/.groove/state.json +3 -0
  21. package/node_modules/@groove-dev/gui/dist/assets/index-x5suAiK7.js +182 -0
  22. package/node_modules/@groove-dev/gui/dist/index.html +1 -1
  23. package/node_modules/@groove-dev/gui/package.json +5 -4
  24. package/node_modules/@groove-dev/gui/src/App.jsx +149 -76
  25. package/node_modules/@groove-dev/gui/src/components/AgentActions.jsx +130 -1
  26. package/node_modules/@groove-dev/gui/src/components/AgentChat.jsx +47 -7
  27. package/node_modules/@groove-dev/gui/src/components/AgentNode.jsx +13 -83
  28. package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +918 -580
  29. package/node_modules/@groove-dev/gui/src/stores/groove.js +31 -2
  30. package/node_modules/@groove-dev/gui/src/views/AgentTree.jsx +133 -67
  31. package/node_modules/@groove-dev/gui/src/views/FileEditor.jsx +85 -1
  32. package/node_modules/@groove-dev/gui/src/views/IntegrationsStore.jsx +121 -44
  33. package/package.json +1 -2
  34. package/packages/cli/package.json +4 -3
  35. package/packages/daemon/integrations-registry.json +0 -40
  36. package/packages/daemon/package.json +4 -3
  37. package/packages/daemon/src/api.js +212 -21
  38. package/packages/daemon/src/index.js +68 -1
  39. package/packages/daemon/src/integrations.js +59 -20
  40. package/packages/daemon/src/process.js +83 -11
  41. package/packages/daemon/src/providers/claude-code.js +4 -0
  42. package/packages/daemon/src/registry.js +1 -1
  43. package/packages/gui/dist/assets/index-x5suAiK7.js +182 -0
  44. package/packages/gui/dist/index.html +1 -1
  45. package/packages/gui/node_modules/.vite/deps/@codemirror_autocomplete.js +68 -0
  46. package/packages/gui/node_modules/.vite/deps/@codemirror_autocomplete.js.map +7 -0
  47. package/packages/gui/node_modules/.vite/deps/@codemirror_commands.js +1420 -0
  48. package/packages/gui/node_modules/.vite/deps/@codemirror_commands.js.map +7 -0
  49. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-css.js +17 -0
  50. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-css.js.map +7 -0
  51. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js +22 -0
  52. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js.map +7 -0
  53. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js +34 -0
  54. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js.map +7 -0
  55. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-json.js +101 -0
  56. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-json.js.map +7 -0
  57. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js +2534 -0
  58. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js.map +7 -0
  59. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js +789 -0
  60. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js.map +7 -0
  61. package/packages/gui/node_modules/.vite/deps/@codemirror_language.js +115 -0
  62. package/packages/gui/node_modules/.vite/deps/@codemirror_language.js.map +7 -0
  63. package/packages/gui/node_modules/.vite/deps/@codemirror_search.js +1136 -0
  64. package/packages/gui/node_modules/.vite/deps/@codemirror_search.js.map +7 -0
  65. package/packages/gui/node_modules/.vite/deps/@codemirror_state.js +63 -0
  66. package/packages/gui/node_modules/.vite/deps/@codemirror_state.js.map +7 -0
  67. package/packages/gui/node_modules/.vite/deps/@codemirror_theme-one-dark.js +179 -0
  68. package/packages/gui/node_modules/.vite/deps/@codemirror_theme-one-dark.js.map +7 -0
  69. package/packages/gui/node_modules/.vite/deps/@codemirror_view.js +104 -0
  70. package/packages/gui/node_modules/.vite/deps/@codemirror_view.js.map +7 -0
  71. package/packages/gui/node_modules/.vite/deps/@xterm_addon-fit.js +46 -0
  72. package/packages/gui/node_modules/.vite/deps/@xterm_addon-fit.js.map +7 -0
  73. package/packages/gui/node_modules/.vite/deps/@xterm_addon-web-links.js +121 -0
  74. package/packages/gui/node_modules/.vite/deps/@xterm_addon-web-links.js.map +7 -0
  75. package/packages/gui/node_modules/.vite/deps/@xterm_xterm.js +9237 -0
  76. package/packages/gui/node_modules/.vite/deps/@xterm_xterm.js.map +7 -0
  77. package/packages/gui/node_modules/.vite/deps/@xyflow_react.js +9934 -0
  78. package/packages/gui/node_modules/.vite/deps/@xyflow_react.js.map +7 -0
  79. package/packages/gui/node_modules/.vite/deps/_metadata.json +184 -0
  80. package/packages/gui/node_modules/.vite/deps/chunk-3EE34IFC.js +5169 -0
  81. package/packages/gui/node_modules/.vite/deps/chunk-3EE34IFC.js.map +7 -0
  82. package/packages/gui/node_modules/.vite/deps/chunk-3IB5EUP7.js +2000 -0
  83. package/packages/gui/node_modules/.vite/deps/chunk-3IB5EUP7.js.map +7 -0
  84. package/packages/gui/node_modules/.vite/deps/chunk-3LBP22MX.js +1115 -0
  85. package/packages/gui/node_modules/.vite/deps/chunk-3LBP22MX.js.map +7 -0
  86. package/packages/gui/node_modules/.vite/deps/chunk-3Q7HT7ZF.js +701 -0
  87. package/packages/gui/node_modules/.vite/deps/chunk-3Q7HT7ZF.js.map +7 -0
  88. package/packages/gui/node_modules/.vite/deps/chunk-44CLUOQE.js +1776 -0
  89. package/packages/gui/node_modules/.vite/deps/chunk-44CLUOQE.js.map +7 -0
  90. package/packages/gui/node_modules/.vite/deps/chunk-5RZAEUNX.js +280 -0
  91. package/packages/gui/node_modules/.vite/deps/chunk-5RZAEUNX.js.map +7 -0
  92. package/packages/gui/node_modules/.vite/deps/chunk-5WRI5ZAA.js +30 -0
  93. package/packages/gui/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +7 -0
  94. package/packages/gui/node_modules/.vite/deps/chunk-7FYDPZIO.js +1004 -0
  95. package/packages/gui/node_modules/.vite/deps/chunk-7FYDPZIO.js.map +7 -0
  96. package/packages/gui/node_modules/.vite/deps/chunk-BX6POZPY.js +292 -0
  97. package/packages/gui/node_modules/.vite/deps/chunk-BX6POZPY.js.map +7 -0
  98. package/packages/gui/node_modules/.vite/deps/chunk-HVFOBSCQ.js +1062 -0
  99. package/packages/gui/node_modules/.vite/deps/chunk-HVFOBSCQ.js.map +7 -0
  100. package/packages/gui/node_modules/.vite/deps/chunk-RE2FU7ZU.js +10985 -0
  101. package/packages/gui/node_modules/.vite/deps/chunk-RE2FU7ZU.js.map +7 -0
  102. package/packages/gui/node_modules/.vite/deps/chunk-YYJMNVCJ.js +3459 -0
  103. package/packages/gui/node_modules/.vite/deps/chunk-YYJMNVCJ.js.map +7 -0
  104. package/packages/gui/node_modules/.vite/deps/package.json +3 -0
  105. package/packages/gui/node_modules/.vite/deps/react-dom.js +6 -0
  106. package/packages/gui/node_modules/.vite/deps/react-dom.js.map +7 -0
  107. package/packages/gui/node_modules/.vite/deps/react-dom_client.js +20217 -0
  108. package/packages/gui/node_modules/.vite/deps/react-dom_client.js.map +7 -0
  109. package/packages/gui/node_modules/.vite/deps/react.js +5 -0
  110. package/packages/gui/node_modules/.vite/deps/react.js.map +7 -0
  111. package/packages/gui/node_modules/.vite/deps/react_jsx-dev-runtime.js +278 -0
  112. package/packages/gui/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
  113. package/packages/gui/node_modules/.vite/deps/react_jsx-runtime.js +6 -0
  114. package/packages/gui/node_modules/.vite/deps/react_jsx-runtime.js.map +7 -0
  115. package/packages/gui/node_modules/.vite/deps/zustand.js +56 -0
  116. package/packages/gui/node_modules/.vite/deps/zustand.js.map +7 -0
  117. package/packages/gui/package.json +5 -4
  118. package/packages/gui/src/App.jsx +149 -76
  119. package/packages/gui/src/components/AgentActions.jsx +130 -1
  120. package/packages/gui/src/components/AgentChat.jsx +47 -7
  121. package/packages/gui/src/components/AgentNode.jsx +13 -83
  122. package/packages/gui/src/components/SpawnPanel.jsx +918 -580
  123. package/packages/gui/src/stores/groove.js +31 -2
  124. package/packages/gui/src/views/AgentTree.jsx +133 -67
  125. package/packages/gui/src/views/FileEditor.jsx +85 -1
  126. package/packages/gui/src/views/IntegrationsStore.jsx +121 -44
  127. package/docs/FILE-EDITOR-PLAN.md +0 -253
  128. package/docs/GUI_DESIGN_SPEC.md +0 -402
  129. package/docs/SKILLS-API-SPEC.md +0 -277
  130. package/node_modules/@groove-dev/gui/dist/assets/index-D5dtDQf0.js +0 -156
  131. package/packages/gui/dist/assets/index-D5dtDQf0.js +0 -156
@@ -1,7 +1,7 @@
1
1
  // GROOVE GUI — App Root
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
- import React, { useEffect } from 'react';
4
+ import React, { useEffect, useState, useRef } from 'react';
5
5
  import { useGrooveStore } from './stores/groove';
6
6
  import AgentTree from './views/AgentTree';
7
7
  import AgentPanel from './components/AgentPanel';
@@ -16,18 +16,41 @@ import IntegrationsStore from './views/IntegrationsStore';
16
16
  import ScheduleManager from './views/ScheduleManager';
17
17
  import FileEditor from './views/FileEditor';
18
18
 
19
- const TABS = [
19
+ const MAIN_TABS = [
20
20
  { id: 'agents', label: 'Agents' },
21
21
  { id: 'editor', label: 'Editor' },
22
- { id: 'integrations', label: 'Integrations' },
23
- { id: 'skills', label: 'Skills' },
24
22
  { id: 'stats', label: 'Stats' },
25
- { id: 'schedules', label: 'Schedules' },
26
23
  { id: 'teams', label: 'Teams' },
27
24
  { id: 'approvals', label: 'Approvals' },
28
25
  ];
29
26
 
30
- export default function App() {
27
+ const DROPDOWN_TABS = [
28
+ { id: 'journalist', label: 'Journalist' },
29
+ { id: 'integrations', label: 'Integrations' },
30
+ { id: 'skills', label: 'Skills' },
31
+ { id: 'schedules', label: 'Schedules' },
32
+ ];
33
+
34
+ const DROPDOWN_IDS = new Set(DROPDOWN_TABS.map((t) => t.id));
35
+
36
+ class ErrorBoundary extends React.Component {
37
+ constructor(props) { super(props); this.state = { error: null }; }
38
+ static getDerivedStateFromError(error) { return { error }; }
39
+ render() {
40
+ if (this.state.error) {
41
+ return React.createElement('div', {
42
+ style: { padding: 40, color: '#e06c75', fontFamily: 'monospace', fontSize: 13, background: '#1e2127', height: '100vh' }
43
+ },
44
+ React.createElement('h2', { style: { color: '#e6e6e6' } }, 'GROOVE — Render Error'),
45
+ React.createElement('pre', { style: { whiteSpace: 'pre-wrap', marginTop: 16, color: '#e06c75' } }, this.state.error.message),
46
+ React.createElement('pre', { style: { whiteSpace: 'pre-wrap', marginTop: 8, color: '#7a8394', fontSize: 11 } }, this.state.error.stack),
47
+ );
48
+ }
49
+ return this.props.children;
50
+ }
51
+ }
52
+
53
+ function AppInner() {
31
54
  const agents = useGrooveStore((s) => s.agents);
32
55
  const connected = useGrooveStore((s) => s.connected);
33
56
  const activeTab = useGrooveStore((s) => s.activeTab);
@@ -40,72 +63,99 @@ export default function App() {
40
63
  const openDetail = useGrooveStore((s) => s.openDetail);
41
64
  const closeDetail = useGrooveStore((s) => s.closeDetail);
42
65
 
66
+ const [dropdownOpen, setDropdownOpen] = useState(false);
67
+ const dropdownRef = useRef(null);
68
+ const moreBtnRef = useRef(null);
69
+
43
70
  useEffect(() => { connect(); }, [connect]);
44
71
 
45
- const runningCount = agents.filter((a) => a.status === 'running').length;
72
+ useEffect(() => {
73
+ if (!dropdownOpen) return;
74
+ const handler = (e) => {
75
+ if (dropdownRef.current?.contains(e.target)) return;
76
+ if (moreBtnRef.current?.contains(e.target)) return;
77
+ setDropdownOpen(false);
78
+ };
79
+ document.addEventListener('mousedown', handler);
80
+ return () => document.removeEventListener('mousedown', handler);
81
+ }, [dropdownOpen]);
82
+
46
83
  const hasAgents = agents.length > 0;
84
+ const moreActive = DROPDOWN_IDS.has(activeTab);
47
85
 
48
86
  return (
49
87
  <div style={styles.root}>
50
88
  {/* Header */}
51
89
  <header style={styles.header}>
52
- <div style={styles.headerLeft}>
53
- <img src="/groove-logo-short.png" alt="GROOVE" style={{ height: 18, marginTop: 3, opacity: 0.85 }} />
54
- {daemonHost && (
55
- <span style={styles.hostBadge}>{daemonHost}</span>
56
- )}
57
- </div>
90
+ <img src="/groove-logo-short.png" alt="GROOVE" style={{ height: 18, marginTop: 3, opacity: 0.85 }} />
91
+ {daemonHost && (
92
+ <span style={styles.hostBadge}>{daemonHost}</span>
93
+ )}
58
94
 
95
+ <div style={{ flex: 1 }} />
59
96
 
60
- <div style={styles.headerCenter}>
61
- {connected && TABS.map((tab) => (
97
+ {connected && MAIN_TABS.map((tab) => (
98
+ <button
99
+ key={tab.id}
100
+ onClick={() => setActiveTab(tab.id)}
101
+ style={{
102
+ ...styles.tabBtn,
103
+ color: activeTab === tab.id ? 'var(--text-bright)' : 'var(--text-dim)',
104
+ }}
105
+ >
106
+ {tab.label}
107
+ </button>
108
+ ))}
109
+
110
+ {connected && (
111
+ <div style={{ position: 'relative' }}>
62
112
  <button
63
- key={tab.id}
64
- onClick={() => setActiveTab(tab.id)}
113
+ ref={moreBtnRef}
114
+ onClick={() => setDropdownOpen((o) => !o)}
65
115
  style={{
66
116
  ...styles.tabBtn,
67
- color: activeTab === tab.id ? 'var(--text-bright)' : 'var(--text-primary)',
68
- borderBottom: activeTab === tab.id ? '2px solid var(--accent)' : '2px solid transparent',
69
- background: activeTab === tab.id ? 'var(--bg-active)' : 'transparent',
117
+ color: moreActive || dropdownOpen ? 'var(--text-bright)' : 'var(--text-dim)',
70
118
  }}
71
119
  >
72
- {tab.label}
120
+ More {'\u25BE'}
73
121
  </button>
74
- ))}
75
- </div>
122
+ {dropdownOpen && (
123
+ <div ref={dropdownRef} style={styles.dropdown}>
124
+ {DROPDOWN_TABS.map((tab) => (
125
+ <button
126
+ key={tab.id}
127
+ onClick={() => {
128
+ if (tab.id === 'journalist') {
129
+ detailPanel?.type === 'journalist' ? closeDetail() : openDetail({ type: 'journalist' });
130
+ } else {
131
+ setActiveTab(tab.id);
132
+ }
133
+ setDropdownOpen(false);
134
+ }}
135
+ style={{
136
+ ...styles.dropdownItem,
137
+ color: (tab.id === 'journalist' ? detailPanel?.type === 'journalist' : activeTab === tab.id) ? 'var(--text-bright)' : 'var(--text-primary)',
138
+ }}
139
+ >
140
+ {tab.label}
141
+ </button>
142
+ ))}
143
+ </div>
144
+ )}
145
+ </div>
146
+ )}
76
147
 
77
- <div style={styles.headerRight}>
78
- {statusMessage && (
79
- <span style={styles.statusText}>{statusMessage}</span>
80
- )}
81
- <span style={styles.agentCount}>
82
- {runningCount > 0
83
- ? `${runningCount} running`
84
- : agents.length > 0
85
- ? `${agents.length} agent${agents.length !== 1 ? 's' : ''}`
86
- : ''}
87
- </span>
88
- {connected && (
89
- <>
90
- <button
91
- onClick={() => detailPanel?.type === 'journalist' ? closeDetail() : openDetail({ type: 'journalist' })}
92
- style={{
93
- ...styles.tabBtn,
94
- color: detailPanel?.type === 'journalist' ? 'var(--text-bright)' : 'var(--text-primary)',
95
- borderBottom: detailPanel?.type === 'journalist' ? '2px solid var(--purple)' : '2px solid transparent',
96
- }}
97
- >
98
- Journalist
99
- </button>
100
- <button
101
- onClick={() => openDetail({ type: 'spawn' })}
102
- style={styles.spawnBtn}
103
- >
104
- + Spawn
105
- </button>
106
- </>
107
- )}
108
- </div>
148
+ {statusMessage && (
149
+ <span style={styles.statusText}>{statusMessage}</span>
150
+ )}
151
+ {connected && (
152
+ <button
153
+ onClick={() => openDetail({ type: 'spawn' })}
154
+ style={styles.spawnBtn}
155
+ >
156
+ + Spawn
157
+ </button>
158
+ )}
109
159
  </header>
110
160
 
111
161
  {/* Status pill — bottom left */}
@@ -155,23 +205,29 @@ export default function App() {
155
205
  {activeTab === 'approvals' && <ApprovalQueue />}
156
206
  </main>
157
207
 
158
- {/* Detail panel — in document flow */}
159
- {detailPanel && (
208
+ {/* Detail panel — sidebar for agent/journalist */}
209
+ {detailPanel && detailPanel.type !== 'spawn' && (
160
210
  <aside style={{
161
211
  ...styles.detailPanel,
162
212
  width: detailPanel.type === 'agent' ? '45%' : 320,
163
213
  }}>
164
214
  <button onClick={closeDetail} style={styles.closeBtn}>x</button>
165
215
  {detailPanel.type === 'agent' && <AgentPanel />}
166
- {detailPanel.type === 'spawn' && <SpawnPanel />}
167
216
  {detailPanel.type === 'journalist' && <JournalistFeed />}
168
217
  </aside>
169
218
  )}
219
+
220
+ {/* Spawn panel — full-screen overlay */}
221
+ {detailPanel?.type === 'spawn' && <SpawnPanel />}
170
222
  </div>
171
223
  </div>
172
224
  );
173
225
  }
174
226
 
227
+ export default function App() {
228
+ return React.createElement(ErrorBoundary, null, React.createElement(AppInner));
229
+ }
230
+
175
231
  const styles = {
176
232
  root: {
177
233
  width: '100%', height: '100%',
@@ -182,51 +238,68 @@ const styles = {
182
238
  height: 40,
183
239
  padding: '0 16px',
184
240
  borderBottom: '1px solid var(--border)',
185
- display: 'flex', alignItems: 'center', justifyContent: 'space-between',
241
+ display: 'flex', alignItems: 'center', gap: 2,
186
242
  background: 'var(--bg-chrome)',
187
243
  flexShrink: 0,
188
244
  position: 'relative',
189
245
  },
190
- headerLeft: {
191
- display: 'flex', alignItems: 'center', gap: 8,
192
- },
193
246
  hostBadge: {
194
247
  fontSize: 9, fontWeight: 600, letterSpacing: 0.5,
195
248
  color: 'var(--text-dim)', background: 'var(--bg-active)',
196
249
  padding: '2px 6px', borderRadius: 3,
197
250
  border: '1px solid var(--border)',
198
251
  fontFamily: 'var(--font)',
199
- },
200
- logo: {
201
- fontSize: 13, fontWeight: 600, letterSpacing: 1.5,
202
- color: 'var(--text-bright)',
203
- },
204
- headerCenter: {
205
- display: 'flex', alignItems: 'center', gap: 0,
206
- },
207
- headerRight: {
208
- display: 'flex', alignItems: 'center', gap: 10,
252
+ marginRight: 4,
209
253
  },
210
254
  tabBtn: {
211
- padding: '10px 14px',
255
+ padding: '0 10px',
212
256
  background: 'transparent',
213
257
  border: 'none',
214
258
  borderBottom: '2px solid transparent',
215
- fontSize: 12, fontWeight: 500,
259
+ fontSize: 11, fontWeight: 500,
216
260
  fontFamily: 'var(--font)',
217
261
  cursor: 'pointer',
218
262
  transition: 'color 0.1s',
263
+ alignSelf: 'stretch',
264
+ display: 'flex',
265
+ alignItems: 'center',
266
+ marginTop: 2,
267
+ },
268
+ dropdown: {
269
+ position: 'absolute',
270
+ top: '100%',
271
+ left: 0,
272
+ background: '#1e2228',
273
+ border: '1px solid var(--border)',
274
+ borderRadius: 6,
275
+ padding: '4px 0',
276
+ zIndex: 100,
277
+ minWidth: 160,
278
+ boxShadow: '0 4px 16px rgba(0,0,0,0.4)',
279
+ },
280
+ dropdownItem: {
281
+ display: 'block',
282
+ width: '100%',
283
+ padding: '8px 16px',
284
+ background: 'transparent',
285
+ border: 'none',
286
+ fontSize: 11,
287
+ fontWeight: 500,
288
+ fontFamily: 'var(--font)',
289
+ color: 'var(--text-primary)',
290
+ cursor: 'pointer',
291
+ textAlign: 'left',
219
292
  },
220
293
  spawnBtn: {
221
294
  padding: '4px 12px',
222
295
  background: 'transparent',
223
296
  border: '1px solid var(--accent)',
224
297
  borderRadius: 2,
225
- color: 'var(--accent)', fontSize: 12, fontWeight: 600,
298
+ color: 'var(--accent)', fontSize: 11, fontWeight: 600,
226
299
  fontFamily: 'var(--font)',
227
300
  cursor: 'pointer',
301
+ marginLeft: 12,
228
302
  },
229
- agentCount: { fontSize: 11, color: 'var(--text-dim)' },
230
303
  statusText: { fontSize: 11, color: 'var(--text-dim)', fontStyle: 'italic' },
231
304
  mainRow: {
232
305
  flex: 1, display: 'flex', overflow: 'hidden',
@@ -3,6 +3,49 @@
3
3
 
4
4
  import React, { useState, useEffect } from 'react';
5
5
  import { useGrooveStore } from '../stores/groove';
6
+ // System directory browser — browses absolute paths, not limited to project dir
7
+ function SystemDirPicker({ initial, onSelect, onClose }) {
8
+ const [currentPath, setCurrentPath] = useState(initial || '');
9
+ const [dirs, setDirs] = useState([]);
10
+ const [parentPath, setParentPath] = useState(null);
11
+
12
+ useEffect(() => {
13
+ fetch(`/api/browse-system?path=${encodeURIComponent(currentPath || '')}`)
14
+ .then((r) => r.json())
15
+ .then((data) => {
16
+ setDirs(data.dirs || []);
17
+ setParentPath(data.parent);
18
+ if (data.current) setCurrentPath(data.current);
19
+ })
20
+ .catch(() => {});
21
+ }, [currentPath]);
22
+
23
+ return (
24
+ <div style={{ border: '1px solid var(--border)', borderRadius: 4, background: 'var(--bg-base)', marginTop: 6, maxHeight: 200, display: 'flex', flexDirection: 'column' }}>
25
+ <div style={{ padding: '4px 8px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0 }}>
26
+ <span style={{ fontSize: 10, color: 'var(--text-dim)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 }}>{currentPath}</span>
27
+ <button onClick={onClose} style={{ background: 'none', border: 'none', color: 'var(--text-dim)', cursor: 'pointer', fontSize: 12, fontFamily: 'var(--font)' }}>&times;</button>
28
+ </div>
29
+ <div style={{ flex: 1, overflowY: 'auto', minHeight: 0 }}>
30
+ {parentPath !== null && (
31
+ <button onClick={() => setCurrentPath(parentPath)} style={{ width: '100%', padding: '4px 8px', background: 'none', border: 'none', borderBottom: '1px solid var(--border)', textAlign: 'left', cursor: 'pointer', fontFamily: 'var(--font)', fontSize: 11, color: 'var(--text-muted)' }}>
32
+ ..
33
+ </button>
34
+ )}
35
+ {dirs.map((d) => (
36
+ <button key={d.path} onClick={() => setCurrentPath(d.path)} style={{ width: '100%', padding: '4px 8px', background: 'none', border: 'none', borderBottom: '1px solid var(--border)', textAlign: 'left', cursor: 'pointer', fontFamily: 'var(--font)', fontSize: 11, color: 'var(--text-primary)' }}>
37
+ {d.name}{d.hasChildren ? '/' : ''}
38
+ </button>
39
+ ))}
40
+ </div>
41
+ <div style={{ padding: '4px 8px', borderTop: '1px solid var(--border)', flexShrink: 0 }}>
42
+ <button onClick={() => { onSelect(currentPath); onClose(); }} style={{ width: '100%', padding: '4px 8px', background: 'var(--accent)', color: 'var(--bg-base)', border: 'none', borderRadius: 3, fontSize: 11, fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--font)' }}>
43
+ Select This Directory
44
+ </button>
45
+ </div>
46
+ </div>
47
+ );
48
+ }
6
49
 
7
50
  export default function AgentActions({ agent }) {
8
51
  const killAgent = useGrooveStore((s) => s.killAgent);
@@ -17,6 +60,9 @@ export default function AgentActions({ agent }) {
17
60
  const [editPrompt, setEditPrompt] = useState('');
18
61
  const [editingPrompt, setEditingPrompt] = useState(false);
19
62
  const [selectedModel, setSelectedModel] = useState(agent.model || '');
63
+ const [editingDir, setEditingDir] = useState(false);
64
+ const [dirInput, setDirInput] = useState(agent.workingDir || '');
65
+ const [showDirPicker, setShowDirPicker] = useState(false);
20
66
  const [providerList, setProviderList] = useState([]);
21
67
  const [installedSkills, setInstalledSkills] = useState([]);
22
68
  const [showSkillPicker, setShowSkillPicker] = useState(false);
@@ -149,10 +195,40 @@ export default function AgentActions({ agent }) {
149
195
  }
150
196
  }
151
197
 
198
+ const [editingName, setEditingName] = useState(false);
199
+ const [nameInput, setNameInput] = useState(agent.name || '');
200
+
152
201
  return (
153
202
  <div style={styles.container}>
203
+ {/* Agent Name */}
204
+ <div style={styles.sectionLabel}>NAME</div>
205
+ {!editingName ? (
206
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
207
+ <span style={{ flex: 1, fontSize: 13, fontWeight: 600, color: 'var(--text-bright)' }}>{agent.name}</span>
208
+ <button onClick={() => { setEditingName(true); setNameInput(agent.name || ''); }} style={styles.editBtn}>Rename</button>
209
+ </div>
210
+ ) : (
211
+ <div>
212
+ <input style={styles.textarea} value={nameInput} onChange={(e) => setNameInput(e.target.value)}
213
+ placeholder="Agent name..." autoFocus
214
+ onKeyDown={(e) => {
215
+ if (e.key === 'Enter' && nameInput.trim()) {
216
+ fetch(`/api/agents/${agent.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: nameInput.trim() }) });
217
+ showStatus('renamed'); setEditingName(false);
218
+ }
219
+ }} />
220
+ <div style={{ display: 'flex', gap: 6, marginTop: 6 }}>
221
+ <button onClick={() => {
222
+ fetch(`/api/agents/${agent.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: nameInput.trim() }) });
223
+ showStatus('renamed'); setEditingName(false);
224
+ }} style={styles.saveBtn} disabled={!nameInput.trim()}>Save</button>
225
+ <button onClick={() => setEditingName(false)} style={styles.cancelBtn}>Cancel</button>
226
+ </div>
227
+ </div>
228
+ )}
229
+
154
230
  {/* Lifecycle controls */}
155
- <div style={styles.sectionLabel}>LIFECYCLE</div>
231
+ <div style={{ ...styles.sectionLabel, marginTop: 20 }}>LIFECYCLE</div>
156
232
 
157
233
  <div style={styles.btnGrid}>
158
234
  {isAlive && (
@@ -216,6 +292,59 @@ export default function AgentActions({ agent }) {
216
292
  </select>
217
293
  <div style={styles.fieldHint}>Changes take effect on next rotation</div>
218
294
 
295
+ {/* Working directory */}
296
+ <div style={{ ...styles.sectionLabel, marginTop: 20 }}>DIRECTORY</div>
297
+ {!editingDir ? (
298
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
299
+ <div style={{ flex: 1, fontSize: 11, color: agent.workingDir ? 'var(--text-primary)' : 'var(--text-muted)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
300
+ {agent.workingDir || 'project root'}
301
+ </div>
302
+ <button onClick={() => { setEditingDir(true); setDirInput(agent.workingDir || ''); }} style={styles.editBtn}>
303
+ Change
304
+ </button>
305
+ </div>
306
+ ) : (
307
+ <div>
308
+ <div style={{ display: 'flex', gap: 6 }}>
309
+ <input
310
+ style={{ ...styles.textarea, flex: 1 }}
311
+ value={dirInput}
312
+ onChange={(e) => setDirInput(e.target.value)}
313
+ placeholder="/absolute/path/to/project"
314
+ autoFocus
315
+ />
316
+ <button onClick={() => setShowDirPicker(true)} style={{ ...styles.editBtn, flexShrink: 0, marginTop: 0 }}>
317
+ Browse
318
+ </button>
319
+ </div>
320
+ {showDirPicker && (
321
+ <SystemDirPicker
322
+ initial={dirInput}
323
+ onSelect={(path) => { setDirInput(path); setShowDirPicker(false); }}
324
+ onClose={() => setShowDirPicker(false)}
325
+ />
326
+ )}
327
+ <div style={{ display: 'flex', gap: 6, marginTop: 6 }}>
328
+ <button onClick={async () => {
329
+ try {
330
+ await fetch(`/api/agents/${agent.id}`, {
331
+ method: 'PATCH', headers: { 'Content-Type': 'application/json' },
332
+ body: JSON.stringify({ workingDir: dirInput.trim() || null }),
333
+ });
334
+ showStatus(`directory set — takes effect on next rotation/restart`);
335
+ setEditingDir(false);
336
+ } catch (err) { showStatus(`failed: ${err.message}`); }
337
+ }} style={styles.saveBtn}>
338
+ Save
339
+ </button>
340
+ <button onClick={() => { setEditingDir(false); setShowDirPicker(false); }} style={styles.cancelBtn}>
341
+ Cancel
342
+ </button>
343
+ </div>
344
+ <div style={styles.fieldHint}>Takes effect on next rotation or restart</div>
345
+ </div>
346
+ )}
347
+
219
348
  {/* Prompt modification */}
220
349
  <div style={{ ...styles.sectionLabel, marginTop: 20 }}>PROMPT</div>
221
350
  {agent.prompt && !editingPrompt && (
@@ -111,7 +111,7 @@ export default function AgentChat({ agent }) {
111
111
  </div>
112
112
 
113
113
  {/* Launch Team button — shown when planner completes */}
114
- {agent.role === 'planner' && agent.status === 'completed' && (
114
+ {agent.role === 'planner' && (agent.status === 'completed' || agent.status === 'crashed' || agent.status === 'stopped') && (
115
115
  <LaunchTeamButton showStatus={showStatus} />
116
116
  )}
117
117
 
@@ -137,6 +137,25 @@ export default function AgentChat({ agent }) {
137
137
  >
138
138
  Send
139
139
  </button>
140
+ <button
141
+ type="button"
142
+ onClick={() => {
143
+ const { chatHistory, activityLog } = useGrooveStore.getState();
144
+ const newChat = { ...chatHistory }; delete newChat[agent.id];
145
+ const newLog = { ...activityLog }; delete newLog[agent.id];
146
+ useGrooveStore.setState({ chatHistory: newChat, activityLog: newLog });
147
+ try { localStorage.setItem('groove:chatHistory', JSON.stringify(newChat)); } catch {}
148
+ try { localStorage.setItem('groove:activityLog', JSON.stringify(newLog)); } catch {}
149
+ }}
150
+ title="Clear chat history"
151
+ style={{
152
+ background: 'none', border: 'none', color: 'var(--text-muted)',
153
+ fontSize: 11, cursor: 'pointer', fontFamily: 'var(--font)',
154
+ padding: '4px 6px', flexShrink: 0,
155
+ }}
156
+ >
157
+ Clear
158
+ </button>
140
159
  </div>
141
160
  </div>
142
161
  );
@@ -183,10 +202,14 @@ function LaunchTeamButton({ showStatus }) {
183
202
  const [launched, setLaunched] = useState(false);
184
203
 
185
204
  useEffect(() => {
186
- fetch('/api/recommended-team')
205
+ const load = () => fetch('/api/recommended-team')
187
206
  .then((r) => r.json())
188
- .then((d) => { if (d.exists && d.agents.length > 0) setTeam(d.agents); })
207
+ .then((d) => { setTeam(d.exists && d.agents.length > 0 ? d.agents : null); })
189
208
  .catch(() => {});
209
+ load();
210
+ // Re-check every 5s in case planner just finished writing a new team
211
+ const interval = setInterval(load, 5000);
212
+ return () => clearInterval(interval);
190
213
  }, []);
191
214
 
192
215
  async function handleLaunch() {
@@ -208,16 +231,27 @@ function LaunchTeamButton({ showStatus }) {
208
231
 
209
232
  if (!team || launched) return null;
210
233
 
234
+ const phase1 = team.filter((a) => !a.phase || a.phase === 1);
235
+ const phase2 = team.filter((a) => a.phase === 2);
236
+
211
237
  return (
212
238
  <div style={styles.launchBox}>
213
- <div style={styles.launchHeader}>Recommended Team ({team.length} agents)</div>
239
+ <div style={styles.launchHeader}>Recommended Team</div>
214
240
  <div style={styles.launchList}>
215
- {team.map((a, i) => (
216
- <div key={i} style={styles.launchAgent}>
241
+ {phase1.map((a, i) => (
242
+ <div key={`p1-${i}`} style={styles.launchAgent}>
243
+ <span style={styles.launchPhase}>1</span>
217
244
  <span style={styles.launchRole}>{a.role}</span>
218
245
  <span style={styles.launchPrompt}>{(a.prompt || '').slice(0, 80)}{(a.prompt || '').length > 80 ? '...' : ''}</span>
219
246
  </div>
220
247
  ))}
248
+ {phase2.map((a, i) => (
249
+ <div key={`p2-${i}`} style={{ ...styles.launchAgent, opacity: 0.7 }}>
250
+ <span style={{ ...styles.launchPhase, background: 'var(--bg-active)', color: 'var(--text-dim)' }}>2</span>
251
+ <span style={styles.launchRole}>{a.role} <span style={{ fontWeight: 400, color: 'var(--text-dim)', fontSize: 9 }}>auto after phase 1</span></span>
252
+ <span style={styles.launchPrompt}>{(a.prompt || '').slice(0, 80)}{(a.prompt || '').length > 80 ? '...' : ''}</span>
253
+ </div>
254
+ ))}
221
255
  </div>
222
256
  <button
223
257
  type="button"
@@ -233,7 +267,7 @@ function LaunchTeamButton({ showStatus }) {
233
267
 
234
268
  // ── FORMATTED TEXT — renders markdown-like agent output cleanly ──
235
269
 
236
- function FormattedText({ text }) {
270
+ export function FormattedText({ text }) {
237
271
  if (!text) return null;
238
272
  const lines = text.split('\n');
239
273
 
@@ -464,6 +498,12 @@ const styles = {
464
498
  display: 'flex', alignItems: 'baseline', gap: 6,
465
499
  fontSize: 10, padding: '2px 0',
466
500
  },
501
+ launchPhase: {
502
+ width: 16, height: 16, borderRadius: 3, flexShrink: 0,
503
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
504
+ fontSize: 9, fontWeight: 700,
505
+ background: 'rgba(51, 175, 188, 0.15)', color: 'var(--accent)',
506
+ },
467
507
  launchRole: {
468
508
  fontWeight: 600, color: 'var(--accent)', minWidth: 60,
469
509
  },