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.
- package/AGENT_ORCHESTRATION.md +375 -0
- package/moe-training/shared/envelope-schema.js +1 -1
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/index.js +3 -1
- package/node_modules/@groove-dev/daemon/src/introducer.js +48 -4
- package/node_modules/@groove-dev/daemon/src/llama-server.js +4 -4
- package/node_modules/@groove-dev/daemon/src/model-lab.js +8 -0
- package/node_modules/@groove-dev/daemon/src/preview.js +85 -58
- package/node_modules/@groove-dev/daemon/src/process.js +9 -0
- package/node_modules/@groove-dev/daemon/src/terminal-pty.js +24 -14
- package/node_modules/@groove-dev/daemon/src/validate.js +0 -4
- package/{packages/gui/dist/assets/codemirror-CFF1Lrnz.js → node_modules/@groove-dev/gui/dist/assets/codemirror-DRQdprYi.js} +11 -11
- package/node_modules/@groove-dev/gui/dist/assets/index-BgQL4bNl.css +1 -0
- package/{packages/gui/dist/assets/index-BiB9oY9U.js → node_modules/@groove-dev/gui/dist/assets/index-Dozp69tK.js} +1721 -1721
- package/node_modules/@groove-dev/gui/dist/index.html +3 -3
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/app.css +6 -6
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +12 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +15 -5
- package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +6 -6
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +11 -9
- package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +26 -3
- package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +6 -6
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +20 -8
- package/node_modules/@groove-dev/gui/src/components/lab/chat-playground.jsx +10 -1
- package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/lab/system-prompt-editor.jsx +17 -3
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +2 -4
- package/node_modules/@groove-dev/gui/src/components/preview/preview-toolbar.jsx +8 -6
- package/node_modules/@groove-dev/gui/src/stores/groove.js +82 -15
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +82 -74
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +11 -9
- package/node_modules/moe-training/shared/envelope-schema.js +1 -1
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/index.js +3 -1
- package/packages/daemon/src/introducer.js +48 -4
- package/packages/daemon/src/llama-server.js +4 -4
- package/packages/daemon/src/model-lab.js +8 -0
- package/packages/daemon/src/preview.js +85 -58
- package/packages/daemon/src/process.js +9 -0
- package/packages/daemon/src/terminal-pty.js +24 -14
- package/packages/daemon/src/validate.js +0 -4
- package/{node_modules/@groove-dev/gui/dist/assets/codemirror-CFF1Lrnz.js → packages/gui/dist/assets/codemirror-DRQdprYi.js} +11 -11
- package/packages/gui/dist/assets/index-BgQL4bNl.css +1 -0
- package/{node_modules/@groove-dev/gui/dist/assets/index-BiB9oY9U.js → packages/gui/dist/assets/index-Dozp69tK.js} +1721 -1721
- package/packages/gui/dist/index.html +3 -3
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/app.css +6 -6
- package/packages/gui/src/components/agents/agent-chat.jsx +12 -1
- package/packages/gui/src/components/agents/agent-feed.jsx +15 -5
- package/packages/gui/src/components/agents/agent-file-tree.jsx +6 -6
- package/packages/gui/src/components/agents/workspace-mode.jsx +11 -9
- package/packages/gui/src/components/editor/code-editor.jsx +26 -3
- package/packages/gui/src/components/editor/file-tree.jsx +6 -6
- package/packages/gui/src/components/editor/terminal.jsx +20 -8
- package/packages/gui/src/components/lab/chat-playground.jsx +10 -1
- package/packages/gui/src/components/lab/lab-assistant.jsx +4 -4
- package/packages/gui/src/components/lab/system-prompt-editor.jsx +17 -3
- package/packages/gui/src/components/layout/terminal-panel.jsx +2 -4
- package/packages/gui/src/components/preview/preview-toolbar.jsx +8 -6
- package/packages/gui/src/stores/groove.js +82 -15
- package/packages/gui/src/views/agents.jsx +82 -74
- package/packages/gui/src/views/editor.jsx +11 -9
- package/CENTRAL_COMMAND_REBUILD.md +0 -689
- package/MERKLE_TREE_ARCHITECTURE.md +0 -354
- package/node_modules/@groove-dev/gui/dist/assets/index-CeyDFVub.css +0 -1
- 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-
|
|
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-
|
|
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-
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BgQL4bNl.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -34,12 +34,12 @@
|
|
|
34
34
|
|
|
35
35
|
/* Semantic */
|
|
36
36
|
--color-accent: #33afbc;
|
|
37
|
-
--color-success: #
|
|
38
|
-
--color-warning: #
|
|
39
|
-
--color-danger: #
|
|
40
|
-
--color-info: #
|
|
41
|
-
--color-purple: #
|
|
42
|
-
--color-orange: #
|
|
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
|
-
|
|
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="
|
|
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
|
-
{
|
|
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-
|
|
11
|
-
css: 'text-
|
|
12
|
-
md: 'text-text-
|
|
13
|
-
go: 'text-
|
|
14
|
-
sql: 'text-
|
|
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-
|
|
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 {
|
|
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(
|
|
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-
|
|
15
|
-
css: 'text-
|
|
16
|
-
md: 'text-text-
|
|
17
|
-
go: 'text-
|
|
18
|
-
sql: 'text-
|
|
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-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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]?.
|
|
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.
|
|
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 {
|
|
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(
|
|
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=
|
|
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
|
-
|
|
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
|
-
{/*
|
|
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-
|
|
99
|
+
: 'w-7 text-text-3 hover:text-text-1 hover:bg-surface-4',
|
|
98
100
|
)}
|
|
99
|
-
title=
|
|
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">
|
|
104
|
+
<span className="text-2xs font-semibold font-sans whitespace-nowrap">Stop server?</span>
|
|
103
105
|
) : (
|
|
104
106
|
<X size={14} />
|
|
105
107
|
)}
|