groove-dev 0.27.131 → 0.27.134

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 (70) hide show
  1. package/AGENT_ORCHESTRATION.md +375 -0
  2. package/moe-training/shared/envelope-schema.js +1 -1
  3. package/node_modules/@groove-dev/cli/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/package.json +1 -1
  5. package/node_modules/@groove-dev/daemon/src/index.js +3 -1
  6. package/node_modules/@groove-dev/daemon/src/introducer.js +48 -4
  7. package/node_modules/@groove-dev/daemon/src/llama-server.js +4 -4
  8. package/node_modules/@groove-dev/daemon/src/model-lab.js +8 -0
  9. package/node_modules/@groove-dev/daemon/src/preview.js +85 -58
  10. package/node_modules/@groove-dev/daemon/src/process.js +9 -0
  11. package/node_modules/@groove-dev/daemon/src/terminal-pty.js +24 -14
  12. package/node_modules/@groove-dev/daemon/src/validate.js +0 -4
  13. package/{packages/gui/dist/assets/codemirror-CFF1Lrnz.js → node_modules/@groove-dev/gui/dist/assets/codemirror-DRQdprYi.js} +11 -11
  14. package/node_modules/@groove-dev/gui/dist/assets/index-BgQL4bNl.css +1 -0
  15. package/{packages/gui/dist/assets/index-BiB9oY9U.js → node_modules/@groove-dev/gui/dist/assets/index-Dozp69tK.js} +1721 -1721
  16. package/node_modules/@groove-dev/gui/dist/index.html +3 -3
  17. package/node_modules/@groove-dev/gui/package.json +1 -1
  18. package/node_modules/@groove-dev/gui/src/app.css +6 -6
  19. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +12 -1
  20. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +15 -5
  21. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +6 -6
  22. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +11 -9
  23. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +26 -3
  24. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +6 -6
  25. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +20 -8
  26. package/node_modules/@groove-dev/gui/src/components/lab/chat-playground.jsx +10 -1
  27. package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +4 -4
  28. package/node_modules/@groove-dev/gui/src/components/lab/system-prompt-editor.jsx +17 -3
  29. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +2 -4
  30. package/node_modules/@groove-dev/gui/src/components/preview/preview-toolbar.jsx +8 -6
  31. package/node_modules/@groove-dev/gui/src/stores/groove.js +82 -15
  32. package/node_modules/@groove-dev/gui/src/views/agents.jsx +82 -74
  33. package/node_modules/@groove-dev/gui/src/views/editor.jsx +11 -9
  34. package/node_modules/moe-training/shared/envelope-schema.js +1 -1
  35. package/package.json +1 -1
  36. package/packages/cli/package.json +1 -1
  37. package/packages/daemon/package.json +1 -1
  38. package/packages/daemon/src/index.js +3 -1
  39. package/packages/daemon/src/introducer.js +48 -4
  40. package/packages/daemon/src/llama-server.js +4 -4
  41. package/packages/daemon/src/model-lab.js +8 -0
  42. package/packages/daemon/src/preview.js +85 -58
  43. package/packages/daemon/src/process.js +9 -0
  44. package/packages/daemon/src/terminal-pty.js +24 -14
  45. package/packages/daemon/src/validate.js +0 -4
  46. package/{node_modules/@groove-dev/gui/dist/assets/codemirror-CFF1Lrnz.js → packages/gui/dist/assets/codemirror-DRQdprYi.js} +11 -11
  47. package/packages/gui/dist/assets/index-BgQL4bNl.css +1 -0
  48. package/{node_modules/@groove-dev/gui/dist/assets/index-BiB9oY9U.js → packages/gui/dist/assets/index-Dozp69tK.js} +1721 -1721
  49. package/packages/gui/dist/index.html +3 -3
  50. package/packages/gui/package.json +1 -1
  51. package/packages/gui/src/app.css +6 -6
  52. package/packages/gui/src/components/agents/agent-chat.jsx +12 -1
  53. package/packages/gui/src/components/agents/agent-feed.jsx +15 -5
  54. package/packages/gui/src/components/agents/agent-file-tree.jsx +6 -6
  55. package/packages/gui/src/components/agents/workspace-mode.jsx +11 -9
  56. package/packages/gui/src/components/editor/code-editor.jsx +26 -3
  57. package/packages/gui/src/components/editor/file-tree.jsx +6 -6
  58. package/packages/gui/src/components/editor/terminal.jsx +20 -8
  59. package/packages/gui/src/components/lab/chat-playground.jsx +10 -1
  60. package/packages/gui/src/components/lab/lab-assistant.jsx +4 -4
  61. package/packages/gui/src/components/lab/system-prompt-editor.jsx +17 -3
  62. package/packages/gui/src/components/layout/terminal-panel.jsx +2 -4
  63. package/packages/gui/src/components/preview/preview-toolbar.jsx +8 -6
  64. package/packages/gui/src/stores/groove.js +82 -15
  65. package/packages/gui/src/views/agents.jsx +82 -74
  66. package/packages/gui/src/views/editor.jsx +11 -9
  67. package/CENTRAL_COMMAND_REBUILD.md +0 -689
  68. package/MERKLE_TREE_ARCHITECTURE.md +0 -354
  69. package/node_modules/@groove-dev/gui/dist/assets/index-CeyDFVub.css +0 -1
  70. package/packages/gui/dist/assets/index-CeyDFVub.css +0 -1
