groove-dev 0.27.15 → 0.27.18

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 (172) hide show
  1. package/CLAUDE.md +0 -10
  2. package/README.md +37 -1
  3. package/developerID_application.cer +0 -0
  4. package/node_modules/@groove-dev/daemon/src/api.js +586 -67
  5. package/node_modules/@groove-dev/daemon/src/classifier.js +24 -0
  6. package/node_modules/@groove-dev/daemon/src/credentials.js +12 -2
  7. package/node_modules/@groove-dev/daemon/src/federation/ambassador.js +204 -0
  8. package/node_modules/@groove-dev/daemon/src/federation/connection.js +359 -0
  9. package/node_modules/@groove-dev/daemon/src/federation/contracts.js +112 -0
  10. package/node_modules/@groove-dev/daemon/src/federation/whitelist.js +190 -0
  11. package/node_modules/@groove-dev/daemon/src/federation.js +166 -7
  12. package/node_modules/@groove-dev/daemon/src/index.js +172 -19
  13. package/node_modules/@groove-dev/daemon/src/introducer.js +52 -7
  14. package/node_modules/@groove-dev/daemon/src/journalist.js +46 -1
  15. package/node_modules/@groove-dev/daemon/src/memory.js +36 -16
  16. package/node_modules/@groove-dev/daemon/src/process.js +140 -23
  17. package/node_modules/@groove-dev/daemon/src/providers/base.js +1 -0
  18. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +14 -0
  19. package/node_modules/@groove-dev/daemon/src/providers/codex.js +124 -28
  20. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +104 -17
  21. package/node_modules/@groove-dev/daemon/src/providers/index.js +17 -0
  22. package/node_modules/@groove-dev/daemon/src/registry.js +10 -1
  23. package/node_modules/@groove-dev/daemon/src/rotator.js +93 -30
  24. package/node_modules/@groove-dev/daemon/src/skills.js +33 -3
  25. package/node_modules/@groove-dev/daemon/src/terminal-pty.js +9 -1
  26. package/node_modules/@groove-dev/daemon/src/tool-executor.js +11 -5
  27. package/node_modules/@groove-dev/daemon/src/toys.js +69 -0
  28. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +24 -5
  29. package/node_modules/@groove-dev/daemon/templates/toys-catalog.json +242 -0
  30. package/node_modules/@groove-dev/daemon/test/classifier.test.js +98 -0
  31. package/node_modules/@groove-dev/daemon/test/introducer.test.js +72 -1
  32. package/node_modules/@groove-dev/daemon/test/journalist.test.js +117 -0
  33. package/node_modules/@groove-dev/daemon/test/memory.test.js +37 -1
  34. package/node_modules/@groove-dev/daemon/test/rotator.test.js +183 -4
  35. package/node_modules/@groove-dev/gui/dist/assets/index-Bg6_D2xK.css +1 -0
  36. package/node_modules/@groove-dev/gui/dist/assets/index-D3rvwTHD.js +8607 -0
  37. package/node_modules/@groove-dev/gui/dist/index.html +3 -2
  38. package/node_modules/@groove-dev/gui/index.html +1 -0
  39. package/node_modules/@groove-dev/gui/src/app.css +7 -0
  40. package/node_modules/@groove-dev/gui/src/app.jsx +37 -10
  41. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +21 -31
  42. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +11 -6
  43. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
  44. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +42 -1
  45. package/node_modules/@groove-dev/gui/src/components/editor/breadcrumbs.jsx +30 -0
  46. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +33 -2
  47. package/node_modules/@groove-dev/gui/src/components/editor/editor-status-bar.jsx +26 -0
  48. package/node_modules/@groove-dev/gui/src/components/editor/editor-tabs.jsx +113 -34
  49. package/node_modules/@groove-dev/gui/src/components/editor/goto-line.jsx +35 -0
  50. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +12 -6
  51. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +15 -3
  52. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +0 -1
  53. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
  54. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +6 -2
  55. package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +11 -1
  56. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +10 -9
  57. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +9 -1
  58. package/node_modules/@groove-dev/gui/src/components/onboarding/provider-card.jsx +134 -0
  59. package/node_modules/@groove-dev/gui/src/components/onboarding/setup-wizard.jsx +819 -0
  60. package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +12 -5
  61. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +15 -8
  62. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-modal.jsx +151 -0
  63. package/node_modules/@groove-dev/gui/src/components/settings/federation-activity.jsx +98 -0
  64. package/node_modules/@groove-dev/gui/src/components/settings/federation-panel.jsx +290 -0
  65. package/node_modules/@groove-dev/gui/src/components/settings/federation-peers.jsx +126 -0
  66. package/node_modules/@groove-dev/gui/src/components/settings/federation-wizard.jsx +293 -0
  67. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +110 -67
  68. package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +3 -3
  69. package/node_modules/@groove-dev/gui/src/components/settings/server-detail.jsx +310 -0
  70. package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +4 -1
  71. package/node_modules/@groove-dev/gui/src/components/settings/server-list.jsx +59 -0
  72. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +549 -0
  73. package/node_modules/@groove-dev/gui/src/components/toys/toy-card.jsx +78 -0
  74. package/node_modules/@groove-dev/gui/src/components/toys/toy-creator.jsx +144 -0
  75. package/node_modules/@groove-dev/gui/src/components/toys/toy-launcher.jsx +187 -0
  76. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +2 -2
  77. package/node_modules/@groove-dev/gui/src/lib/electron.js +15 -0
  78. package/node_modules/@groove-dev/gui/src/lib/format.js +1 -0
  79. package/node_modules/@groove-dev/gui/src/stores/groove.js +388 -63
  80. package/node_modules/@groove-dev/gui/src/views/agents.jsx +148 -42
  81. package/node_modules/@groove-dev/gui/src/views/editor.jsx +92 -2
  82. package/node_modules/@groove-dev/gui/src/views/federation.jsx +37 -0
  83. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +2 -42
  84. package/node_modules/@groove-dev/gui/src/views/settings.jsx +35 -134
  85. package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +327 -0
  86. package/node_modules/@groove-dev/gui/src/views/teams.jsx +3 -3
  87. package/node_modules/@groove-dev/gui/src/views/toys.jsx +162 -0
  88. package/package.json +1 -1
  89. package/packages/daemon/src/api.js +586 -67
  90. package/packages/daemon/src/classifier.js +24 -0
  91. package/packages/daemon/src/credentials.js +12 -2
  92. package/packages/daemon/src/federation/ambassador.js +204 -0
  93. package/packages/daemon/src/federation/connection.js +359 -0
  94. package/packages/daemon/src/federation/contracts.js +112 -0
  95. package/packages/daemon/src/federation/whitelist.js +190 -0
  96. package/packages/daemon/src/federation.js +166 -7
  97. package/packages/daemon/src/index.js +172 -19
  98. package/packages/daemon/src/introducer.js +52 -7
  99. package/packages/daemon/src/journalist.js +46 -1
  100. package/packages/daemon/src/memory.js +36 -16
  101. package/packages/daemon/src/process.js +140 -23
  102. package/packages/daemon/src/providers/base.js +1 -0
  103. package/packages/daemon/src/providers/claude-code.js +14 -0
  104. package/packages/daemon/src/providers/codex.js +124 -28
  105. package/packages/daemon/src/providers/gemini.js +104 -17
  106. package/packages/daemon/src/providers/index.js +17 -0
  107. package/packages/daemon/src/registry.js +10 -1
  108. package/packages/daemon/src/rotator.js +93 -30
  109. package/packages/daemon/src/skills.js +33 -3
  110. package/packages/daemon/src/terminal-pty.js +9 -1
  111. package/packages/daemon/src/tool-executor.js +11 -5
  112. package/packages/daemon/src/toys.js +69 -0
  113. package/packages/daemon/src/tunnel-manager.js +24 -5
  114. package/packages/daemon/templates/toys-catalog.json +242 -0
  115. package/packages/gui/dist/assets/index-Bg6_D2xK.css +1 -0
  116. package/packages/gui/dist/assets/index-D3rvwTHD.js +8607 -0
  117. package/packages/gui/dist/index.html +3 -2
  118. package/packages/gui/index.html +1 -0
  119. package/packages/gui/src/app.css +7 -0
  120. package/packages/gui/src/app.jsx +37 -10
  121. package/packages/gui/src/components/agents/agent-chat.jsx +21 -31
  122. package/packages/gui/src/components/agents/agent-config.jsx +11 -6
  123. package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
  124. package/packages/gui/src/components/agents/spawn-wizard.jsx +42 -1
  125. package/packages/gui/src/components/editor/breadcrumbs.jsx +30 -0
  126. package/packages/gui/src/components/editor/code-editor.jsx +33 -2
  127. package/packages/gui/src/components/editor/editor-status-bar.jsx +26 -0
  128. package/packages/gui/src/components/editor/editor-tabs.jsx +113 -34
  129. package/packages/gui/src/components/editor/goto-line.jsx +35 -0
  130. package/packages/gui/src/components/editor/terminal.jsx +12 -6
  131. package/packages/gui/src/components/layout/activity-bar.jsx +15 -3
  132. package/packages/gui/src/components/layout/app-shell.jsx +0 -1
  133. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
  134. package/packages/gui/src/components/layout/command-palette.jsx +6 -2
  135. package/packages/gui/src/components/layout/status-bar.jsx +11 -1
  136. package/packages/gui/src/components/layout/terminal-panel.jsx +10 -9
  137. package/packages/gui/src/components/marketplace/repo-import.jsx +9 -1
  138. package/packages/gui/src/components/onboarding/provider-card.jsx +134 -0
  139. package/packages/gui/src/components/onboarding/setup-wizard.jsx +819 -0
  140. package/packages/gui/src/components/pro/pro-gate.jsx +12 -5
  141. package/packages/gui/src/components/pro/upgrade-card.jsx +15 -8
  142. package/packages/gui/src/components/pro/upgrade-modal.jsx +151 -0
  143. package/packages/gui/src/components/settings/federation-activity.jsx +98 -0
  144. package/packages/gui/src/components/settings/federation-panel.jsx +290 -0
  145. package/packages/gui/src/components/settings/federation-peers.jsx +126 -0
  146. package/packages/gui/src/components/settings/federation-wizard.jsx +293 -0
  147. package/packages/gui/src/components/settings/quick-connect.jsx +110 -67
  148. package/packages/gui/src/components/settings/remote-server-card.jsx +3 -3
  149. package/packages/gui/src/components/settings/server-detail.jsx +310 -0
  150. package/packages/gui/src/components/settings/server-dialog.jsx +4 -1
  151. package/packages/gui/src/components/settings/server-list.jsx +59 -0
  152. package/packages/gui/src/components/settings/ssh-wizard.jsx +549 -0
  153. package/packages/gui/src/components/toys/toy-card.jsx +78 -0
  154. package/packages/gui/src/components/toys/toy-creator.jsx +144 -0
  155. package/packages/gui/src/components/toys/toy-launcher.jsx +187 -0
  156. package/packages/gui/src/components/ui/toast.jsx +2 -2
  157. package/packages/gui/src/lib/electron.js +15 -0
  158. package/packages/gui/src/lib/format.js +1 -0
  159. package/packages/gui/src/stores/groove.js +388 -63
  160. package/packages/gui/src/views/agents.jsx +148 -42
  161. package/packages/gui/src/views/editor.jsx +92 -2
  162. package/packages/gui/src/views/federation.jsx +37 -0
  163. package/packages/gui/src/views/marketplace.jsx +2 -42
  164. package/packages/gui/src/views/settings.jsx +35 -134
  165. package/packages/gui/src/views/subscription-panel.jsx +327 -0
  166. package/packages/gui/src/views/teams.jsx +3 -3
  167. package/packages/gui/src/views/toys.jsx +162 -0
  168. package/plans/chat-persistence-refactor.md +154 -0
  169. package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +0 -1
  170. package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +0 -677
  171. package/packages/gui/dist/assets/index-BE6lYcd7.css +0 -1
  172. package/packages/gui/dist/assets/index-zdzOLAZM.js +0 -677
