groove-dev 0.8.0

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 (84) hide show
  1. package/CLAUDE.md +197 -0
  2. package/LICENSE +40 -0
  3. package/README.md +115 -0
  4. package/docs/GUI_DESIGN_SPEC.md +402 -0
  5. package/favicon.png +0 -0
  6. package/groove-logo-short.png +0 -0
  7. package/groove-logo.png +0 -0
  8. package/package.json +70 -0
  9. package/packages/cli/bin/groove.js +98 -0
  10. package/packages/cli/package.json +15 -0
  11. package/packages/cli/src/client.js +25 -0
  12. package/packages/cli/src/commands/agents.js +38 -0
  13. package/packages/cli/src/commands/approve.js +50 -0
  14. package/packages/cli/src/commands/config.js +35 -0
  15. package/packages/cli/src/commands/kill.js +15 -0
  16. package/packages/cli/src/commands/nuke.js +19 -0
  17. package/packages/cli/src/commands/providers.js +40 -0
  18. package/packages/cli/src/commands/rotate.js +16 -0
  19. package/packages/cli/src/commands/spawn.js +91 -0
  20. package/packages/cli/src/commands/start.js +31 -0
  21. package/packages/cli/src/commands/status.js +38 -0
  22. package/packages/cli/src/commands/stop.js +15 -0
  23. package/packages/cli/src/commands/team.js +77 -0
  24. package/packages/daemon/package.json +18 -0
  25. package/packages/daemon/src/adaptive.js +237 -0
  26. package/packages/daemon/src/api.js +533 -0
  27. package/packages/daemon/src/classifier.js +126 -0
  28. package/packages/daemon/src/credentials.js +121 -0
  29. package/packages/daemon/src/firstrun.js +93 -0
  30. package/packages/daemon/src/index.js +208 -0
  31. package/packages/daemon/src/introducer.js +238 -0
  32. package/packages/daemon/src/journalist.js +600 -0
  33. package/packages/daemon/src/lockmanager.js +58 -0
  34. package/packages/daemon/src/pm.js +108 -0
  35. package/packages/daemon/src/process.js +361 -0
  36. package/packages/daemon/src/providers/aider.js +72 -0
  37. package/packages/daemon/src/providers/base.js +38 -0
  38. package/packages/daemon/src/providers/claude-code.js +167 -0
  39. package/packages/daemon/src/providers/codex.js +68 -0
  40. package/packages/daemon/src/providers/gemini.js +62 -0
  41. package/packages/daemon/src/providers/index.js +38 -0
  42. package/packages/daemon/src/providers/ollama.js +94 -0
  43. package/packages/daemon/src/registry.js +89 -0
  44. package/packages/daemon/src/rotator.js +185 -0
  45. package/packages/daemon/src/router.js +132 -0
  46. package/packages/daemon/src/state.js +34 -0
  47. package/packages/daemon/src/supervisor.js +178 -0
  48. package/packages/daemon/src/teams.js +203 -0
  49. package/packages/daemon/src/terminal/base.js +27 -0
  50. package/packages/daemon/src/terminal/generic.js +27 -0
  51. package/packages/daemon/src/terminal/tmux.js +64 -0
  52. package/packages/daemon/src/tokentracker.js +124 -0
  53. package/packages/daemon/src/validate.js +122 -0
  54. package/packages/daemon/templates/api-builder.json +18 -0
  55. package/packages/daemon/templates/fullstack.json +18 -0
  56. package/packages/daemon/templates/monorepo.json +24 -0
  57. package/packages/gui/dist/assets/index-BO95Rm1F.js +73 -0
  58. package/packages/gui/dist/assets/index-CPzm9ZE9.css +1 -0
  59. package/packages/gui/dist/favicon.png +0 -0
  60. package/packages/gui/dist/groove-logo-short.png +0 -0
  61. package/packages/gui/dist/groove-logo.png +0 -0
  62. package/packages/gui/dist/index.html +13 -0
  63. package/packages/gui/index.html +12 -0
  64. package/packages/gui/package.json +22 -0
  65. package/packages/gui/public/favicon.png +0 -0
  66. package/packages/gui/public/groove-logo-short.png +0 -0
  67. package/packages/gui/public/groove-logo.png +0 -0
  68. package/packages/gui/src/App.jsx +215 -0
  69. package/packages/gui/src/components/AgentActions.jsx +347 -0
  70. package/packages/gui/src/components/AgentChat.jsx +479 -0
  71. package/packages/gui/src/components/AgentNode.jsx +117 -0
  72. package/packages/gui/src/components/AgentPanel.jsx +115 -0
  73. package/packages/gui/src/components/AgentStats.jsx +333 -0
  74. package/packages/gui/src/components/ApprovalQueue.jsx +156 -0
  75. package/packages/gui/src/components/EmptyState.jsx +100 -0
  76. package/packages/gui/src/components/SpawnPanel.jsx +515 -0
  77. package/packages/gui/src/components/TeamSelector.jsx +162 -0
  78. package/packages/gui/src/main.jsx +9 -0
  79. package/packages/gui/src/stores/groove.js +247 -0
  80. package/packages/gui/src/theme.css +67 -0
  81. package/packages/gui/src/views/AgentTree.jsx +148 -0
  82. package/packages/gui/src/views/CommandCenter.jsx +620 -0
  83. package/packages/gui/src/views/JournalistFeed.jsx +149 -0
  84. package/packages/gui/vite.config.js +19 -0