@@ -6,12 +6,12 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <link rel="icon" type="image/png" href="/favicon.png" />
8
8
  <title>Groove GUI</title>
9
- <script type="module" crossorigin src="/assets/index-BiB9oY9U.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-Dozp69tK.js"></script>
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendor-26L3JoZv.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/reactflow-DoBZjiHE.js">
12
- <link rel="modulepreload" crossorigin href="/assets/codemirror-CFF1Lrnz.js">
12
+ <link rel="modulepreload" crossorigin href="/assets/codemirror-DRQdprYi.js">
13
13
  <link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
14
- <link rel="stylesheet" crossorigin href="/assets/index-CeyDFVub.css">
14
+ <link rel="stylesheet" crossorigin href="/assets/index-BgQL4bNl.css">
15
15
  </head>
16
16
  <body>
17
17
  <div id="root"></div>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.131",
3
+ "version": "0.27.134",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -34,12 +34,12 @@
34
34
 
35
35
  /* Semantic */
36
36
  --color-accent: #33afbc;
37
- --color-success: #4ae168;
38
- --color-warning: #e5c07b;
39
- --color-danger: #e06c75;
40
- --color-info: #61afef;
41
- --color-purple: #c678dd;
42
- --color-orange: #d19a66;
37
+ --color-success: #73c991;
38
+ --color-warning: #cda869;
39
+ --color-danger: #d4736e;
40
+ --color-info: #7ab0df;
41
+ --color-purple: #b07fd5;
42
+ --color-orange: #c4956a;
43
43
 
44
44
  /* Fonts */
45
45
  --font-sans: 'Inter Variable', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
