groove-dev 0.27.141 → 0.27.143

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 (74) hide show
  1. package/CLAUDE.md +7 -0
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +18 -7
  5. package/node_modules/@groove-dev/daemon/src/introducer.js +1 -1
  6. package/node_modules/@groove-dev/daemon/src/journalist.js +3 -2
  7. package/node_modules/@groove-dev/daemon/src/keeper.js +2 -2
  8. package/node_modules/@groove-dev/daemon/src/memory.js +8 -5
  9. package/node_modules/@groove-dev/daemon/src/process.js +5 -16
  10. package/node_modules/@groove-dev/daemon/src/rotator.js +25 -8
  11. package/node_modules/@groove-dev/gui/dist/assets/{codemirror-BQqYnZfL.js → codemirror-BYKpdS2W.js} +10 -10
  12. package/node_modules/@groove-dev/gui/dist/assets/index-CCVvAoQn.css +1 -0
  13. package/node_modules/@groove-dev/gui/dist/assets/index-DGIv_TRm.js +984 -0
  14. package/node_modules/@groove-dev/gui/dist/index.html +3 -3
  15. package/node_modules/@groove-dev/gui/package.json +1 -1
  16. package/node_modules/@groove-dev/gui/src/app.jsx +0 -2
  17. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +73 -17
  18. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +8 -2
  19. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +5 -6
  20. package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +79 -5
  21. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +2 -53
  22. package/node_modules/@groove-dev/gui/src/components/dashboard/context-gauges.jsx +111 -0
  23. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +70 -33
  24. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +2 -68
  25. package/node_modules/@groove-dev/gui/src/components/editor/selection-menu.jsx +2 -0
  26. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +1 -2
  27. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +0 -1
  28. package/node_modules/@groove-dev/gui/src/stores/groove.js +3 -3
  29. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +2 -0
  30. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +3 -71
  31. package/node_modules/@groove-dev/gui/src/views/models.jsx +5 -6
  32. package/package.json +1 -1
  33. package/packages/cli/package.json +1 -1
  34. package/packages/daemon/package.json +1 -1
  35. package/packages/daemon/src/api.js +18 -7
  36. package/packages/daemon/src/introducer.js +1 -1
  37. package/packages/daemon/src/journalist.js +3 -2
  38. package/packages/daemon/src/keeper.js +2 -2
  39. package/packages/daemon/src/memory.js +8 -5
  40. package/packages/daemon/src/process.js +5 -16
  41. package/packages/daemon/src/rotator.js +25 -8
  42. package/packages/gui/dist/assets/{codemirror-BQqYnZfL.js → codemirror-BYKpdS2W.js} +10 -10
  43. package/packages/gui/dist/assets/index-CCVvAoQn.css +1 -0
  44. package/packages/gui/dist/assets/index-DGIv_TRm.js +984 -0
  45. package/packages/gui/dist/index.html +3 -3
  46. package/packages/gui/package.json +1 -1
  47. package/packages/gui/src/app.jsx +0 -2
  48. package/packages/gui/src/components/agents/agent-chat.jsx +73 -17
  49. package/packages/gui/src/components/agents/agent-feed.jsx +8 -2
  50. package/packages/gui/src/components/agents/agent-file-tree.jsx +5 -6
  51. package/packages/gui/src/components/agents/agent-panel.jsx +79 -5
  52. package/packages/gui/src/components/agents/workspace-mode.jsx +2 -53
  53. package/packages/gui/src/components/dashboard/context-gauges.jsx +111 -0
  54. package/packages/gui/src/components/dashboard/routing-chart.jsx +70 -33
  55. package/packages/gui/src/components/editor/code-editor.jsx +2 -68
  56. package/packages/gui/src/components/editor/selection-menu.jsx +2 -0
  57. package/packages/gui/src/components/layout/activity-bar.jsx +1 -2
  58. package/packages/gui/src/components/layout/terminal-panel.jsx +0 -1
  59. package/packages/gui/src/stores/groove.js +3 -3
  60. package/packages/gui/src/views/dashboard.jsx +2 -0
  61. package/packages/gui/src/views/marketplace.jsx +3 -71
  62. package/packages/gui/src/views/models.jsx +5 -6
  63. package/node_modules/@groove-dev/gui/dist/assets/index-A4e1gIDh.css +0 -1
  64. package/node_modules/@groove-dev/gui/dist/assets/index-P1hsM27-.js +0 -8696
  65. package/node_modules/@groove-dev/gui/src/components/toys/toy-card.jsx +0 -78
  66. package/node_modules/@groove-dev/gui/src/components/toys/toy-creator.jsx +0 -144
  67. package/node_modules/@groove-dev/gui/src/components/toys/toy-launcher.jsx +0 -187
  68. package/node_modules/@groove-dev/gui/src/views/toys.jsx +0 -162
  69. package/packages/gui/dist/assets/index-A4e1gIDh.css +0 -1
  70. package/packages/gui/dist/assets/index-P1hsM27-.js +0 -8696
  71. package/packages/gui/src/components/toys/toy-card.jsx +0 -78
  72. package/packages/gui/src/components/toys/toy-creator.jsx +0 -144
  73. package/packages/gui/src/components/toys/toy-launcher.jsx +0 -187
  74. package/packages/gui/src/views/toys.jsx +0 -162