@@ -0,0 +1,126 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState } from 'react';
3
+ import { useGrooveStore } from '../../stores/groove';
4
+ import { Button } from '../ui/button';
5
+ import { Badge } from '../ui/badge';
6
+ import { StatusDot } from '../ui/status-dot';
7
+ import { cn } from '../../lib/cn';
8
+ import {
9
+ Server, Link2, Send, Unplug, Eye, Plus, Wifi,
10
+ } from 'lucide-react';
11
+
12
+ function connectionBadge(state) {
13
+ switch (state) {
14
+ case 'established': return <Badge variant="success" className="text-2xs gap-1"><StatusDot status="running" size="sm" /> Connected</Badge>;
15
+ case 'connecting': return <Badge variant="warning" className="text-2xs" dot="pulse">Connecting</Badge>;
16
+ case 'error': return <Badge variant="danger" className="text-2xs">Error</Badge>;
17
+ default: return <Badge variant="default" className="text-2xs">Unknown</Badge>;
18
+ }
19
+ }
20
+
21
+ export function FederationPeers({ onOpenWizard }) {
22
+ const connections = useGrooveStore((s) => s.federation.connections);
23
+ const peers = useGrooveStore((s) => s.federation.peers);
24
+ const sendPouch = useGrooveStore((s) => s.sendPouch);
25
+ const [sendingTo, setSendingTo] = useState(null);
26
+
27
+ const allPeers = peers.length > 0 ? peers : connections;
28
+
29
+ async function handleSendPouch(peerId) {
30
+ setSendingTo(peerId);
31
+ try {
32
+ await sendPouch(peerId, { type: 'ping' });
33
+ } catch {}
34
+ setSendingTo(null);
35
+ }
36
+
37
+ return (
38
+ <div className="space-y-3">
39
+ <div className="flex items-center justify-between">
40
+ <div className="flex items-center gap-2">
41
+ <Server size={12} className="text-accent" />
42
+ <span className="text-xs font-semibold text-text-1 font-sans">Connected Peers</span>
43
+ {allPeers.length > 0 && (
44
+ <Badge variant="success" className="text-2xs">{allPeers.length}</Badge>
45
+ )}
46
+ </div>
47
+ <Button size="sm" variant="primary" onClick={onOpenWizard} className="h-7 text-2xs gap-1.5">
48
+ <Plus size={11} />
49
+ Pair New Peer
50
+ </Button>
51
+ </div>
52
+
53
+ {allPeers.length === 0 ? (
54
+ <div className="rounded-lg border border-dashed border-border-subtle bg-surface-1/50 px-4 py-8 text-center">
55
+ <Link2 size={20} className="text-text-4 mx-auto mb-2" />
56
+ <p className="text-xs text-text-3 font-sans mb-1">No peers connected</p>
57
+ <p className="text-2xs text-text-4 font-sans mb-3">Pair with a remote Groove daemon to share agents and coordinate work.</p>
58
+ <Button size="sm" variant="outline" onClick={onOpenWizard} className="h-7 text-2xs gap-1.5">
59
+ <Plus size={11} />
60
+ Pair Your First Peer
61
+ </Button>
62
+ </div>
63
+ ) : (
64
+ <div className="grid gap-2">
65
+ {allPeers.map((peer) => {
66
+ const id = peer.peerId || peer.ip || peer.id;
67
+ const name = peer.name || peer.peerId || 'Unknown Peer';
68
+ const state = peer.state || peer.status || 'unknown';
69
+ const ip = peer.ip || peer.address || '';
70
+ const latency = peer.latency;
71
+
72
+ return (
73
+ <div key={id} className="rounded-md border border-border-subtle bg-surface-1 p-3">
74
+ <div className="flex items-start gap-3">
75
+ <div className="flex h-8 w-8 items-center justify-center rounded-md bg-accent/10 flex-shrink-0 mt-0.5">
76
+ <Server size={14} className="text-accent" />
77
+ </div>
78
+ <div className="flex-1 min-w-0">
79
+ <div className="flex items-center gap-2 mb-0.5">
80
+ <span className="text-xs font-semibold text-text-0 font-sans truncate">{name}</span>
81
+ {connectionBadge(state)}
82
+ </div>
83
+ <div className="flex items-center gap-3 text-2xs text-text-3">
84
+ {ip && (
85
+ <span className="font-mono truncate">{ip}{peer.port ? `:${peer.port}` : ''}</span>
86
+ )}
87
+ {latency != null && (
88
+ <span className="flex items-center gap-1 font-sans">
89
+ <Wifi size={9} className={cn(
90
+ latency < 100 ? 'text-success' : latency < 300 ? 'text-warning' : 'text-danger'
91
+ )} />
92
+ {latency}ms
93
+ </span>
94
+ )}
95
+ </div>
96
+ </div>
97
+ </div>
98
+
99
+ <div className="flex items-center gap-1.5 mt-3 pt-2.5 border-t border-border-subtle">
100
+ <Button size="sm" variant="ghost" className="h-6 text-2xs gap-1 text-text-2">
101
+ <Eye size={10} />
102
+ Details
103
+ </Button>
104
+ <Button
105
+ size="sm"
106
+ variant="ghost"
107
+ className="h-6 text-2xs gap-1 text-text-2"
108
+ disabled={sendingTo === id}
109
+ onClick={() => handleSendPouch(id)}
110
+ >
111
+ <Send size={10} />
112
+ Send Pouch
113
+ </Button>
114
+ <Button size="sm" variant="ghost" className="h-6 text-2xs gap-1 text-danger/70 hover:text-danger ml-auto">
115
+ <Unplug size={10} />
116
+ Disconnect
117
+ </Button>
118
+ </div>
119
+ </div>
120
+ );
121
+ })}
122
+ </div>
123
+ )}
124
+ </div>
125
+ );
126
+ }
@@ -0,0 +1,293 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState } from 'react';
3
+ import { useGrooveStore } from '../../stores/groove';
4
+ import { Dialog, DialogContent } from '../ui/dialog';
5
+ import { Button } from '../ui/button';
6
+ import { Input } from '../ui/input';
7
+ import { Badge } from '../ui/badge';
8
+ import { StatusDot } from '../ui/status-dot';
9
+ import { cn } from '../../lib/cn';
10
+ import {
11
+ Link2, Loader2, Check, Server, ArrowRight, Wifi, AlertCircle,
12
+ } from 'lucide-react';
13
+
14
+ const STEPS = [
15
+ { label: 'Connect', icon: Link2 },
16
+ { label: 'Verify', icon: Server },
17
+ { label: 'Paired', icon: Check },
18
+ ];
19
+
20
+ function StepIndicator({ current }) {
21
+ return (
22
+ <div className="flex items-center justify-center gap-2 mb-6">
23
+ {STEPS.map((step, i) => {
24
+ const done = i < current;
25
+ const active = i === current;
26
+ const Icon = step.icon;
27
+ return (
28
+ <div key={step.label} className="flex items-center gap-2">
29
+ {i > 0 && (
30
+ <div className={cn(
31
+ 'w-8 h-px',
32
+ done ? 'bg-accent' : 'bg-border-subtle',
33
+ )} />
34
+ )}
35
+ <div className="flex items-center gap-1.5">
36
+ <div className={cn(
37
+ 'w-6 h-6 rounded-full flex items-center justify-center transition-colors',
38
+ done ? 'bg-accent text-white' : active ? 'bg-accent/15 text-accent border border-accent/30' : 'bg-surface-3 text-text-4',
39
+ )}>
40
+ {done ? <Check size={12} /> : <Icon size={12} />}
41
+ </div>
42
+ <span className={cn(
43
+ 'text-2xs font-semibold font-sans',
44
+ active ? 'text-text-0' : done ? 'text-accent' : 'text-text-4',
45
+ )}>
46
+ {step.label}
47
+ </span>
48
+ </div>
49
+ </div>
50
+ );
51
+ })}
52
+ </div>
53
+ );
54
+ }
55
+
56
+ export function FederationWizard({ open, onOpenChange }) {
57
+ const addToWhitelist = useGrooveStore((s) => s.addToWhitelist);
58
+ const fetchFederationStatus = useGrooveStore((s) => s.fetchFederationStatus);
59
+
60
+ const [step, setStep] = useState(0);
61
+ const [ip, setIp] = useState('');
62
+ const [port, setPort] = useState('31415');
63
+ const [name, setName] = useState('');
64
+ const [testing, setTesting] = useState(false);
65
+ const [testResult, setTestResult] = useState(null);
66
+ const [pairing, setPairing] = useState(false);
67
+ const [error, setError] = useState(null);
68
+ const [remoteInfo, setRemoteInfo] = useState(null);
69
+
70
+ function reset() {
71
+ setStep(0);
72
+ setIp('');
73
+ setPort('31415');
74
+ setName('');
75
+ setTesting(false);
76
+ setTestResult(null);
77
+ setPairing(false);
78
+ setError(null);
79
+ setRemoteInfo(null);
80
+ }
81
+
82
+ function handleOpenChange(open) {
83
+ if (!open) reset();
84
+ onOpenChange(open);
85
+ }
86
+
87
+ async function testReachability() {
88
+ setTesting(true);
89
+ setTestResult(null);
90
+ setError(null);
91
+ try {
92
+ const target = `${ip.trim()}:${port || '31415'}`;
93
+ const res = await fetch(`http://localhost:31415/api/federation/test?target=${encodeURIComponent(target)}`);
94
+ if (res.ok) {
95
+ const data = await res.json();
96
+ setTestResult('reachable');
97
+ setRemoteInfo(data);
98
+ } else {
99
+ setTestResult('unreachable');
100
+ }
101
+ } catch {
102
+ setTestResult('unreachable');
103
+ }
104
+ setTesting(false);
105
+ }
106
+
107
+ async function handlePair() {
108
+ setPairing(true);
109
+ setError(null);
110
+ try {
111
+ await addToWhitelist(ip.trim(), parseInt(port, 10) || 31415, name.trim() || undefined);
112
+ await fetchFederationStatus();
113
+ setStep(2);
114
+ } catch (err) {
115
+ setError(err.message || 'Failed to pair with peer');
116
+ }
117
+ setPairing(false);
118
+ }
119
+
120
+ return (
121
+ <Dialog open={open} onOpenChange={handleOpenChange}>
122
+ <DialogContent title="Pair New Peer" description="Connect to a remote Groove daemon">
123
+ <div className="px-5 py-5">
124
+ <StepIndicator current={step} />
125
+
126
+ {step === 0 && (
127
+ <div className="space-y-4">
128
+ <Input
129
+ label="Friendly Name"
130
+ placeholder="e.g. Production Server"
131
+ value={name}
132
+ onChange={(e) => setName(e.target.value)}
133
+ />
134
+ <div className="grid grid-cols-[1fr,100px] gap-2">
135
+ <Input
136
+ label="IP / Hostname"
137
+ placeholder="192.168.1.100"
138
+ value={ip}
139
+ onChange={(e) => setIp(e.target.value)}
140
+ mono
141
+ />
142
+ <Input
143
+ label="Port"
144
+ placeholder="31415"
145
+ value={port}
146
+ onChange={(e) => setPort(e.target.value)}
147
+ mono
148
+ />
149
+ </div>
150
+
151
+ {testResult === 'reachable' && (
152
+ <div className="flex items-center gap-2 rounded-md bg-success/10 border border-success/20 px-3 py-2">
153
+ <Wifi size={13} className="text-success" />
154
+ <span className="text-2xs text-success font-sans font-medium">Peer is reachable</span>
155
+ </div>
156
+ )}
157
+ {testResult === 'unreachable' && (
158
+ <div className="flex items-center gap-2 rounded-md bg-danger/10 border border-danger/20 px-3 py-2">
159
+ <AlertCircle size={13} className="text-danger" />
160
+ <span className="text-2xs text-danger font-sans font-medium">Could not reach peer — check IP and port</span>
161
+ </div>
162
+ )}
163
+
164
+ <div className="flex items-center gap-2 pt-2">
165
+ <Button
166
+ size="sm"
167
+ variant="ghost"
168
+ disabled={!ip.trim() || testing}
169
+ onClick={testReachability}
170
+ className="h-8 text-xs gap-1.5"
171
+ >
172
+ {testing ? <Loader2 size={12} className="animate-spin" /> : <Wifi size={12} />}
173
+ Test Reachability
174
+ </Button>
175
+ <Button
176
+ size="sm"
177
+ variant="primary"
178
+ disabled={!ip.trim()}
179
+ onClick={() => setStep(1)}
180
+ className="h-8 text-xs gap-1.5 ml-auto"
181
+ >
182
+ Continue
183
+ <ArrowRight size={12} />
184
+ </Button>
185
+ </div>
186
+ </div>
187
+ )}
188
+
189
+ {step === 1 && (
190
+ <div className="space-y-4">
191
+ <div className="rounded-md border border-border-subtle bg-surface-0 p-4">
192
+ <div className="flex items-center gap-3 mb-3">
193
+ <div className="flex h-9 w-9 items-center justify-center rounded-md bg-accent/10">
194
+ <Server size={16} className="text-accent" />
195
+ </div>
196
+ <div>
197
+ <p className="text-sm font-semibold text-text-0 font-sans">{name || 'Remote Peer'}</p>
198
+ <p className="text-2xs text-text-3 font-mono">{ip}:{port || '31415'}</p>
199
+ </div>
200
+ </div>
201
+
202
+ {remoteInfo && (
203
+ <div className="space-y-1.5 pt-2 border-t border-border-subtle">
204
+ {remoteInfo.version && (
205
+ <div className="flex items-center justify-between text-2xs font-sans">
206
+ <span className="text-text-3">Version</span>
207
+ <span className="text-text-1 font-mono">{remoteInfo.version}</span>
208
+ </div>
209
+ )}
210
+ {remoteInfo.peerId && (
211
+ <div className="flex items-center justify-between text-2xs font-sans">
212
+ <span className="text-text-3">Peer ID</span>
213
+ <span className="text-text-1 font-mono truncate max-w-40">{remoteInfo.peerId}</span>
214
+ </div>
215
+ )}
216
+ {remoteInfo.agents != null && (
217
+ <div className="flex items-center justify-between text-2xs font-sans">
218
+ <span className="text-text-3">Active Agents</span>
219
+ <span className="text-text-1">{remoteInfo.agents}</span>
220
+ </div>
221
+ )}
222
+ </div>
223
+ )}
224
+ </div>
225
+
226
+ {error && (
227
+ <div className="flex items-center gap-2 rounded-md bg-danger/10 border border-danger/20 px-3 py-2">
228
+ <AlertCircle size={13} className="text-danger" />
229
+ <span className="text-2xs text-danger font-sans">{error}</span>
230
+ </div>
231
+ )}
232
+
233
+ <div className="flex items-center gap-2 pt-2">
234
+ <Button size="sm" variant="ghost" onClick={() => setStep(0)} className="h-8 text-xs">
235
+ Back
236
+ </Button>
237
+ <Button
238
+ size="sm"
239
+ variant="primary"
240
+ disabled={pairing}
241
+ onClick={handlePair}
242
+ className="h-8 text-xs gap-1.5 ml-auto"
243
+ >
244
+ {pairing ? <Loader2 size={12} className="animate-spin" /> : <Link2 size={12} />}
245
+ Confirm Pairing
246
+ </Button>
247
+ </div>
248
+ </div>
249
+ )}
250
+
251
+ {step === 2 && (
252
+ <div className="text-center space-y-4">
253
+ <div className="flex h-12 w-12 items-center justify-center rounded-full bg-success/15 mx-auto">
254
+ <Check size={20} className="text-success" />
255
+ </div>
256
+ <div>
257
+ <h3 className="text-sm font-semibold text-text-0 font-sans mb-1">Peer Paired Successfully</h3>
258
+ <p className="text-2xs text-text-3 font-sans">
259
+ {name || ip} has been added to your federation whitelist.
260
+ </p>
261
+ </div>
262
+
263
+ <div className="rounded-md border border-border-subtle bg-surface-0 p-3 text-left">
264
+ <div className="flex items-center gap-3">
265
+ <div className="flex h-8 w-8 items-center justify-center rounded-md bg-accent/10">
266
+ <Server size={14} className="text-accent" />
267
+ </div>
268
+ <div className="flex-1 min-w-0">
269
+ <span className="text-xs font-semibold text-text-0 font-sans block truncate">{name || 'Remote Peer'}</span>
270
+ <span className="text-2xs text-text-3 font-mono">{ip}:{port || '31415'}</span>
271
+ </div>
272
+ <Badge variant="success" className="text-2xs gap-1">
273
+ <StatusDot status="running" size="sm" />
274
+ Whitelisted
275
+ </Badge>
276
+ </div>
277
+ </div>
278
+
279
+ <Button
280
+ size="sm"
281
+ variant="primary"
282
+ onClick={() => handleOpenChange(false)}
283
+ className="h-8 text-xs"
284
+ >
285
+ Done
286
+ </Button>
287
+ </div>
288
+ )}
289
+ </div>
290
+ </DialogContent>
291
+ </Dialog>
292
+ );
293
+ }
@@ -4,15 +4,19 @@ import { useGrooveStore } from '../../stores/groove';
4
4
  import { cn } from '../../lib/cn';