@@ -0,0 +1 @@
1
+ @import"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700;800&display=swap";:root{--bg-base: #24282f;--bg-chrome: #282c34;--bg-surface: #2c313a;--bg-hover: #333842;--bg-active: #3a3f4b;--text-primary: #abb2bf;--text-bright: #e6e6e6;--text-dim: #5c6370;--text-muted: #3e4451;--border: #4b5263;--accent: #33afbc;--green: #4ae168;--amber: #e5c07b;--red: #e06c75;--purple: #c678dd;--blue: #61afef;--font: "JetBrains Mono", "SF Mono", "Fira Code", "Cascadia Code", Consolas, monospace}*{margin:0;padding:0;box-sizing:border-box}body{font-family:var(--font);background:var(--bg-base);color:var(--text-primary);font-size:12px;-webkit-font-smoothing:antialiased}#root{width:100vw;height:100vh}::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--bg-hover);border-radius:2px}::selection{background:#33afbc40}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}@keyframes neuralFlow{0%{transform:translate(-50%)}to{transform:translate(0)}}.react-flow__edge-path{stroke-width:1!important}.react-flow__edge.animated path{stroke-dasharray:6 4!important;stroke-width:1!important}.react-flow{direction:ltr;--xy-edge-stroke-default: #b1b1b7;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #555;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(255, 255, 255, .5);--xy-minimap-background-color-default: #fff;--xy-minimap-mask-background-color-default: rgba(240, 240, 240, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #e2e2e2;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: transparent;--xy-background-pattern-dots-color-default: #91919a;--xy-background-pattern-lines-color-default: #eee;--xy-background-pattern-cross-color-default: #e2e2e2;background-color:var(--xy-background-color, var(--xy-background-color-default));--xy-node-color-default: inherit;--xy-node-border-default: 1px solid #1a192b;--xy-node-background-color-default: #fff;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #1a192b;--xy-node-border-radius-default: 3px;--xy-handle-background-color-default: #1a192b;--xy-handle-border-color-default: #fff;--xy-selection-background-color-default: rgba(0, 89, 220, .08);--xy-selection-border-default: 1px dotted rgba(0, 89, 220, .8);--xy-controls-button-background-color-default: #fefefe;--xy-controls-button-background-color-hover-default: #f4f4f4;--xy-controls-button-color-default: inherit;--xy-controls-button-color-hover-default: inherit;--xy-controls-button-border-color-default: #eee;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #ffffff;--xy-edge-label-color-default: inherit;--xy-resize-background-color-default: #3367d9}.react-flow.dark{--xy-edge-stroke-default: #3e3e3e;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #727272;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(150, 150, 150, .25);--xy-minimap-background-color-default: #141414;--xy-minimap-mask-background-color-default: rgba(60, 60, 60, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #2b2b2b;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: #141414;--xy-background-pattern-dots-color-default: #777;--xy-background-pattern-lines-color-default: #777;--xy-background-pattern-cross-color-default: #777;--xy-node-color-default: #f8f8f8;--xy-node-border-default: 1px solid #3c3c3c;--xy-node-background-color-default: #1e1e1e;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #999;--xy-handle-background-color-default: #bebebe;--xy-handle-border-color-default: #1e1e1e;--xy-selection-background-color-default: rgba(200, 200, 220, .08);--xy-selection-border-default: 1px dotted rgba(200, 200, 220, .8);--xy-controls-button-background-color-default: #2b2b2b;--xy-controls-button-background-color-hover-default: #3e3e3e;--xy-controls-button-color-default: #f8f8f8;--xy-controls-button-color-hover-default: #fff;--xy-controls-button-border-color-default: #5b5b5b;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #141414;--xy-edge-label-color-default: #f8f8f8}.react-flow__background{background-color:var(--xy-background-color-props, var(--xy-background-color, var(--xy-background-color-default)));pointer-events:none;z-index:-1}.react-flow__container{position:absolute;width:100%;height:100%;top:0;left:0}.react-flow__pane{z-index:1}.react-flow__pane.draggable{cursor:grab}.react-flow__pane.dragging{cursor:grabbing}.react-flow__pane.selection{cursor:pointer}.react-flow__viewport{transform-origin:0 0;z-index:2;pointer-events:none}.react-flow__renderer{z-index:4}.react-flow__selection{z-index:6}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible{outline:none}.react-flow__edge-path{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default));stroke-width:var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));fill:none}.react-flow__connection-path{stroke:var(--xy-connectionline-stroke, var(--xy-connectionline-stroke-default));stroke-width:var(--xy-connectionline-stroke-width, var(--xy-connectionline-stroke-width-default));fill:none}.react-flow .react-flow__edges{position:absolute}.react-flow .react-flow__edges svg{overflow:visible;position:absolute;pointer-events:none}.react-flow__edge{pointer-events:visibleStroke}.react-flow__edge.selectable{cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;animation:dashdraw .5s linear infinite}.react-flow__edge.animated path.react-flow__edge-interaction{stroke-dasharray:none;animation:none}.react-flow__edge.inactive{pointer-events:none}.react-flow__edge.selected,.react-flow__edge:focus,.react-flow__edge:focus-visible{outline:none}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge.selectable:focus .react-flow__edge-path,.react-flow__edge.selectable:focus-visible .react-flow__edge-path{stroke:var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default))}.react-flow__edge-textwrapper{pointer-events:all}.react-flow__edge .react-flow__edge-text{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__arrowhead polyline{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__arrowhead polyline.arrowclosed{fill:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__connection{pointer-events:none}.react-flow__connection .animated{stroke-dasharray:5;animation:dashdraw .5s linear infinite}svg.react-flow__connectionline{z-index:1001;overflow:visible;position:absolute}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default}.react-flow__node.selectable{cursor:pointer}.react-flow__node.draggable{cursor:grab;pointer-events:all}.react-flow__node.draggable.dragging{cursor:grabbing}.react-flow__nodesselection{z-index:3;transform-origin:left top;pointer-events:none}.react-flow__nodesselection-rect{position:absolute;pointer-events:all;cursor:grab}.react-flow__handle{position:absolute;pointer-events:none;min-width:5px;min-height:5px;width:6px;height:6px;background-color:var(--xy-handle-background-color, var(--xy-handle-background-color-default));border:1px solid var(--xy-handle-border-color, var(--xy-handle-border-color-default));border-radius:100%}.react-flow__handle.connectingfrom{pointer-events:all}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;left:50%;bottom:0;transform:translate(-50%,50%)}.react-flow__handle-top{top:0;left:50%;transform:translate(-50%,-50%)}.react-flow__handle-left{top:50%;left:0;transform:translate(-50%,-50%)}.react-flow__handle-right{top:50%;right:0;transform:translate(50%,-50%)}.react-flow__edgeupdater{cursor:move;pointer-events:all}.react-flow__pane.selection .react-flow__panel{pointer-events:none}.react-flow__panel{position:absolute;z-index:5;margin:15px}.react-flow__panel.top{top:0}.react-flow__panel.bottom{bottom:0}.react-flow__panel.top.center,.react-flow__panel.bottom.center{left:50%;transform:translate(-15px) translate(-50%)}.react-flow__panel.left{left:0}.react-flow__panel.right{right:0}.react-flow__panel.left.center,.react-flow__panel.right.center{top:50%;transform:translateY(-15px) translateY(-50%)}.react-flow__attribution{font-size:10px;background:var(--xy-attribution-background-color, var(--xy-attribution-background-color-default));padding:2px 3px;margin:0}.react-flow__attribution a{text-decoration:none;color:#999}@keyframes dashdraw{0%{stroke-dashoffset:10}}.react-flow__edgelabel-renderer{position:absolute;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;left:0;top:0}.react-flow__viewport-portal{position:absolute;width:100%;height:100%;left:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__minimap{background:var( --xy-minimap-background-color-props, var(--xy-minimap-background-color, var(--xy-minimap-background-color-default)) )}.react-flow__minimap-svg{display:block}.react-flow__minimap-mask{fill:var( --xy-minimap-mask-background-color-props, var(--xy-minimap-mask-background-color, var(--xy-minimap-mask-background-color-default)) );stroke:var( --xy-minimap-mask-stroke-color-props, var(--xy-minimap-mask-stroke-color, var(--xy-minimap-mask-stroke-color-default)) );stroke-width:var( --xy-minimap-mask-stroke-width-props, var(--xy-minimap-mask-stroke-width, var(--xy-minimap-mask-stroke-width-default)) )}.react-flow__minimap-node{fill:var( --xy-minimap-node-background-color-props, var(--xy-minimap-node-background-color, var(--xy-minimap-node-background-color-default)) );stroke:var( --xy-minimap-node-stroke-color-props, var(--xy-minimap-node-stroke-color, var(--xy-minimap-node-stroke-color-default)) );stroke-width:var( --xy-minimap-node-stroke-width-props, var(--xy-minimap-node-stroke-width, var(--xy-minimap-node-stroke-width-default)) )}.react-flow__background-pattern.dots{fill:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-dots-color-default)) )}.react-flow__background-pattern.lines{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-lines-color-default)) )}.react-flow__background-pattern.cross{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-cross-color-default)) )}.react-flow__controls{display:flex;flex-direction:column;box-shadow:var(--xy-controls-box-shadow, var(--xy-controls-box-shadow-default))}.react-flow__controls.horizontal{flex-direction:row}.react-flow__controls-button{display:flex;justify-content:center;align-items:center;height:26px;width:26px;padding:4px;border:none;background:var(--xy-controls-button-background-color, var(--xy-controls-button-background-color-default));border-bottom:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) );color:var( --xy-controls-button-color-props, var(--xy-controls-button-color, var(--xy-controls-button-color-default)) );cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__controls-button svg{width:100%;max-width:12px;max-height:12px;fill:currentColor}.react-flow__edge.updating .react-flow__edge-path{stroke:#777}.react-flow__edge-text{font-size:10px}.react-flow__node.selectable:focus,.react-flow__node.selectable:focus-visible{outline:none}.react-flow__node-input,.react-flow__node-default,.react-flow__node-output,.react-flow__node-group{padding:10px;border-radius:var(--xy-node-border-radius, var(--xy-node-border-radius-default));width:150px;font-size:12px;color:var(--xy-node-color, var(--xy-node-color-default));text-align:center;border:var(--xy-node-border, var(--xy-node-border-default));background-color:var(--xy-node-background-color, var(--xy-node-background-color-default))}.react-flow__node-input.selectable:hover,.react-flow__node-default.selectable:hover,.react-flow__node-output.selectable:hover,.react-flow__node-group.selectable:hover{box-shadow:var(--xy-node-boxshadow-hover, var(--xy-node-boxshadow-hover-default))}.react-flow__node-input.selectable.selected,.react-flow__node-input.selectable:focus,.react-flow__node-input.selectable:focus-visible,.react-flow__node-default.selectable.selected,.react-flow__node-default.selectable:focus,.react-flow__node-default.selectable:focus-visible,.react-flow__node-output.selectable.selected,.react-flow__node-output.selectable:focus,.react-flow__node-output.selectable:focus-visible,.react-flow__node-group.selectable.selected,.react-flow__node-group.selectable:focus,.react-flow__node-group.selectable:focus-visible{box-shadow:var(--xy-node-boxshadow-selected, var(--xy-node-boxshadow-selected-default))}.react-flow__node-group{background-color:var(--xy-node-group-background-color, var(--xy-node-group-background-color-default))}.react-flow__nodesselection-rect,.react-flow__selection{background:var(--xy-selection-background-color, var(--xy-selection-background-color-default));border:var(--xy-selection-border, var(--xy-selection-border-default))}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible,.react-flow__selection:focus,.react-flow__selection:focus-visible{outline:none}.react-flow__controls-button:hover{background:var( --xy-controls-button-background-color-hover-props, var(--xy-controls-button-background-color-hover, var(--xy-controls-button-background-color-hover-default)) );color:var( --xy-controls-button-color-hover-props, var(--xy-controls-button-color-hover, var(--xy-controls-button-color-hover-default)) )}.react-flow__controls-button:disabled{pointer-events:none}.react-flow__controls-button:disabled svg{fill-opacity:.4}.react-flow__controls-button:last-child{border-bottom:none}.react-flow__controls.horizontal .react-flow__controls-button{border-bottom:none;border-right:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) )}.react-flow__controls.horizontal .react-flow__controls-button:last-child{border-right:none}.react-flow__resize-control{position:absolute}.react-flow__resize-control.left,.react-flow__resize-control.right{cursor:ew-resize}.react-flow__resize-control.top,.react-flow__resize-control.bottom{cursor:ns-resize}.react-flow__resize-control.top.left,.react-flow__resize-control.bottom.right{cursor:nwse-resize}.react-flow__resize-control.bottom.left,.react-flow__resize-control.top.right{cursor:nesw-resize}.react-flow__resize-control.handle{width:5px;height:5px;border:1px solid #fff;border-radius:1px;background-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));translate:-50% -50%}.react-flow__resize-control.handle.left{left:0;top:50%}.react-flow__resize-control.handle.right{left:100%;top:50%}.react-flow__resize-control.handle.top{left:50%;top:0}.react-flow__resize-control.handle.bottom{left:50%;top:100%}.react-flow__resize-control.handle.top.left,.react-flow__resize-control.handle.bottom.left{left:0}.react-flow__resize-control.handle.top.right,.react-flow__resize-control.handle.bottom.right{left:100%}.react-flow__resize-control.line{border-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));border-width:0;border-style:solid}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;transform:translate(-50%);top:0;height:100%}.react-flow__resize-control.line.left{left:0;border-left-width:1px}.react-flow__resize-control.line.right{left:100%;border-right-width:1px}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{height:1px;transform:translateY(-50%);left:0;width:100%}.react-flow__resize-control.line.top{top:0;border-top-width:1px}.react-flow__resize-control.line.bottom{border-bottom-width:1px;top:100%}.react-flow__edge-textbg{fill:var(--xy-edge-label-background-color, var(--xy-edge-label-background-color-default))}.react-flow__edge-text{fill:var(--xy-edge-label-color, var(--xy-edge-label-color-default))}
Binary file
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>GROOVE</title>
7
+ <script type="module" crossorigin src="/assets/index-BO95Rm1F.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-CPzm9ZE9.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>GROOVE</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.jsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@groove-dev/gui",
3
+ "version": "0.8.0",
4
+ "description": "GROOVE GUI — visual agent control plane",
5
+ "license": "FSL-1.1-Apache-2.0",
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "vite",
9
+ "build": "vite build",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "react": "^19.0.0",
14
+ "react-dom": "^19.0.0",
15
+ "zustand": "^5.0.0",
16
+ "@xyflow/react": "^12.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "@vitejs/plugin-react": "^4.3.0",
20
+ "vite": "^6.0.0"
21
+ }
22
+ }
Binary file
@@ -0,0 +1,215 @@
1
+ // GROOVE GUI — App Root
2
+ // FSL-1.1-Apache-2.0 — see LICENSE
3
+
4
+ import React, { useEffect } from 'react';
5
+ import { useGrooveStore } from './stores/groove';
6
+ import AgentTree from './views/AgentTree';
7
+ import AgentPanel from './components/AgentPanel';
8
+ import EmptyState from './components/EmptyState';
9
+ import SpawnPanel from './components/SpawnPanel';
10
+ import JournalistFeed from './views/JournalistFeed';
11
+ import TeamSelector from './components/TeamSelector';
12
+ import CommandCenter from './views/CommandCenter';
13
+ import ApprovalQueue from './components/ApprovalQueue';
14
+
15
+ const TABS = [
16
+ { id: 'agents', label: 'Agents' },
17
+ { id: 'stats', label: 'Stats' },
18
+ { id: 'teams', label: 'Teams' },
19
+ { id: 'approvals', label: 'Approvals' },
20
+ ];
21
+
22
+ export default function App() {
23
+ const agents = useGrooveStore((s) => s.agents);
24
+ const connected = useGrooveStore((s) => s.connected);
25
+ const activeTab = useGrooveStore((s) => s.activeTab);
26
+ const detailPanel = useGrooveStore((s) => s.detailPanel);
27
+ const statusMessage = useGrooveStore((s) => s.statusMessage);
28
+ const connect = useGrooveStore((s) => s.connect);
29
+ const setActiveTab = useGrooveStore((s) => s.setActiveTab);
30
+ const openDetail = useGrooveStore((s) => s.openDetail);
31
+ const closeDetail = useGrooveStore((s) => s.closeDetail);
32
+
33
+ useEffect(() => { connect(); }, [connect]);
34
+
35
+ const runningCount = agents.filter((a) => a.status === 'running').length;
36
+ const hasAgents = agents.length > 0;
37
+
38
+ return (
39
+ <div style={styles.root}>
40
+ {/* Header */}
41
+ <header style={styles.header}>
42
+ <div style={styles.headerLeft}>
43
+ <img src="/groove-logo-short.png" alt="GROOVE" style={{ height: 18, marginTop: 3, opacity: 0.85 }} />
44
+ </div>
45
+
46
+
47
+ <div style={styles.headerCenter}>
48
+ {connected && TABS.map((tab) => (
49
+ <button
50
+ key={tab.id}
51
+ onClick={() => setActiveTab(tab.id)}
52
+ style={{
53
+ ...styles.tabBtn,
54
+ color: activeTab === tab.id ? 'var(--text-bright)' : 'var(--text-primary)',
55
+ borderBottom: activeTab === tab.id ? '2px solid var(--accent)' : '2px solid transparent',
56
+ background: activeTab === tab.id ? 'var(--bg-active)' : 'transparent',
57
+ }}
58
+ >
59
+ {tab.label}
60
+ </button>
61
+ ))}
62
+ </div>
63
+
64
+ <div style={styles.headerRight}>
65
+ {statusMessage && (
66
+ <span style={styles.statusText}>{statusMessage}</span>
67
+ )}
68
+ <span style={styles.agentCount}>
69
+ {runningCount > 0
70
+ ? `${runningCount} running`
71
+ : agents.length > 0
72
+ ? `${agents.length} agent${agents.length !== 1 ? 's' : ''}`
73
+ : ''}
74
+ </span>
75
+ {connected && (
76
+ <>
77
+ <button
78
+ onClick={() => detailPanel?.type === 'journalist' ? closeDetail() : openDetail({ type: 'journalist' })}
79
+ style={{
80
+ ...styles.tabBtn,
81
+ color: detailPanel?.type === 'journalist' ? 'var(--text-bright)' : 'var(--text-primary)',
82
+ borderBottom: detailPanel?.type === 'journalist' ? '2px solid var(--purple)' : '2px solid transparent',
83
+ }}
84
+ >
85
+ Journalist
86
+ </button>
87
+ <button
88
+ onClick={() => openDetail({ type: 'spawn' })}
89
+ style={styles.spawnBtn}
90
+ >
91
+ + Spawn
92
+ </button>
93
+ </>
94
+ )}
95
+ </div>
96
+ </header>
97
+
98
+ {/* Status pill — bottom left */}
99
+ <div style={{
100
+ position: 'fixed', bottom: 10, left: 12, zIndex: 50,
101
+ display: 'flex', alignItems: 'center', gap: 5,
102
+ }}>
103
+ <div style={{
104
+ width: 5, height: 5, borderRadius: '50%',
105
+ background: connected ? 'var(--green)' : 'var(--red)',
106
+ animation: 'pulse 2s infinite',
107
+ }} />
108
+ <span style={{
109
+ fontSize: 9, fontWeight: 600, letterSpacing: 0.8,
110
+ color: connected ? 'var(--green)' : 'var(--red)',
111
+ textTransform: 'uppercase',
112
+ fontFamily: 'var(--font)',
113
+ animation: 'pulse 3s infinite',
114
+ }}>
115
+ {connected ? 'connected' : 'offline'}
116
+ </span>
117
+ </div>
118
+
119
+ {/* Main row */}
120
+ <div style={styles.mainRow}>
121
+ <main style={styles.content}>
122
+ {activeTab === 'agents' && (
123
+ !hasAgents ? <EmptyState /> : <AgentTree />
124
+ )}
125
+ {activeTab === 'stats' && <CommandCenter />}
126
+ {activeTab === 'teams' && <TeamSelector />}
127
+ {activeTab === 'approvals' && <ApprovalQueue />}
128
+ </main>
129
+
130
+ {/* Detail panel — in document flow */}
131
+ {detailPanel && (
132
+ <aside style={{
133
+ ...styles.detailPanel,
134
+ width: detailPanel.type === 'agent' ? '45%' : 320,
135
+ }}>
136
+ <button onClick={closeDetail} style={styles.closeBtn}>x</button>
137
+ {detailPanel.type === 'agent' && <AgentPanel />}
138
+ {detailPanel.type === 'spawn' && <SpawnPanel />}
139
+ {detailPanel.type === 'journalist' && <JournalistFeed />}
140
+ </aside>
141
+ )}
142
+ </div>
143
+ </div>
144
+ );
145
+ }
146
+
147
+ const styles = {
148
+ root: {
149
+ width: '100%', height: '100%',
150
+ display: 'flex', flexDirection: 'column',
151
+ background: 'var(--bg-base)', color: 'var(--text-primary)',
152
+ },
153
+ header: {
154
+ height: 40,
155
+ padding: '0 16px',
156
+ borderBottom: '1px solid var(--border)',
157
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
158
+ background: 'var(--bg-chrome)',
159
+ flexShrink: 0,
160
+ position: 'relative',
161
+ },
162
+ headerLeft: {
163
+ display: 'flex', alignItems: 'center', gap: 8,
164
+ },
165
+ logo: {
166
+ fontSize: 13, fontWeight: 600, letterSpacing: 1.5,
167
+ color: 'var(--text-bright)',
168
+ },
169
+ headerCenter: {
170
+ display: 'flex', alignItems: 'center', gap: 0,
171
+ },
172
+ headerRight: {
173
+ display: 'flex', alignItems: 'center', gap: 10,
174
+ },
175
+ tabBtn: {
176
+ padding: '10px 14px',
177
+ background: 'transparent',
178
+ border: 'none',
179
+ borderBottom: '2px solid transparent',
180
+ fontSize: 12, fontWeight: 500,
181
+ fontFamily: 'var(--font)',
182
+ cursor: 'pointer',
183
+ transition: 'color 0.1s',
184
+ },
185
+ spawnBtn: {
186
+ padding: '4px 12px',
187
+ background: 'transparent',
188
+ border: '1px solid var(--accent)',
189
+ borderRadius: 2,
190
+ color: 'var(--accent)', fontSize: 12, fontWeight: 600,
191
+ fontFamily: 'var(--font)',
192
+ cursor: 'pointer',
193
+ },
194
+ agentCount: { fontSize: 11, color: 'var(--text-dim)' },
195
+ statusText: { fontSize: 11, color: 'var(--text-dim)', fontStyle: 'italic' },
196
+ mainRow: {
197
+ flex: 1, display: 'flex', overflow: 'hidden',
198
+ },
199
+ content: {
200
+ flex: 1, overflow: 'hidden', position: 'relative',
201
+ },
202
+ detailPanel: {
203
+ width: 320, flexShrink: 0,
204
+ background: 'var(--bg-chrome)',
205
+ borderLeft: '1px solid var(--border)',
206
+ padding: 16, overflowY: 'auto',
207
+ position: 'relative',
208
+ },
209
+ closeBtn: {
210
+ position: 'absolute', top: 8, right: 10,
211
+ background: 'none', border: 'none', color: 'var(--text-dim)',
212
+ fontSize: 14, cursor: 'pointer', fontFamily: 'var(--font)',
213
+ padding: '2px 6px',
214
+ },
215
+ };
@@ -0,0 +1,347 @@
1
+ // GROOVE GUI — Agent Actions Tab
2
+ // FSL-1.1-Apache-2.0 — see LICENSE
3
+
4
+ import React, { useState, useEffect } from 'react';
5
+ import { useGrooveStore } from '../stores/groove';
6
+
7
+ export default function AgentActions({ agent }) {
8
+ const killAgent = useGrooveStore((s) => s.killAgent);
9
+ const rotateAgent = useGrooveStore((s) => s.rotateAgent);
10
+ const spawnAgent = useGrooveStore((s) => s.spawnAgent);
11
+ const instructAgent = useGrooveStore((s) => s.instructAgent);
12
+ const showStatus = useGrooveStore((s) => s.showStatus);
13
+ const closeDetail = useGrooveStore((s) => s.closeDetail);
14
+
15
+ const [confirmKill, setConfirmKill] = useState(false);
16
+ const [confirmDelete, setConfirmDelete] = useState(false);
17
+ const [editPrompt, setEditPrompt] = useState('');
18
+ const [editingPrompt, setEditingPrompt] = useState(false);
19
+ const [selectedModel, setSelectedModel] = useState(agent.model || '');
20
+ const [providerList, setProviderList] = useState([]);
21
+
22
+ const isAlive = agent.status === 'running' || agent.status === 'starting';
23
+
24
+ useEffect(() => {
25
+ fetch('/api/providers').then(r => r.json()).then(setProviderList).catch(() => {});
26
+ }, []);
27
+
28
+ const currentProvider = providerList.find((p) => p.id === agent.provider);
29
+ const models = currentProvider?.models || [];
30
+
31
+ async function handleRotate() {
32
+ try {
33
+ await rotateAgent(agent.id);
34
+ } catch (err) {
35
+ showStatus(`rotate failed: ${err.message}`);
36
+ }
37
+ }
38
+
39
+ async function handleKill() {
40
+ if (!confirmKill) {
41
+ setConfirmKill(true);
42
+ setTimeout(() => setConfirmKill(false), 3000);
43
+ return;
44
+ }
45
+ try {
46
+ await killAgent(agent.id);
47
+ showStatus(`${agent.name} killed`);
48
+ } catch (err) {
49
+ showStatus(`kill failed: ${err.message}`);
50
+ }
51
+ setConfirmKill(false);
52
+ }
53
+
54
+ async function handleDelete() {
55
+ if (!confirmDelete) {
56
+ setConfirmDelete(true);
57
+ setTimeout(() => setConfirmDelete(false), 3000);
58
+ return;
59
+ }
60
+ try {
61
+ await killAgent(agent.id, true);
62
+ closeDetail();
63
+ showStatus(`${agent.name} deleted`);
64
+ } catch (err) {
65
+ showStatus(`delete failed: ${err.message}`);
66
+ }
67
+ setConfirmDelete(false);
68
+ }
69
+
70
+ async function handleClone() {
71
+ try {
72
+ const newAgent = await spawnAgent({
73
+ role: agent.role,
74
+ scope: agent.scope,
75
+ prompt: agent.prompt,
76
+ provider: agent.provider,
77
+ model: agent.model,
78
+ });
79
+ showStatus(`cloned as ${newAgent.name}`);
80
+ } catch (err) {
81
+ showStatus(`clone failed: ${err.message}`);
82
+ }
83
+ }
84
+
85
+ async function handleModelChange(newModel) {
86
+ setSelectedModel(newModel);
87
+ try {
88
+ // Model change requires rotation to take effect
89
+ await fetch(`/api/agents/${agent.id}`, {
90
+ method: 'PATCH',
91
+ headers: { 'Content-Type': 'application/json' },
92
+ body: JSON.stringify({ model: newModel || null }),
93
+ });
94
+ showStatus(`model set to ${newModel || 'default'} (takes effect on next rotation)`);
95
+ } catch (err) {
96
+ showStatus(`model change failed: ${err.message}`);
97
+ }
98
+ }
99
+
100
+ async function handlePromptSave() {
101
+ if (!editPrompt.trim()) return;
102
+ try {
103
+ // Send as an instruction — rotates agent with new prompt
104
+ await instructAgent(agent.id, editPrompt.trim());
105
+ setEditingPrompt(false);
106
+ setEditPrompt('');
107
+ } catch (err) {
108
+ showStatus(`prompt update failed: ${err.message}`);
109
+ }
110
+ }
111
+
112
+ async function handleRestart() {
113
+ try {
114
+ // Purge old dead entry first so name can be reused
115
+ await killAgent(agent.id, true);
116
+ const newAgent = await spawnAgent({
117
+ role: agent.role,
118
+ scope: agent.scope,
119
+ prompt: agent.prompt,
120
+ provider: agent.provider,
121
+ model: agent.model,
122
+ });
123
+ showStatus(`restarted as ${newAgent.name}`);
124
+ } catch (err) {
125
+ showStatus(`restart failed: ${err.message}`);
126
+ }
127
+ }
128
+
129
+ return (
130
+ <div style={styles.container}>
131
+ {/* Lifecycle controls */}
132
+ <div style={styles.sectionLabel}>LIFECYCLE</div>
133
+
134
+ <div style={styles.btnGrid}>
135
+ {isAlive && (
136
+ <>
137
+ <ActionButton
138
+ icon="~"
139
+ label="Rotate"
140
+ desc="Fresh context + handoff brief"
141
+ onClick={handleRotate}
142
+ color="var(--accent)"
143
+ />
144
+ <ActionButton
145
+ icon="||"
146
+ label={confirmKill ? 'Confirm Kill' : 'Stop'}
147
+ desc="Stop the agent process"
148
+ onClick={handleKill}
149
+ color={confirmKill ? 'var(--red)' : 'var(--amber)'}
150
+ />
151
+ </>
152
+ )}
153
+ {!isAlive && (
154
+ <ActionButton
155
+ icon=">"
156
+ label="Restart"
157
+ desc="Spawn fresh with same config"
158
+ onClick={handleRestart}
159
+ color="var(--green)"
160
+ />
161
+ )}
162
+ <ActionButton
163
+ icon="+"
164
+ label="Clone"
165
+ desc="Spawn duplicate agent"
166
+ onClick={handleClone}
167
+ color="var(--accent)"
168
+ />
169
+ <ActionButton
170
+ icon="x"
171
+ label={confirmDelete ? 'Confirm Delete' : 'Delete'}
172
+ desc="Kill and remove permanently"
173
+ onClick={handleDelete}
174
+ color={confirmDelete ? 'var(--red)' : 'var(--text-dim)'}
175
+ />
176
+ </div>
177
+
178
+ {/* Model selector */}
179
+ <div style={{ ...styles.sectionLabel, marginTop: 20 }}>MODEL</div>
180
+ <select
181
+ style={styles.select}
182
+ value={selectedModel}
183
+ onChange={(e) => handleModelChange(e.target.value)}
184
+ >
185
+ <option value="">Default</option>
186
+ {models.map((m) => (
187
+ <option key={m.id} value={m.id}>
188
+ {m.name} ({m.tier})
189
+ </option>
190
+ ))}
191
+ </select>
192
+ <div style={styles.fieldHint}>Changes take effect on next rotation</div>
193
+
194
+ {/* Prompt modification */}
195
+ <div style={{ ...styles.sectionLabel, marginTop: 20 }}>PROMPT</div>
196
+ {agent.prompt && !editingPrompt && (
197
+ <div style={styles.currentPrompt}>
198
+ <div style={styles.promptText}>{agent.prompt}</div>
199
+ {isAlive && (
200
+ <button
201
+ onClick={() => { setEditingPrompt(true); setEditPrompt(''); }}
202
+ style={styles.editBtn}
203
+ >
204
+ Send New Instruction
205
+ </button>
206
+ )}
207
+ </div>
208
+ )}
209
+ {!agent.prompt && !editingPrompt && (
210
+ <div style={styles.noPrompt}>No prompt set</div>
211
+ )}
212
+ {editingPrompt && (
213
+ <div>
214
+ <textarea
215
+ style={styles.textarea}
216
+ value={editPrompt}
217
+ onChange={(e) => setEditPrompt(e.target.value)}
218
+ placeholder="New instruction for this agent..."
219
+ rows={4}
220
+ autoFocus
221
+ />
222
+ <div style={{ display: 'flex', gap: 6, marginTop: 6 }}>
223
+ <button onClick={handlePromptSave} style={styles.saveBtn} disabled={!editPrompt.trim()}>
224
+ Send (rotates agent)
225
+ </button>
226
+ <button onClick={() => setEditingPrompt(false)} style={styles.cancelBtn}>
227
+ Cancel
228
+ </button>
229
+ </div>
230
+ </div>
231
+ )}
232
+
233
+ {/* Current config */}
234
+ <div style={{ ...styles.sectionLabel, marginTop: 20 }}>CONFIGURATION</div>
235
+ <ConfigRow label="ID" value={agent.id} />
236
+ <ConfigRow label="Role" value={agent.role} />
237
+ <ConfigRow label="Provider" value={agent.provider} />
238
+ <ConfigRow label="Model" value={agent.model || 'default'} />
239
+ <ConfigRow label="Scope" value={(agent.scope || []).join(', ') || 'unrestricted'} />
240
+ <ConfigRow label="Status" value={agent.status} />
241
+ </div>
242
+ );
243
+ }
244
+
245
+ function ActionButton({ icon, label, desc, onClick, color }) {
246
+ return (
247
+ <button onClick={onClick} style={{ ...styles.actionBtn, borderColor: color }}>
248
+ <span style={{ ...styles.actionIcon, color }}>{icon}</span>
249
+ <div>
250
+ <div style={styles.actionTitle}>{label}</div>
251
+ <div style={styles.actionDesc}>{desc}</div>
252
+ </div>
253
+ </button>
254
+ );
255
+ }
256
+
257
+ function ConfigRow({ label, value }) {
258
+ return (
259
+ <div style={styles.configRow}>
260
+ <span style={{ color: 'var(--text-dim)', fontSize: 11, minWidth: 60 }}>{label}</span>
261
+ <span style={{
262
+ color: 'var(--text-primary)', fontSize: 11,
263
+ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
264
+ }}>
265
+ {value}
266
+ </span>
267
+ </div>
268
+ );
269
+ }
270
+
271
+ const styles = {
272
+ container: {
273
+ flex: 1, overflowY: 'auto', padding: '10px 0',
274
+ },
275
+ sectionLabel: {
276
+ fontSize: 11, color: 'var(--text-dim)', textTransform: 'uppercase',
277
+ letterSpacing: 1.5, marginBottom: 8, fontWeight: 600,
278
+ },
279
+ btnGrid: {
280
+ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4,
281
+ },
282
+ actionBtn: {
283
+ display: 'flex', alignItems: 'center', gap: 8,
284
+ padding: '8px 10px',
285
+ background: 'var(--bg-surface)', border: '1px solid var(--border)',
286
+ borderRadius: 2,
287
+ cursor: 'pointer', textAlign: 'left',
288
+ fontFamily: 'var(--font)',
289
+ },
290
+ actionIcon: {
291
+ fontSize: 14, fontWeight: 700,
292
+ width: 18, textAlign: 'center', flexShrink: 0,
293
+ },
294
+ actionTitle: {
295
+ fontSize: 11, color: 'var(--text-primary)', fontWeight: 600,
296
+ },
297
+ actionDesc: {
298
+ fontSize: 9, color: 'var(--text-dim)', marginTop: 1,
299
+ },
300
+ select: {
301
+ width: '100%', background: 'var(--bg-surface)', border: '1px solid var(--border)',
302
+ borderRadius: 2, padding: '6px 8px',
303
+ color: 'var(--text-primary)', fontSize: 12,
304
+ fontFamily: 'var(--font)', outline: 'none',
305
+ },
306
+ fieldHint: {
307
+ fontSize: 10, color: 'var(--text-muted)', marginTop: 4,
308
+ },
309
+ currentPrompt: {},
310
+ promptText: {
311
+ background: 'var(--bg-base)', border: '1px solid var(--border)', borderRadius: 2,
312
+ padding: 8, fontSize: 12, color: 'var(--text-primary)', lineHeight: 1.5,
313
+ whiteSpace: 'pre-wrap', maxHeight: 100, overflowY: 'auto',
314
+ },
315
+ noPrompt: {
316
+ fontSize: 12, color: 'var(--text-dim)', fontStyle: 'italic',
317
+ },
318
+ editBtn: {
319
+ marginTop: 6, padding: '4px 10px',
320
+ background: 'transparent', border: '1px solid var(--accent)',
321
+ borderRadius: 2,
322
+ color: 'var(--accent)', fontSize: 11, fontWeight: 600,
323
+ fontFamily: 'var(--font)', cursor: 'pointer',
324
+ },
325
+ textarea: {
326
+ width: '100%', background: 'var(--bg-surface)', border: '1px solid var(--border)',
327
+ borderRadius: 2, padding: '6px 8px',
328
+ color: 'var(--text-primary)', fontSize: 12,
329
+ fontFamily: 'var(--font)', outline: 'none', resize: 'vertical',
330
+ },
331
+ saveBtn: {
332
+ flex: 1, padding: '6px',
333
+ background: 'transparent', border: '1px solid var(--accent)',
334
+ borderRadius: 2, color: 'var(--accent)', fontSize: 11, fontWeight: 600,
335
+ fontFamily: 'var(--font)', cursor: 'pointer',
336
+ },
337
+ cancelBtn: {
338
+ padding: '6px 12px',
339
+ background: 'transparent', border: '1px solid var(--border)',
340
+ borderRadius: 2, color: 'var(--text-dim)', fontSize: 11,
341
+ fontFamily: 'var(--font)', cursor: 'pointer',
342
+ },
343
+ configRow: {
344
+ display: 'flex', gap: 8, padding: '3px 0',
345
+ borderBottom: '1px solid var(--bg-surface)',
346
+ },
347
+ };