@@ -6,6 +6,65 @@ import { fmtNum, fmtPct } from '../../lib/format';
6
6
  const TIER_COLORS = { heavy: HEX.danger, medium: HEX.warning, light: HEX.success };
7
7
  const TIER_LABELS = { heavy: 'Heavy', medium: 'Medium', light: 'Light' };
8
8
 
9
+ const DONUT_SIZE = 80;
10
+ const DONUT_STROKE = 6;
11
+ const DONUT_RADIUS = (DONUT_SIZE - DONUT_STROKE) / 2;
12
+ const DONUT_CIRCUMFERENCE = 2 * Math.PI * DONUT_RADIUS;
13
+
14
+ function TierDonut({ byTier, total, tiers }) {
15
+ let offset = 0;
16
+ const segments = [];
17
+ for (const tier of tiers) {
18
+ const count = byTier[tier] || 0;
19
+ if (count === 0) continue;
20
+ const pct = count / total;
21
+ const dashLen = pct * DONUT_CIRCUMFERENCE;
22
+ segments.push({ tier, dashLen, offset });
23
+ offset += dashLen;
24
+ }
25
+
26
+ const heavyPct = total > 0 ? Math.round(((byTier.heavy || 0) / total) * 100) : 0;
27
+
28
+ return (
29
+ <svg width={DONUT_SIZE} height={DONUT_SIZE} viewBox={`0 0 ${DONUT_SIZE} ${DONUT_SIZE}`} className="flex-shrink-0">
30
+ <circle
31
+ cx={DONUT_SIZE / 2} cy={DONUT_SIZE / 2} r={DONUT_RADIUS}
32
+ fill="none" strokeWidth={DONUT_STROKE}
33
+ className="stroke-surface-4"
34
+ />
35
+ {segments.map((seg) => (
36
+ <circle
37
+ key={seg.tier}
38
+ cx={DONUT_SIZE / 2} cy={DONUT_SIZE / 2} r={DONUT_RADIUS}
39
+ fill="none" strokeWidth={DONUT_STROKE}
40
+ strokeLinecap="butt"
41
+ style={{
42
+ stroke: TIER_COLORS[seg.tier],
43
+ strokeDasharray: `${seg.dashLen} ${DONUT_CIRCUMFERENCE - seg.dashLen}`,
44
+ strokeDashoffset: -seg.offset,
45
+ }}
46
+ transform={`rotate(-90 ${DONUT_SIZE / 2} ${DONUT_SIZE / 2})`}
47
+ />
48
+ ))}
49
+ <text
50
+ x={DONUT_SIZE / 2} y={DONUT_SIZE / 2 - 2}
51
+ textAnchor="middle" dominantBaseline="central"
52
+ className="fill-text-0 text-sm font-mono font-semibold"
53
+ >
54
+ {fmtNum(total)}
55
+ </text>
56
+ <text
57
+ x={DONUT_SIZE / 2} y={DONUT_SIZE / 2 + 11}
58
+ textAnchor="middle" dominantBaseline="central"
59
+ className="fill-text-3 font-mono"
60
+ style={{ fontSize: 7 }}
61
+ >
62
+ decisions
63
+ </text>
64
+ </svg>
65
+ );
66
+ }
67
+
9
68
  const RoutingChart = memo(function RoutingChart({ routing, agentBreakdown }) {
10
69
  if (!routing) return null;
11
70
 
@@ -13,7 +72,6 @@ const RoutingChart = memo(function RoutingChart({ routing, agentBreakdown }) {
13
72
  const tiers = ['heavy', 'medium', 'light'];
14
73
  const total = tiers.reduce((s, t) => s + (byTier[t] || 0), 0);
15
74
 
16
- // Build model usage from agent breakdown
17
75
  const modelUsage = {};
18
76
  for (const a of (agentBreakdown || [])) {
19
77
  const model = a.model || 'default';
@@ -26,43 +84,27 @@ const RoutingChart = memo(function RoutingChart({ routing, agentBreakdown }) {
26
84
 
27
85
  return (
28
86
  <div className="flex flex-col h-full px-3 py-3 overflow-y-auto">
29
- {/* Tier distribution bar */}
87
+ {/* Tier donut + legend */}
30
88
  {total > 0 && (
31
- <div className="space-y-1.5 mb-3">
32
- <div className="flex items-center gap-2">
33
- <span className="text-2xs font-mono text-text-3 uppercase tracking-wider">Tier Distribution</span>
34
- <span className="text-2xs font-mono text-text-4 ml-auto tabular-nums">{fmtNum(total)} decisions</span>
35
- </div>
36
- {/* Stacked horizontal bar */}
37
- <div className="h-0.5 bg-surface-2 rounded-sm overflow-hidden flex">
89
+ <div className="flex items-center gap-3 mb-3">
90
+ <TierDonut byTier={byTier} total={total} tiers={tiers} />
91
+ <div className="flex flex-col gap-1.5 flex-1 min-w-0">
38
92
  {tiers.map((tier) => {
39
93
  const count = byTier[tier] || 0;
40
94
  if (count === 0) return null;
41
95
  const pct = (count / total) * 100;
42
- return (
43
- <div
44
- key={tier}
45
- className="h-full transition-all duration-500"
46
- style={{ width: `${pct}%`, background: TIER_COLORS[tier] }}
47
- title={`${TIER_LABELS[tier]}: ${count} (${Math.round(pct)}%)`}
48
- />
49
- );
50
- })}
51
- </div>
52
- {/* Tier legend */}
53
- <div className="flex items-center gap-3">
54
- {tiers.map((tier) => {
55
- const count = byTier[tier] || 0;
56
- if (count === 0) return null;
57
- const pct = total > 0 ? (count / total) * 100 : 0;
58
96
  return (
59
97
  <div key={tier} className="flex items-center gap-1.5">
60
98
  <span className="w-1.5 h-1.5 rounded-full flex-shrink-0" style={{ background: TIER_COLORS[tier] }} />
61
- <span className="text-2xs font-mono text-text-2">{TIER_LABELS[tier]}</span>
62
- <span className="text-2xs font-mono text-text-4 tabular-nums">{fmtPct(pct)}</span>
99
+ <span className="text-2xs font-mono text-text-2 flex-1">{TIER_LABELS[tier]}</span>
100
+ <span className="text-2xs font-mono text-text-4 tabular-nums">{count}</span>
101
+ <span className="text-2xs font-mono text-text-4 tabular-nums w-8 text-right">{fmtPct(pct)}</span>
63
102
  </div>
64
103
  );
65
104
  })}
105
+ {autoRoutedCount > 0 && (
106
+ <div className="text-2xs font-mono text-text-4 mt-0.5">{autoRoutedCount} auto-routed</div>
107
+ )}
66
108
  </div>
67
109
  </div>
68
110
  )}
@@ -70,12 +112,7 @@ const RoutingChart = memo(function RoutingChart({ routing, agentBreakdown }) {
70
112
  {/* Model usage breakdown */}
71
113
  {modelEntries.length > 0 && (
72
114
  <div className="space-y-1.5 flex-1">
73
- <div className="flex items-center gap-2">
74
- <span className="text-2xs font-mono text-text-3 uppercase tracking-wider">Models in Use</span>
75
- {autoRoutedCount > 0 && (
76
- <span className="text-2xs font-mono text-text-4 ml-auto">{autoRoutedCount} auto</span>
77
- )}
78
- </div>
115
+ <span className="text-2xs font-mono text-text-3 uppercase tracking-wider">Models in Use</span>
79
116
  <div className="space-y-1.5">
80
117
  {modelEntries.map(([model, usage]) => {
81
118
  const barPct = maxModelTokens > 0 ? (usage.tokens / maxModelTokens) * 100 : 0;
@@ -1,7 +1,7 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  import { useRef, useEffect, useCallback } from 'react';
3
- import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter, Decoration, ViewPlugin, gutter, GutterMarker } from '@codemirror/view';
4
- import { EditorState, Compartment, StateField, StateEffect, RangeSet } from '@codemirror/state';
3
+ import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter } from '@codemirror/view';
4
+ import { EditorState, Compartment } from '@codemirror/state';
5
5
  import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
6
6
  import { bracketMatching } from '@codemirror/language';
7
7
  import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
@@ -26,7 +26,6 @@ import {
26
26
  vscodeLight, xcodeLight,
27
27
  } from '@uiw/codemirror-themes-all';
28
28
  import { useGrooveStore } from '../../stores/groove';
29
- import { api } from '../../lib/api';
30
29
 
31
30
  const LANGS = {
32
31
  javascript: () => javascript({ jsx: true, typescript: false }),
@@ -79,52 +78,6 @@ export const EDITOR_THEMES = {
79
78
  basicLight: { label: 'Basic Light', ext: basicLight },
80
79
  };
81
80
 
82
- // ── Git gutter decorations ───────────────────────────────────
83
- const setGitLines = StateEffect.define();
84
-
85
- const gitLinesField = StateField.define({
86
- create() { return { added: new Set(), modified: new Set(), deleted: new Set() }; },
87
- update(value, tr) {
88
- for (const e of tr.effects) {
89
- if (e.is(setGitLines)) return e.value;
90
- }
91
- return value;
92
- },
93
- });
94
-
95
- class GitGutterMarker extends GutterMarker {
96
- constructor(type) { super(); this.type = type; }
97
- toDOM() {
98
- const el = document.createElement('div');
99
- el.style.width = '3px';
100
- el.style.height = '100%';
101
- el.style.borderRadius = '1px';
102
- if (this.type === 'added') el.style.background = 'var(--color-success)';
103
- else if (this.type === 'modified') el.style.background = 'var(--color-warning)';
104
- else el.style.background = 'var(--color-danger)';
105
- return el;
106
- }
107
- }
108
-
109
- const addedMarker = new GitGutterMarker('added');
110
- const modifiedMarker = new GitGutterMarker('modified');
111
- const deletedMarker = new GitGutterMarker('deleted');
112
-
113
- const gitGutter = gutter({
114
- class: 'cm-git-gutter',
115
- markers(view) {
116
- const lines = view.state.field(gitLinesField);
117
- const markers = [];
118
- for (let i = 1; i <= view.state.doc.lines; i++) {
119
- const lineStart = view.state.doc.line(i).from;
120
- if (lines.added.has(i)) markers.push(addedMarker.range(lineStart));
121
- else if (lines.modified.has(i)) markers.push(modifiedMarker.range(lineStart));
122
- else if (lines.deleted.has(i)) markers.push(deletedMarker.range(lineStart));
123
- }
124
- return RangeSet.of(markers);
125
- },
126
- });
127
-
128
81
  const editorChrome = EditorView.theme({
129
82
  '&': { fontFamily: 'var(--font-mono)', fontSize: '12px', height: '100%', lineHeight: '1.6' },
130
83
  '.cm-scroller': { overflow: 'auto', padding: '4px 0' },
@@ -139,8 +92,6 @@ const editorChrome = EditorView.theme({
139
92
  '.cm-search .cm-button, .cm-button': { borderRadius: '4px', padding: '2px 8px', fontSize: '10px', fontFamily: 'var(--font-sans)', cursor: 'pointer', backgroundImage: 'none' },
140
93
  '.cm-search br': { display: 'none' },
141
94
  '.cm-panel.cm-search [name=close]': { cursor: 'pointer', padding: '0 4px' },
142
- '.cm-git-gutter': { width: '4px', marginRight: '2px' },
143
- '.cm-git-gutter .cm-gutterElement': { padding: '0', minWidth: '3px' },
144
95
  });
145
96
 
146
97
  function getThemeExt(key) {
@@ -186,8 +137,6 @@ export function CodeEditor({ content, language, onChange, onSave, onCursorChange
186
137
  const state = EditorState.create({
187
138
  doc: content || '',
188
139
  extensions: [
189
- gitLinesField,
190
- gitGutter,
191
140
  lineNumbers(),
192
141
  highlightActiveLine(),
193
142
  highlightActiveLineGutter(),
@@ -220,21 +169,6 @@ export function CodeEditor({ content, language, onChange, onSave, onCursorChange
220
169
  return () => { view.destroy(); viewRef.current = null; if (externalViewRef) externalViewRef.current = null; };
221
170
  }, []);
222
171
 
223
- // Fetch and apply git line status when filePath changes
224
- useEffect(() => {
225
- const view = viewRef.current;
226
- if (!view || !filePath) return;
227
- api.get(`/files/git-line-status?path=${encodeURIComponent(filePath)}`).then((data) => {
228
- if (!data?.lines) return;
229
- const lines = {
230
- added: new Set(data.lines.added || []),
231
- modified: new Set(data.lines.modified || []),
232
- deleted: new Set(data.lines.deleted || []),
233
- };
234
- view.dispatch({ effects: setGitLines.of(lines) });
235
- }).catch(() => {});
236
- }, [filePath, content]);
237
-
238
172
  useEffect(() => {
239
173
  const view = viewRef.current;
240
174
  if (!view) return;
@@ -18,6 +18,7 @@ export function SelectionMenu({ x, y, filePath, lineStart, lineEnd, selectedCode
18
18
  const sendCodeToAgent = useGrooveStore((s) => s.sendCodeToAgent);
19
19
  const toggleAiPanel = useGrooveStore((s) => s.toggleAiPanel);
20
20
  const aiPanelOpen = useGrooveStore((s) => s.editorAiPanelOpen);
21
+ const selectAgent = useGrooveStore((s) => s.selectAgent);
21
22
 
22
23
  useEffect(() => {
23
24
  function handleClick(e) {
@@ -51,6 +52,7 @@ export function SelectionMenu({ x, y, filePath, lineStart, lineEnd, selectedCode
51
52
  if (!agentId) return;
52
53
  sendCodeToAgent(agentId, action.instruction, filePath, lineStart, lineEnd, selectedCode);
53
54
  if (!aiPanelOpen) toggleAiPanel();
55
+ selectAgent(agentId);
54
56
  onClose();
55
57
  }
56
58
 
@@ -1,5 +1,5 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { Network, Code2, ChartSpline, Puzzle, Gamepad2, Users, Box, FlaskConical, Newspaper, Settings, Globe, MessageCircle, Eye, BookOpen } from 'lucide-react';
2
+ import { Network, Code2, ChartSpline, Puzzle, Users, Box, FlaskConical, Newspaper, Settings, Globe, MessageCircle, Eye, BookOpen } from 'lucide-react';
3
3
  import { cn } from '../../lib/cn';
4
4
  import { Tooltip } from '../ui/tooltip';
5
5
  import { useGrooveStore } from '../../stores/groove';
@@ -13,7 +13,6 @@ const BASE_NAV_ITEMS = [
13
13
  { id: 'memory', icon: BookOpen, label: 'Memory' },
14
14
  { id: 'teams', icon: Users, label: 'Teams' },
15
15
  { id: 'marketplace', icon: Puzzle, label: 'Marketplace' },
16
- { id: 'toys', icon: Gamepad2, label: 'Toys' },
17
16
  { id: 'models', icon: Box, label: 'Models' },
18
17
  { id: 'model-lab', icon: FlaskConical, label: 'Model Lab' },
19
18
  ];
@@ -228,7 +228,6 @@ export function TerminalPanel({
228
228
  <Tooltip content={activeAgent ? `Send to ${agent?.name || 'agent'}` : 'Send to agent'} side="top">
229
229
  <button
230
230
  onClick={handleSendClick}
231
- disabled={sending}
232
231
  className={cn(
233
232
  'flex items-center gap-1.5 px-2 py-1 rounded text-xs font-sans cursor-pointer transition-colors mr-1',
234
233
  'bg-accent/15 text-accent hover:bg-accent/25 disabled:opacity-50',
@@ -1322,7 +1322,7 @@ export const useGrooveStore = create((set, get) => ({
1322
1322
  },
1323
1323
 
1324
1324
  async _handleKeeperCommand(agentId, message, command) {
1325
- const rest = message.replace(/\[\w+[-\w]*\]|\b(?:save|append|update|delete|view|doc|link|read|instruct)\b/i, '').trim();
1325
+ const rest = message.replace(/\[\w+[-\w]*\]/i, '').trim();
1326
1326
  const tags = (rest.match(/#[\w/.-]+/g) || []).map(t => t.replace(/^#/, ''));
1327
1327
 
1328
1328
  const addSystemMsg = (text) => {
@@ -2684,9 +2684,9 @@ export const useGrooveStore = create((set, get) => ({
2684
2684
 
2685
2685
  async instructAgent(id, message) {
2686
2686
  // ── Keeper command interception ─────────────────────────
2687
- const keeperCmd = message.match(/\[(save|append|update|delete|view|doc|link|read|instruct)\]|\b(save|append|update|delete|view|doc|link|read)\b(?=\s+#[\w/.-])/i);
2687
+ const keeperCmd = message.match(/\[(save|append|update|delete|view|doc|link|read|instruct)\]/i);
2688
2688
  if (keeperCmd) {
2689
- const handled = await get()._handleKeeperCommand(id, message, (keeperCmd[1] || keeperCmd[2]).toLowerCase());
2689
+ const handled = await get()._handleKeeperCommand(id, message, keeperCmd[1].toLowerCase());
2690
2690
  if (handled === true) return { status: 'keeper_handled' };
2691
2691
  if (handled?.passthrough) {
2692
2692
  message = handled.passthrough;
@@ -4,6 +4,7 @@ import { useGrooveStore } from '../stores/groove';
4
4
  import { DashboardHeader } from '../components/dashboard/header-bar';
5
5
  import { KpiStrip } from '../components/dashboard/kpi-card';
6
6
  import { FleetPanel } from '../components/dashboard/fleet-panel';
7
+ import { ContextGauges } from '../components/dashboard/context-gauges';
7
8
  import { TokenChart } from '../components/dashboard/token-chart';
8
9
  import { CacheRing } from '../components/dashboard/cache-ring';
9
10
  import { RoutingChart } from '../components/dashboard/routing-chart';
@@ -142,6 +143,7 @@ export default function DashboardView() {
142
143
  <div className="px-3 pt-2.5 pb-1 flex-shrink-0">
143
144
  <span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Agent Fleet</span>
144
145
  </div>
146
+ <ContextGauges agentBreakdown={agentBreakdown} />
145
147
  <FleetPanel agentBreakdown={agentBreakdown} rotating={rotating} teams={teams} />
146
148
  </div>
147
149
 
@@ -4,15 +4,13 @@ import { ScrollArea } from '../components/ui/scroll-area';
4
4
  import { Badge } from '../components/ui/badge';
5
5
  import { Button } from '../components/ui/button';
6
6
  import { Skeleton } from '../components/ui/skeleton';
7
- import { SkillCard, SkillCardSkeleton } from '../components/marketplace/skill-card';
7
+ import { SkillCardSkeleton } from '../components/marketplace/skill-card';
8
8
  import { MarketplaceCard } from '../components/marketplace/marketplace-card';
9
9
  import { SearchBar } from '../components/marketplace/search-bar';
10
- import { CategoryBar } from '../components/marketplace/category-bar';
11
10
  import { MarketplaceBadge } from '../components/marketplace/marketplace-badge';
12
11
  import { StarRating } from '../components/marketplace/star-rating';
13
12
  import { PriceBadge } from '../components/marketplace/price-badge';
14
13
  import { VerifiedShield } from '../components/marketplace/verified-shield';
15
- import { markFavorites } from '../components/marketplace/favorites';
16
14
  import { api } from '../lib/api';
17
15
  import { useToast } from '../lib/hooks/use-toast';
18
16
  import { fmtNum, timeAgo } from '../lib/format';
@@ -22,7 +20,7 @@ import { RepoImport } from '../components/marketplace/repo-import';
22
20
  import { RepoCard } from '../components/marketplace/repo-card';
23
21
  import { RepoNukeDialog } from '../components/marketplace/repo-nuke-dialog';
24
22
  import {
25
- ChevronLeft, ChevronDown, Sparkles, Plug, LogIn,
23
+ ChevronLeft, Plug, LogIn,
26
24
  Upload, Package, Download, ShoppingBag, RefreshCw, Trash2,
27
25
  GitBranch,
28
26
  } from 'lucide-react';
@@ -214,70 +212,6 @@ function SkillDetail({ skill, onBack }) {
214
212
  }
215
213
 
216
214
  // ── Skills Browse ────────────────────────────────────────
217
- function SkillsBrowse() {
218
- const [skills, setSkills] = useState([]);
219
- const [loading, setLoading] = useState(true);
220
- const [search, setSearch] = useState('');
221
- const [category, setCategory] = useState('');
222
- const [sort, setSort] = useState('popular');
223
- const [selectedSkill, setSelectedSkill] = useState(null);
224
-
225
- useEffect(() => {
226
- setLoading(true);
227
- const params = new URLSearchParams();
228
- if (search) params.set('search', search);
229
- if (category) params.set('category', category);
230
- if (sort) params.set('sort', sort);
231
- api.get(`/skills/registry?${params}`)
232
- .then((d) => setSkills(markFavorites(d.skills || d.items || (Array.isArray(d) ? d : []))))
233
- .catch(() => setSkills([]))
234
- .finally(() => setLoading(false));
235
- }, [search, category, sort]);
236
-
237
- if (selectedSkill) {
238
- return <SkillDetail skill={selectedSkill} onBack={() => setSelectedSkill(null)} />;
239
- }
240
-
241
- return (
242
- <ScrollArea className="h-full">
243
- <div className="px-5 py-4">
244
- <div className="flex items-center gap-3">
245
- <div className="w-72">
246
- <SearchBar value={search} onChange={setSearch} />
247
- </div>
248
- <CategoryBar selected={category} onSelect={setCategory} />
249
- <div className="flex-1" />
250
- <div className="relative flex-shrink-0">
251
- <select
252
- value={sort}
253
- onChange={(e) => setSort(e.target.value)}
254
- className="appearance-none font-sans cursor-pointer pr-7 py-2 pl-3 text-xs bg-surface-0 border border-border-subtle rounded text-text-1 focus:outline-none"
255
- >
256
- <option value="popular">Popular</option>
257
- <option value="rating">Top Rated</option>
258
- <option value="newest">Newest</option>
259
- <option value="name">A-Z</option>
260
- </select>
261
- <ChevronDown size={12} className="absolute right-2 top-1/2 -translate-y-1/2 text-text-4 pointer-events-none" />
262
- </div>
263
- <span className="text-2xs text-text-4 font-mono flex-shrink-0">{skills.length}</span>
264
- </div>
265
-
266
- <div className="mt-4 grid gap-3" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))' }}>
267
- {loading
268
- ? Array.from({ length: 8 }).map((_, i) => <SkillCardSkeleton key={i} />)
269
- : skills.map((s) => <SkillCard key={s.id} skill={s} onClick={setSelectedSkill} />)
270
- }
271
- </div>
272
-
273
- {!loading && skills.length === 0 && (
274
- <div className="text-center py-16 text-text-4 font-sans text-sm">No skills found.</div>
275
- )}
276
- </div>
277
- </ScrollArea>
278
- );
279
- }
280
-
281
215
  // ── Integrations Browse ──────────────────────────────────
282
216
  const GOOGLE_IDS = new Set(['gmail', 'google-calendar', 'google-drive', 'google-docs', 'google-sheets', 'google-slides']);
283
217
 
@@ -666,10 +600,9 @@ function GitHubBrowse() {
666
600
 
667
601
  // ── Main ─────────────────────────────────────────────────
668
602
  export default function MarketplaceView() {
669
- const [tab, setTab] = useState('skills');
603
+ const [tab, setTab] = useState('integrations');
670
604
 
671
605
  const tabs = [
672
- { id: 'skills', label: 'Skills', icon: Sparkles },
673
606
  { id: 'integrations', label: 'Integrations', icon: Plug },
674
607
  { id: 'github', label: 'GitHub', icon: GitBranch },
675
608
  { id: 'library', label: 'My Library', icon: Package },
@@ -701,7 +634,6 @@ export default function MarketplaceView() {
701
634
 
702
635
  {/* Content */}
703
636
  <div className="flex-1 min-h-0">
704
- {tab === 'skills' && <SkillsBrowse />}
705
637
  {tab === 'integrations' && <IntegrationsBrowse />}
706
638
  {tab === 'github' && <GitHubBrowse />}
707
639
  {tab === 'library' && <MyLibrary />}
@@ -1,6 +1,5 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  import { useState, useEffect, useRef, useMemo } from 'react';
3
- import { ScrollArea } from '../components/ui/scroll-area';
4
3
  import { Badge } from '../components/ui/badge';
5
4
  import { Button } from '../components/ui/button';
6
5
  import { api } from '../lib/api';
@@ -603,12 +602,12 @@ export default function ModelsView() {
603
602
  if (filter === 'running') list = list.filter((m) => m.status === 'running');
604
603
  else if (filter === 'ready') list = list.filter((m) => m.status === 'ready');
605
604
  else if (filter === 'downloaded') list = list.filter((m) => m.status === 'downloaded' || m.status === 'downloading');
606
- if (searchQuery.trim()) {
605
+ if (searchQuery.trim() && discoveryTab !== 'search') {
607
606
  const q = searchQuery.toLowerCase();
608
607
  list = list.filter((m) => m.name.toLowerCase().includes(q));
609
608
  }
610
609
  return list;
611
- }, [unifiedModels, filter, searchQuery]);
610
+ }, [unifiedModels, filter, searchQuery, discoveryTab]);
612
611
 
613
612
  const filterCounts = useMemo(() => ({
614
613
  all: unifiedModels.length,
@@ -749,7 +748,7 @@ export default function ModelsView() {
749
748
  </div>
750
749
 
751
750
  {/* ════ ZONE 2 + 3: Scrollable Content ════ */}
752
- <ScrollArea className="flex-1">
751
+ <div className="flex-1 min-h-0 overflow-y-auto">
753
752
  <div className="p-5 space-y-6">
754
753
 
755
754
  {/* Empty State */}
@@ -794,7 +793,7 @@ export default function ModelsView() {
794
793
  </div>
795
794
  ) : (
796
795
  /* ── Card Grid ── */
797
- <div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-3">
796
+ <div className="grid gap-3" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))' }}>
798
797
  {filteredModels.map((model) => (
799
798
  <UnifiedModelCard
800
799
  key={model.id}
@@ -965,7 +964,7 @@ export default function ModelsView() {
965
964
  )}
966
965
  </div>
967
966
  </div>
968
- </ScrollArea>
967
+ </div>
969
968
  </div>
970
969
  );
971
970
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.141",
3
+ "version": "0.27.143",
4
4
  "description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.141",
3
+ "version": "0.27.143",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.141",
3
+ "version": "0.27.143",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -460,13 +460,14 @@ export function createApi(app, daemon) {
460
460
  // Discoveries (error → fix pairs)
461
461
  app.get('/api/memory/discoveries', (req, res) => {
462
462
  const role = req.query.role;
463
+ const teamId = req.query.teamId;
463
464
  const limit = Math.min(parseInt(req.query.limit) || 100, 500);
464
- res.json({ discoveries: daemon.memory.listDiscoveries({ role, limit }) });
465
+ res.json({ discoveries: daemon.memory.listDiscoveries({ role, teamId, limit }) });
465
466
  });
466
467
 
467
468
  app.post('/api/memory/discoveries', (req, res) => {
468
- const { agentId, role, trigger, fix, outcome } = req.body || {};
469
- const result = daemon.memory.addDiscovery({ agentId, role, trigger, fix, outcome });
469
+ const { agentId, role, trigger, fix, outcome, teamId } = req.body || {};
470
+ const result = daemon.memory.addDiscovery({ agentId, role, trigger, fix, outcome, teamId });
470
471
  if (!result.added && result.error) {
471
472
  return res.status(400).json(result);
472
473
  }
@@ -3171,11 +3172,21 @@ Keep responses concise. Help them think, don't lecture them about the system the
3171
3172
 
3172
3173
  function validateFilePath(relPath, projectDir) {
3173
3174
  if (!relPath || typeof relPath !== 'string') return { error: 'path is required' };
3174
- if (relPath.startsWith('/') || relPath.includes('..') || relPath.includes('\0')) {
3175
- return { error: 'Invalid path' };
3175
+ if (relPath.includes('\0')) return { error: 'Invalid path' };
3176
+
3177
+ let fullPath;
3178
+ if (relPath.startsWith('/')) {
3179
+ if (relPath.includes('..')) return { error: 'Invalid path' };
3180
+ if (!relPath.startsWith(projectDir + '/') && relPath !== projectDir) {
3181
+ return { error: 'Path outside project' };
3182
+ }
3183
+ fullPath = relPath;
3184
+ } else {
3185
+ if (relPath.includes('..')) return { error: 'Invalid path' };
3186
+ fullPath = resolve(projectDir, relPath);
3187
+ if (!fullPath.startsWith(projectDir)) return { error: 'Path outside project' };
3176
3188
  }
3177
- const fullPath = resolve(projectDir, relPath);
3178
- if (!fullPath.startsWith(projectDir)) return { error: 'Path outside project' };
3189
+
3179
3190
  // Symlink resolution — ensure real path is also within project
3180
3191
  try {
3181
3192
  const realPath = realpathSync(fullPath);
@@ -437,7 +437,7 @@ export class Introducer {
437
437
  }
438
438
 
439
439
  if (!isLightPlanner && (hasTask || isRotation)) {
440
- const discoveries = this.daemon.memory.getDiscoveriesMarkdown(newAgent.role, 8, 600, newAgent.scope);
440
+ const discoveries = this.daemon.memory.getDiscoveriesMarkdown(newAgent.role, 8, 600, newAgent.scope, newAgent.teamId);
441
441
  if (discoveries) {
442
442
  parts.push(`### Known Fixes for ${newAgent.role} Role\n${discoveries}`);
443
443
  }
@@ -877,7 +877,7 @@ export class Journalist {
877
877
  const entries = agentLog?.entries || [];
878
878
 
879
879
  // Layer 7 memory: discoveries (inline, not pointer — agents lose context with pointers), constraints, specializations
880
- const discoveries = this.daemon.memory?.getDiscoveriesMarkdown(agent.role, 10, 1500) || '';
880
+ const discoveries = this.daemon.memory?.getDiscoveriesMarkdown(agent.role, 10, 1500, agent.scope, agent.teamId) || '';
881
881
  const constraints = this.daemon.memory?.getConstraintsMarkdown(2000) || '';
882
882
  const specialization = this.daemon.memory?.getSpecialization(agent.id);
883
883
  const specLine = specialization?.avgQualityScore != null
@@ -1106,7 +1106,7 @@ export class Journalist {
1106
1106
  if (!thread) return null;
1107
1107
 
1108
1108
  const constraints = this.daemon.memory?.getConstraintsMarkdown(2000) || '';
1109
- const discoveries = this.daemon.memory?.getDiscoveriesMarkdown(agent.role, 5, 1000) || '';
1109
+ const discoveries = this.daemon.memory?.getDiscoveriesMarkdown(agent.role, 5, 1000, agent.scope, agent.teamId) || '';
1110
1110
 
1111
1111
  let prompt = [
1112
1112
  `# Session Context Resume`,
@@ -1352,6 +1352,7 @@ export class Journalist {
1352
1352
  this.daemon.memory.addDiscovery({
1353
1353
  agentId: agent.id,
1354
1354
  role: agent.role,
1355
+ teamId: agent.teamId || null,
1355
1356
  trigger: trigger.slice(0, 300),
1356
1357
  fix: fix.slice(0, 500),
1357
1358
  outcome: 'success',
@@ -254,9 +254,9 @@ export class Keeper {
254
254
  // ── Command parser ────────────────────────────────────────
255
255
 
256
256
  static parseCommand(text) {
257
- const cmdMatch = text.match(/\[(save|append|update|delete|view|doc|link|read|instruct)\]|\b(save|append|update|delete|view|doc|link|read)\b(?=\s+#[\w/.-])/i);
257
+ const cmdMatch = text.match(/\[(save|append|update|delete|view|doc|link|read|instruct)\]/i);
258
258
  if (!cmdMatch) return null;
259
- const command = (cmdMatch[1] || cmdMatch[2]).toLowerCase();
259
+ const command = cmdMatch[1].toLowerCase();
260
260
  const rest = text.slice(cmdMatch.index + cmdMatch[0].length).trim();
261
261
 
262
262
  if (command === 'instruct') {