@@ -109,9 +109,20 @@ export function AgentChat({ agent }) {
109
109
  const scrollRef = useRef(null);
110
110
  const inputRef = useRef(null);
111
111
  const fileInputRef = useRef(null);
112
+ const isAtBottomRef = useRef(true);
112
113
 
113
114
  useEffect(() => {
114
- if (scrollRef.current) {
115
+ const el = scrollRef.current;
116
+ if (!el) return;
117
+ function handleScroll() {
118
+ isAtBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 60;
119
+ }
120
+ el.addEventListener('scroll', handleScroll);
121
+ return () => el.removeEventListener('scroll', handleScroll);
122
+ }, []);
123
+
124
+ useEffect(() => {
125
+ if (isAtBottomRef.current && scrollRef.current) {
115
126
  scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
116
127
  }
117
128
  }, [chatHistory.length, activityLog.length]);
@@ -320,13 +320,12 @@ function ActivityGroup({ entries, isLive }) {
320
320
  }
321
321
 
322
322
  const current = entries[Math.min(cycleIdx, entries.length - 1)];
323
- const display = current.text?.length > 60 ? current.text.slice(0, 60) + '...' : current.text;
324
323
 
325
324
  return (
326
- <div className="inline-flex items-center gap-2 px-3 py-2 max-w-[280px] rounded-md bg-surface-3/50 border border-border-subtle/30">
325
+ <div className="flex items-center gap-2 px-3 py-2 w-full rounded-md bg-surface-3/50 border border-border-subtle/30">
327
326
  <Loader2 size={11} className="text-accent animate-spin flex-shrink-0" />
328
- <span className="text-[11px] text-text-2 font-mono truncate transition-opacity duration-300">
329
- {display}
327
+ <span className="text-[11px] text-text-2 font-mono truncate min-w-0 flex-1 transition-opacity duration-300">
328
+ {current.text}
330
329
  </span>
331
330
  {entries.length > 1 && (
332
331
  <span className="text-[10px] text-text-4 font-mono flex-shrink-0">{entries.length}</span>
@@ -485,6 +484,17 @@ export function AgentFeed({ agent }) {
485
484
  const scrollRef = useRef(null);
486
485
  const inputRef = useRef(null);
487
486
  const fileInputRef = useRef(null);
487
+ const isAtBottomRef = useRef(true);
488
+
489
+ useEffect(() => {
490
+ const el = scrollRef.current;
491
+ if (!el) return;
492
+ function handleScroll() {
493
+ isAtBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 60;
494
+ }
495
+ el.addEventListener('scroll', handleScroll);
496
+ return () => el.removeEventListener('scroll', handleScroll);
497
+ }, []);
488
498
 
489
499
  const onDragStart = useCallback((e) => {
490
500
  e.preventDefault();
@@ -542,7 +552,7 @@ export function AgentFeed({ agent }) {
542
552
  }, [chatHistory, activityLog]);
543
553
 
544
554
  useEffect(() => {
545
- if (scrollRef.current) {
555
+ if (isAtBottomRef.current && scrollRef.current) {
546
556
  requestAnimationFrame(() => {
547
557
  if (scrollRef.current) {
548
558
  scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
@@ -7,11 +7,11 @@ import { ChevronRight, ChevronDown, File, Folder, FolderOpen, FileEdit, Eye, Fil
7
7
  import { ScrollArea } from '../ui/scroll-area';
8
8
 
9
9
  const FILE_COLORS = {
10
- js: 'text-warning', jsx: 'text-warning', ts: 'text-info', tsx: 'text-info',
11
- css: 'text-info', html: 'text-orange', json: 'text-warning',
12
- md: 'text-text-2', py: 'text-success', rs: 'text-orange',
13
- go: 'text-accent', sh: 'text-success', yaml: 'text-danger', yml: 'text-danger',
14
- sql: 'text-purple', xml: 'text-orange', svg: 'text-warning',
10
+ js: 'text-text-2', jsx: 'text-text-2', ts: 'text-text-2', tsx: 'text-text-2',
11
+ css: 'text-text-3', html: 'text-text-3', json: 'text-text-3',
12
+ md: 'text-text-3', py: 'text-text-2', rs: 'text-text-3',
13
+ go: 'text-text-2', sh: 'text-text-3', yaml: 'text-text-3', yml: 'text-text-3',
14
+ sql: 'text-text-3', xml: 'text-text-3', svg: 'text-text-3',
15
15
  };
16
16
 
17
17
  function getFileColor(name) {
@@ -135,7 +135,7 @@ function TreeEntry({ entry, depth, onOpen, expandedDirs, onToggleDir, onContextM
135
135
  onDoubleClick={handleCtxMenu}
136
136
  onContextMenu={handleCtxMenu}
137
137
  className={cn(
138
- 'w-full flex items-center gap-1.5 py-1 text-xs font-sans cursor-pointer',
138
+ 'w-full flex items-center gap-1.5 py-[3px] text-[13px] font-sans cursor-pointer',
139
139
  'hover:bg-surface-4/50 transition-colors text-left',
140
140
  isDragging && 'opacity-50',
141
141
  isDragOver && 'bg-accent/15 ring-1 ring-accent/50 rounded',
@@ -226,17 +226,19 @@ export function WorkspaceMode() {
226
226
  />
227
227
  </div>
228
228
 
229
+ {/* Sidebar expand rail */}
230
+ {sidebarCollapsed && (
231
+ <button
232
+ onClick={() => setSidebarCollapsed(false)}
233
+ className="flex-shrink-0 w-6 flex items-center justify-center border-r border-border bg-surface-2 text-text-4 hover:text-text-0 hover:bg-surface-3 transition-colors cursor-pointer"
234
+ title="Show sidebar"
235
+ >
236
+ <PanelLeftOpen size={14} />
237
+ </button>
238
+ )}
239
+
229
240
  {/* Editor Area */}
230
241
  <div className="flex-1 flex flex-col min-w-0 bg-surface-1">
231
- {sidebarCollapsed && (
232
- <button
233
- onClick={() => setSidebarCollapsed(false)}
234
- className="absolute top-2 left-14 z-10 w-7 h-7 flex items-center justify-center rounded-md text-text-3 hover:text-text-0 hover:bg-surface-3 transition-colors cursor-pointer"
235
- title="Show sidebar"
236
- >
237
- <PanelLeftOpen size={15} />
238
- </button>
239
- )}
240
242
  {workspaceReviewMode ? (
241
243
  <CodeReview agentId={agent.id} />
242
244
  ) : (
@@ -3,10 +3,10 @@ import { useRef, useEffect } from 'react';
3
3
  import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter } from '@codemirror/view';
4
4
  import { EditorState, Compartment } from '@codemirror/state';
5
5
  import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
6
- import { bracketMatching, syntaxHighlighting } from '@codemirror/language';
6
+ import { bracketMatching, syntaxHighlighting, HighlightStyle } from '@codemirror/language';
7
7
  import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
8
8
  import { autocompletion } from '@codemirror/autocomplete';
9
- import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
9
+ import { tags as t } from '@lezer/highlight';
10
10
  import { javascript } from '@codemirror/lang-javascript';
11
11
  import { css } from '@codemirror/lang-css';
12
12
  import { html } from '@codemirror/lang-html';
@@ -24,6 +24,29 @@ const LANGS = {
24
24
  python: () => python(),
25
25
  };
26
26
 
27
+ const grooveHighlightStyle = HighlightStyle.define([
28
+ { tag: t.keyword, color: '#b07fd5' },
29
+ { tag: [t.name, t.deleted, t.character, t.macroName], color: '#d4d8e0' },
30
+ { tag: [t.function(t.variableName), t.labelName], color: '#7ab0df' },
31
+ { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: '#c4956a' },
32
+ { tag: [t.definition(t.name), t.separator], color: '#bcc2cd' },
33
+ { tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: '#cda869' },
34
+ { tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.special(t.string)], color: '#73c991' },
35
+ { tag: [t.meta, t.comment], color: '#6e7681', fontStyle: 'italic' },
36
+ { tag: t.strong, fontWeight: 'bold' },
37
+ { tag: t.emphasis, fontStyle: 'italic' },
38
+ { tag: t.strikethrough, textDecoration: 'line-through' },
39
+ { tag: t.link, color: '#7ab0df', textDecoration: 'underline' },
40
+ { tag: t.heading, fontWeight: '400', color: '#bcc2cd' },
41
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#c4956a' },
42
+ { tag: [t.processingInstruction, t.string, t.inserted], color: '#73c991' },
43
+ { tag: t.invalid, color: '#d4736e' },
44
+ { tag: t.propertyName, color: '#7ab0df' },
45
+ { tag: [t.tagName], color: '#d4736e' },
46
+ { tag: t.attributeName, color: '#cda869' },
47
+ { tag: t.attributeValue, color: '#73c991' },
48
+ ]);
49
+
27
50
  // Custom theme overrides to match our design tokens
28
51
  const grooveTheme = EditorView.theme({
29
52
  '&': { backgroundColor: '#13161b', color: '#d4d8e0', fontFamily: 'var(--font-mono)', fontSize: '12px', height: '100%', lineHeight: '1.6' },
@@ -98,7 +121,7 @@ export function CodeEditor({ content, language, onChange, onSave, onCursorChange
98
121
  autocompletion(),
99
122
  keymap.of([...defaultKeymap, ...historyKeymap, ...searchKeymap]),
100
123
  saveKeymap,
101
- syntaxHighlighting(oneDarkHighlightStyle),
124
+ syntaxHighlighting(grooveHighlightStyle),
102
125
  grooveTheme,
103
126
  langCompartment.current.of(langExt()),
104
127
  EditorView.updateListener.of((update) => {
@@ -11,11 +11,11 @@ import {
11
11
  import { ScrollArea } from '../ui/scroll-area';
12
12
 
13
13
  const FILE_COLORS = {
14
- js: 'text-warning', jsx: 'text-warning', ts: 'text-info', tsx: 'text-info',
15
- css: 'text-info', html: 'text-orange', json: 'text-warning',
16
- md: 'text-text-2', py: 'text-success', rs: 'text-orange',
17
- go: 'text-accent', sh: 'text-success', yaml: 'text-danger', yml: 'text-danger',
18
- sql: 'text-purple', xml: 'text-orange', svg: 'text-warning',
14
+ js: 'text-text-2', jsx: 'text-text-2', ts: 'text-text-2', tsx: 'text-text-2',
15
+ css: 'text-text-3', html: 'text-text-3', json: 'text-text-3',
16
+ md: 'text-text-3', py: 'text-text-2', rs: 'text-text-3',
17
+ go: 'text-text-2', sh: 'text-text-3', yaml: 'text-text-3', yml: 'text-text-3',
18
+ sql: 'text-text-3', xml: 'text-text-3', svg: 'text-text-3',
19
19
  };
20
20
 
21
21
  function getFileColor(name) {
@@ -130,7 +130,7 @@ function TreeNode({ entry, depth = 0, activePath, onFileClick, onDirToggle, expa
130
130
  onDoubleClick={handleContextMenu}
131
131
  onContextMenu={handleContextMenu}
132
132
  className={cn(
133
- 'w-full flex items-center gap-1.5 py-1 text-xs font-sans cursor-pointer',
133
+ 'w-full flex items-center gap-1.5 py-[3px] text-[13px] font-sans cursor-pointer',
134
134
  'hover:bg-surface-5 transition-colors text-left select-none',
135
135
  isActive && 'bg-accent/10 text-text-0',
136
136
  !isActive && 'text-text-1',
@@ -20,8 +20,9 @@ const THEME = {
20
20
  };
21
21
 
22
22
  let tabCounter = 0;
23
+ let spawnSeq = 0;
23
24
 
24
- function TerminalInstance({ tabId, visible }) {
25
+ function TerminalInstance({ tabId, visible, registerKill }) {
25
26
  const containerRef = useRef(null);
26
27
  const termRef = useRef(null);
27
28
  const fitRef = useRef(null);
@@ -29,6 +30,15 @@ function TerminalInstance({ tabId, visible }) {
29
30
  const handlerRef = useRef(null);
30
31
  const mountedRef = useRef(false);
31
32
 
33
+ useEffect(() => {
34
+ registerKill?.(tabId, () => {
35
+ const ws = useGrooveStore.getState().ws;
36
+ if (ws?.readyState === WebSocket.OPEN && termIdRef.current) {
37
+ ws.send(JSON.stringify({ type: 'terminal:kill', id: termIdRef.current }));
38
+ }
39
+ });
40
+ }, [tabId, registerKill]);
41
+
32
42
  useEffect(() => {
33
43
  if (!containerRef.current || mountedRef.current) return;
34
44
  mountedRef.current = true;
@@ -67,13 +77,14 @@ function TerminalInstance({ tabId, visible }) {
67
77
  return;
68
78
  }
69
79
 
70
- ws.send(JSON.stringify({ type: 'terminal:spawn', cols: term.cols, rows: term.rows }));
80
+ const requestId = `spawn-${++spawnSeq}`;
81
+ ws.send(JSON.stringify({ type: 'terminal:spawn', cols: term.cols, rows: term.rows, requestId }));
71
82
 
72
83
  function onMessage(event) {
73
84
  let msg;
74
85
  try { msg = JSON.parse(event.data); } catch { return; }
75
86
 
76
- if (msg.type === 'terminal:spawned' && !termIdRef.current) {
87
+ if (msg.type === 'terminal:spawned' && msg.requestId === requestId && !termIdRef.current) {
77
88
  termIdRef.current = msg.id;
78
89
  } else if (msg.type === 'terminal:output' && msg.id === termIdRef.current) {
79
90
  term.write(msg.data);
@@ -114,10 +125,6 @@ function TerminalInstance({ tabId, visible }) {
114
125
 
115
126
  return () => {
116
127
  observer.disconnect();
117
- const ws = useGrooveStore.getState().ws;
118
- if (ws?.readyState === WebSocket.OPEN && termIdRef.current) {
119
- ws.send(JSON.stringify({ type: 'terminal:kill', id: termIdRef.current }));
120
- }
121
128
  if (handlerRef.current) {
122
129
  handlerRef.current.ws.removeEventListener('message', handlerRef.current.handler);
123
130
  }
@@ -155,6 +162,9 @@ export function TerminalManager() {
155
162
 
156
163
  const [tabs, setTabs] = useState([{ id: 'term-0', label: 'Terminal' }]);
157
164
  const [activeTab, setActiveTab] = useState('term-0');
165
+ const killFns = useRef({});
166
+
167
+ const registerKill = useCallback((tabId, fn) => { killFns.current[tabId] = fn; }, []);
158
168
 
159
169
  const addTab = useCallback(() => {
160
170
  tabCounter++;
@@ -168,6 +178,8 @@ export function TerminalManager() {
168
178
  }, []);
169
179
 
170
180
  const closeTab = useCallback((id) => {
181
+ killFns.current[id]?.();
182
+ delete killFns.current[id];
171
183
  setTabs((prev) => {
172
184
  const next = prev.filter((t) => t.id !== id);
173
185
  if (next.length === 0) {
@@ -199,7 +211,7 @@ export function TerminalManager() {
199
211
  onMinimize={() => setFullHeight(false)}
200
212
  >
201
213
  {tabs.map((tab) => (
202
- <TerminalInstance key={tab.id} tabId={tab.id} visible={tab.id === activeTab} />
214
+ <TerminalInstance key={tab.id} tabId={tab.id} visible={tab.id === activeTab} registerKill={registerKill} />
203
215
  ))}
204
216
  </TerminalPanel>
205
217
  );
@@ -48,12 +48,21 @@ function UserMessage({ msg }) {
48
48
  }
49
49
 
50
50
  function AssistantMessage({ msg, streaming }) {
51
- const isStreaming = streaming && !msg.content && !msg.error;
51
+ const isStreaming = streaming && !msg.content && !msg.reasoning && !msg.error;
52
+ const isReasoning = streaming && msg.reasoning && !msg.content;
52
53
  return (
53
54
  <div>
54
55
  <div className="text-2xs text-text-3 font-sans mb-0.5 font-medium flex items-center gap-1">
55
56
  <Bot size={10} /> Assistant
56
57
  </div>
58
+ {msg.reasoning && (
59
+ <div className="border-l-2 border-text-4/30 pl-3 py-0.5 mb-1">
60
+ <div className="text-2xs font-sans text-text-3 italic whitespace-pre-wrap break-words leading-relaxed">
61
+ {msg.reasoning}
62
+ {isReasoning && <span className="inline-block w-1 h-3 bg-text-4/50 ml-0.5 animate-pulse" />}
63
+ </div>
64
+ </div>
65
+ )}
57
66
  <div className={cn(
58
67
  'border-l-2 pl-3 py-0.5',
59
68
  msg.error ? 'border-danger/40' : 'border-accent/40',
@@ -21,7 +21,7 @@ function AssistantMessage({ msg }) {
21
21
  'text-xs font-sans whitespace-pre-wrap break-words leading-relaxed',
22
22
  msg.error ? 'text-danger' : 'text-text-1',
23
23
  )}>
24
- {msg.content}
24
+ {msg.text}
25
25
  </div>
26
26
  </div>
27
27
  </div>
@@ -33,7 +33,7 @@ function UserMessage({ msg }) {
33
33
  <div className="flex justify-end">
34
34
  <div className="max-w-[85%]">
35
35
  <div className="px-3 py-2 rounded-xl rounded-br-sm bg-accent/10 border border-accent/15">
36
- <p className="text-xs text-text-0 font-sans whitespace-pre-wrap break-words leading-relaxed">{msg.content}</p>
36
+ <p className="text-xs text-text-0 font-sans whitespace-pre-wrap break-words leading-relaxed">{msg.text}</p>
37
37
  </div>
38
38
  </div>
39
39
  </div>
@@ -65,7 +65,7 @@ export function LabAssistant() {
65
65
  const el = scrollRef.current.querySelector('[data-radix-scroll-area-viewport]');
66
66
  if (el) el.scrollTop = el.scrollHeight;
67
67
  }
68
- }, [messages.length, messages[messages.length - 1]?.content]);
68
+ }, [messages.length, messages[messages.length - 1]?.text]);
69
69
 
70
70
  const handleSend = useCallback(() => {
71
71
  const text = input.trim();
@@ -120,7 +120,7 @@ export function LabAssistant() {
120
120
  </div>
121
121
  ) : (
122
122
  messages.map((msg, i) =>
123
- msg.role === 'user' ? (
123
+ msg.from === 'user' ? (
124
124
  <UserMessage key={i} msg={msg} />
125
125
  ) : (
126
126
  <AssistantMessage key={i} msg={msg} />
@@ -6,8 +6,8 @@ import { cn } from '../../lib/cn';
6
6
  import { EditorView, keymap, lineNumbers } from '@codemirror/view';
7
7
  import { EditorState } from '@codemirror/state';
8
8
  import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
9
- import { syntaxHighlighting } from '@codemirror/language';
10
- import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
9
+ import { syntaxHighlighting, HighlightStyle } from '@codemirror/language';
10
+ import { tags as t } from '@lezer/highlight';
11
11
  import { markdown } from '@codemirror/lang-markdown';
12
12
 
13
13
  const editorTheme = EditorView.theme({
@@ -20,6 +20,20 @@ const editorTheme = EditorView.theme({
20
20
  '&.cm-focused .cm-selectionBackground, .cm-selectionBackground': { backgroundColor: 'rgba(51, 175, 188, 0.15)' },
21
21
  });
22
22
 
23
+ const promptHighlightStyle = HighlightStyle.define([
24
+ { tag: t.keyword, color: '#b07fd5' },
25
+ { tag: [t.name, t.deleted, t.character, t.macroName], color: '#d4d8e0' },
26
+ { tag: [t.function(t.variableName), t.labelName], color: '#7ab0df' },
27
+ { tag: [t.meta, t.comment], color: '#6e7681', fontStyle: 'italic' },
28
+ { tag: t.strong, fontWeight: 'bold' },
29
+ { tag: t.emphasis, fontStyle: 'italic' },
30
+ { tag: t.link, color: '#7ab0df', textDecoration: 'underline' },
31
+ { tag: t.heading, fontWeight: '400', color: '#bcc2cd' },
32
+ { tag: [t.processingInstruction, t.string, t.inserted], color: '#73c991' },
33
+ { tag: [t.atom, t.bool], color: '#c4956a' },
34
+ { tag: t.invalid, color: '#d4736e' },
35
+ ]);
36
+
23
37
  export function SystemPromptEditor() {
24
38
  const systemPrompt = useGrooveStore((s) => s.labSystemPrompt);
25
39
  const setSystemPrompt = useGrooveStore((s) => s.setLabSystemPrompt);
@@ -48,7 +62,7 @@ export function SystemPromptEditor() {
48
62
  extensions: [
49
63
  lineNumbers(),
50
64
  history(),
51
- syntaxHighlighting(oneDarkHighlightStyle),
65
+ syntaxHighlighting(promptHighlightStyle),
52
66
  markdown(),
53
67
  keymap.of([...defaultKeymap, ...historyKeymap]),
54
68
  editorTheme,
@@ -48,14 +48,12 @@ export function TerminalPanel({
48
48
  document.addEventListener('mouseup', onMouseUp);
49
49
  }, [height, onHeightChange, fullHeight]);
50
50
 
51
- if (!visible) return null;
52
-
53
51
  const tabList = tabs || [{ id: 'default', label: 'Terminal' }];
54
52
 
55
53
  return (
56
54
  <div
57
- className="flex flex-col border-t border-border bg-surface-0 relative"
58
- style={fullHeight ? { flex: 1, minHeight: 0 } : { height, flexShrink: 0 }}
55
+ className={cn('flex flex-col border-t border-border bg-surface-0 relative', !visible && 'hidden')}
56
+ style={visible ? (fullHeight ? { flex: 1, minHeight: 0 } : { height, flexShrink: 0 }) : { height: 0 }}
59
57
  >
60
58
  {/* Resize handle */}
61
59
  {!fullHeight && (
@@ -15,6 +15,7 @@ export function PreviewToolbar({ onRefresh }) {
15
15
  const setPreviewDevice = useGrooveStore((s) => s.setPreviewDevice);
16
16
  const toggleScreenshotMode = useGrooveStore((s) => s.toggleScreenshotMode);
17
17
  const closePreview = useGrooveStore((s) => s.closePreview);
18
+ const stopPreview = useGrooveStore((s) => s.stopPreview);
18
19
 
19
20
  const [confirming, setConfirming] = useState(false);
20
21
  const timerRef = useRef(null);
@@ -27,7 +28,7 @@ export function PreviewToolbar({ onRefresh }) {
27
28
  if (confirming) {
28
29
  if (timerRef.current) clearTimeout(timerRef.current);
29
30
  setConfirming(false);
30
- closePreview();
31
+ stopPreview();
31
32
  } else {
32
33
  setConfirming(true);
33
34
  timerRef.current = setTimeout(() => setConfirming(false), 2000);
@@ -87,19 +88,20 @@ export function PreviewToolbar({ onRefresh }) {
87
88
  <Camera size={14} />
88
89
  </button>
89
90
 
90
- {/* Close preview two-click confirmation */}
91
+ {/* Hide preview (first click) / Stop server (second click) */}
91
92
  <button
92
- onClick={handleClose}
93
+ onClick={confirming ? handleClose : closePreview}
94
+ onContextMenu={(e) => { e.preventDefault(); setConfirming(true); timerRef.current = setTimeout(() => setConfirming(false), 3000); }}
93
95
  className={cn(
94
96
  'h-7 flex items-center justify-center rounded-md transition-all cursor-pointer',
95
97
  confirming
96
98
  ? 'px-2 gap-1.5 bg-danger/15 text-danger border border-danger/25'
97
- : 'w-7 text-text-3 hover:text-danger hover:bg-danger/10',
99
+ : 'w-7 text-text-3 hover:text-text-1 hover:bg-surface-4',
98
100
  )}
99
- title="Close Preview"
101
+ title={confirming ? 'Click to stop server' : 'Hide preview'}
100
102
  >
101
103
  {confirming ? (
102
- <span className="text-2xs font-semibold font-sans whitespace-nowrap">Close?</span>
104
+ <span className="text-2xs font-semibold font-sans whitespace-nowrap">Stop server?</span>
103
105
  ) : (
104
106
  <X size={14} />
105
107
  )}