5
5
  import { AnimatePresence, motion } from 'framer-motion';
6
6
  import {
7
- Server, Radio, ExternalLink, Loader2, X, Plus, Settings,
7
+ Server, Radio, ExternalLink, Loader2, X, Plus, ArrowLeft,
8
8
  } from 'lucide-react';
9
9
  import { StatusDot } from '../ui/status-dot';
10
+ import { Button } from '../ui/button';
11
+ import { SSHWizard } from './ssh-wizard';
10
12
 
11
13
  export function QuickConnect() {
12
14
  const open = useGrooveStore((s) => s.quickConnectOpen);
13
15
  const toggle = useGrooveStore((s) => s.toggleQuickConnect);
14
16
  const savedTunnels = useGrooveStore((s) => s.savedTunnels);
17
+ const addToast = useGrooveStore((s) => s.addToast);
15
18
  const [connectingId, setConnectingId] = useState(null);
19
+ const [showWizard, setShowWizard] = useState(false);
16
20
 
17
21
  if (!open) return null;
18
22
 
@@ -32,9 +36,14 @@ export function QuickConnect() {
32
36
  toggle();
33
37
  }
34
38
 
39
+ function handleClose() {
40
+ setShowWizard(false);
41
+ toggle();
42
+ }
43
+
35
44
  return (
36
45
  <>
37
- <div className="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm" onClick={toggle} />
46
+ <div className="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm" onClick={handleClose} />
38
47
 
39
48
  <AnimatePresence>
40
49
  <motion.div
@@ -42,85 +51,119 @@ export function QuickConnect() {
42
51
  animate={{ opacity: 1, y: 0, scale: 1 }}
43
52
  exit={{ opacity: 0, y: -10, scale: 0.98 }}
44
53
  transition={{ duration: 0.15 }}
45
- className="fixed top-[20%] left-1/2 -translate-x-1/2 z-50 w-[400px] bg-surface-1 border border-border rounded-lg shadow-2xl overflow-hidden"
54
+ className={cn(
55
+ 'fixed top-[15%] left-1/2 -translate-x-1/2 z-50 bg-surface-1 border border-border rounded-lg shadow-2xl overflow-hidden',
56
+ showWizard ? 'w-[520px]' : 'w-[400px]',
57
+ )}
46
58
  >
47
59
  {/* Header */}
48
60
  <div className="flex items-center justify-between px-4 py-3 border-b border-border-subtle">
49
61
  <div className="flex items-center gap-2">
62
+ {showWizard && (
63
+ <button
64
+ onClick={() => setShowWizard(false)}
65
+ className="p-1 -ml-1 text-text-4 hover:text-text-1 cursor-pointer transition-colors"
66
+ >
67
+ <ArrowLeft size={14} />
68
+ </button>
69
+ )}
50
70
  <Radio size={15} className="text-accent" />
51
- <span className="text-sm font-semibold text-text-0 font-sans">Quick Connect</span>
71
+ <span className="text-sm font-semibold text-text-0 font-sans">
72
+ {showWizard ? 'Add Connection' : 'Quick Connect'}
73
+ </span>
52
74
  </div>
53
- <button onClick={toggle} className="p-1 text-text-4 hover:text-text-1 cursor-pointer transition-colors">
75
+ <button onClick={handleClose} className="p-1 text-text-4 hover:text-text-1 cursor-pointer transition-colors">
54
76
  <X size={14} />
55
77
  </button>
56
78
  </div>
57
79
 
58
- {/* Server list */}
59
- <div className="overflow-y-auto max-h-[320px] py-1">
60
- {savedTunnels.length === 0 ? (
61
- <div className="px-4 py-8 text-center">
62
- <Server size={24} className="text-text-4 mx-auto mb-2" />
63
- <p className="text-sm text-text-3 font-sans">No saved servers</p>
64
- <p className="text-2xs text-text-4 font-sans mt-1">Add one in Settings to get started.</p>
65
- <button
66
- onClick={() => {
67
- toggle();
68
- useGrooveStore.getState().setActiveView('settings');
69
- }}
70
- className="mt-3 inline-flex items-center gap-1.5 text-xs text-accent hover:text-accent/80 font-sans cursor-pointer transition-colors"
71
- >
72
- <Settings size={12} /> Go to Settings
73
- </button>
80
+ {showWizard ? (
81
+ <SSHWizard
82
+ server={null}
83
+ onSave={async (data) => {
84
+ if (data.id) {
85
+ await useGrooveStore.getState().updateTunnel(data.id, data);
86
+ } else {
87
+ await useGrooveStore.getState().saveTunnel(data);
88
+ }
89
+ addToast('success', data.id ? 'Server updated' : 'Server added');
90
+ }}
91
+ onTest={() => {
92
+ const tunnels = useGrooveStore.getState().savedTunnels;
93
+ const last = tunnels[tunnels.length - 1];
94
+ if (last?.id) return useGrooveStore.getState().testTunnel(last.id);
95
+ }}
96
+ onConnect={() => {
97
+ const tunnels = useGrooveStore.getState().savedTunnels;
98
+ const last = tunnels[tunnels.length - 1];
99
+ if (last?.id) return useGrooveStore.getState().connectTunnel(last.id);
100
+ }}
101
+ onCancel={() => setShowWizard(false)}
102
+ />
103
+ ) : (
104
+ <>
105
+ {/* Server list */}
106
+ <div className="overflow-y-auto max-h-[320px] py-1">
107
+ {savedTunnels.length === 0 ? (
108
+ <div className="px-4 py-8 text-center">
109
+ <Server size={24} className="text-text-4 mx-auto mb-2" />
110
+ <p className="text-sm text-text-3 font-sans">No saved servers</p>
111
+ <p className="text-2xs text-text-4 font-sans mt-1">Add a connection to get started.</p>
112
+ <Button
113
+ variant="primary"
114
+ size="sm"
115
+ onClick={() => setShowWizard(true)}
116
+ className="h-8 text-xs gap-1.5 mt-3"
117
+ >
118
+ <Plus size={12} /> Add Connection
119
+ </Button>
120
+ </div>
121
+ ) : (
122
+ savedTunnels.map((server) => (
123
+ <button
124
+ key={server.id}
125
+ onClick={() => server.active ? handleOpenRemote(server) : handleConnect(server.id)}
126
+ disabled={connectingId === server.id}
127
+ className={cn(
128
+ 'w-full flex items-center gap-3 px-4 py-2.5 text-left cursor-pointer transition-colors',
129
+ 'hover:bg-surface-5',
130
+ connectingId === server.id && 'opacity-60 pointer-events-none',
131
+ )}
132
+ >
133
+ <Server size={15} className={server.active ? 'text-success' : 'text-text-4'} />
134
+ <div className="flex-1 min-w-0">
135
+ <div className="flex items-center gap-2">
136
+ <span className="text-sm font-medium text-text-0 font-sans truncate">{server.name}</span>
137
+ {server.active && <StatusDot status="running" size="sm" />}
138
+ </div>
139
+ <span className="text-2xs text-text-4 font-mono">{server.user}@{server.host}</span>
140
+ </div>
141
+ <div className="flex-shrink-0">
142
+ {connectingId === server.id ? (
143
+ <Loader2 size={14} className="text-text-3 animate-spin" />
144
+ ) : server.active ? (
145
+ <span className="flex items-center gap-1 text-2xs text-success font-sans">
146
+ <ExternalLink size={11} /> Open
147
+ </span>
148
+ ) : (
149
+ <span className="text-2xs text-text-3 font-sans">Connect</span>
150
+ )}
151
+ </div>
152
+ </button>
153
+ ))
154
+ )}
74
155
  </div>
75
- ) : (
76
- savedTunnels.map((server) => (
156
+
157
+ {/* Footer with Add button */}
158
+ <div className="px-4 py-2.5 border-t border-border-subtle">
77
159
  <button
78
- key={server.id}
79
- onClick={() => server.active ? handleOpenRemote(server) : handleConnect(server.id)}
80
- disabled={connectingId === server.id}
81
- className={cn(
82
- 'w-full flex items-center gap-3 px-4 py-2.5 text-left cursor-pointer transition-colors',
83
- 'hover:bg-surface-5',
84
- connectingId === server.id && 'opacity-60 pointer-events-none',
85
- )}
160
+ onClick={() => setShowWizard(true)}
161
+ className="flex items-center gap-1.5 text-2xs text-accent hover:text-accent/80 font-sans font-medium cursor-pointer transition-colors"
86
162
  >
87
- <Server size={15} className={server.active ? 'text-success' : 'text-text-4'} />
88
- <div className="flex-1 min-w-0">
89
- <div className="flex items-center gap-2">
90
- <span className="text-sm font-medium text-text-0 font-sans truncate">{server.name}</span>
91
- {server.active && <StatusDot status="running" size="sm" />}
92
- </div>
93
- <span className="text-2xs text-text-4 font-mono">{server.user}@{server.host}</span>
94
- </div>
95
- <div className="flex-shrink-0">
96
- {connectingId === server.id ? (
97
- <Loader2 size={14} className="text-text-3 animate-spin" />
98
- ) : server.active ? (
99
- <span className="flex items-center gap-1 text-2xs text-success font-sans">
100
- <ExternalLink size={11} /> Open
101
- </span>
102
- ) : (
103
- <span className="text-2xs text-text-3 font-sans">Connect</span>
104
- )}
105
- </div>
163
+ <Plus size={10} /> Add new connection
106
164
  </button>
107
- ))
108
- )}
109
- </div>
110
-
111
- {/* Footer */}
112
- {savedTunnels.length > 0 && (
113
- <div className="px-4 py-2 border-t border-border-subtle">
114
- <button
115
- onClick={() => {
116
- toggle();
117
- useGrooveStore.getState().setActiveView('settings');
118
- }}
119
- className="flex items-center gap-1.5 text-2xs text-text-4 hover:text-text-2 font-sans cursor-pointer transition-colors"
120
- >
121
- <Plus size={10} /> Manage servers in Settings
122
- </button>
123
- </div>
165
+ </div>
166
+ </>
124
167
  )}
125
168
  </motion.div>
126
169
  </AnimatePresence>
@@ -111,10 +111,10 @@ export function RemoteServerCard({ server, onEdit, onDelete, onConnect, onDiscon
111
111
  {server.user}@{server.host}:{server.port || 22}
112
112
  </div>
113
113
 
114
- {/* SSH key path */}
115
- {server.sshKeyPath && (
114
+ {/* SSH key */}
115
+ {server.sshKeyDisplay && (
116
116
  <div className="text-2xs text-text-4 font-mono truncate mb-2">
117
- Key: {server.sshKeyPath}
117
+ Key: {server.sshKeyDisplay}
118
118
  </div>
119
119
  )}
120
120