groove-dev 0.27.30 → 0.27.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/cli/src/commands/start.js +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +32 -2
- package/node_modules/@groove-dev/daemon/src/firstrun.js +1 -0
- package/node_modules/@groove-dev/daemon/src/index.js +14 -0
- package/node_modules/@groove-dev/daemon/src/journalist.js +16 -4
- package/node_modules/@groove-dev/daemon/src/memory.js +6 -1
- package/node_modules/@groove-dev/daemon/src/process.js +44 -28
- package/node_modules/@groove-dev/daemon/src/providers/local.js +4 -2
- package/node_modules/@groove-dev/daemon/src/providers/ollama.js +35 -3
- package/node_modules/@groove-dev/daemon/src/rotator.js +1 -0
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +19 -7
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-BwNjgBny.css → index-BnLiWvrh.css} +1 -1
- package/node_modules/@groove-dev/gui/dist/assets/{index-PxWmJjcJ.js → index-BoU6IhQI.js} +1635 -1635
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +16 -1
- package/node_modules/@groove-dev/gui/src/components/layout/project-picker.jsx +127 -0
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +22 -15
- package/node_modules/@groove-dev/gui/src/stores/groove.js +39 -2
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/commands/start.js +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +32 -2
- package/packages/daemon/src/firstrun.js +1 -0
- package/packages/daemon/src/index.js +14 -0
- package/packages/daemon/src/journalist.js +16 -4
- package/packages/daemon/src/memory.js +6 -1
- package/packages/daemon/src/process.js +44 -28
- package/packages/daemon/src/providers/local.js +4 -2
- package/packages/daemon/src/providers/ollama.js +35 -3
- package/packages/daemon/src/rotator.js +1 -0
- package/packages/daemon/src/tunnel-manager.js +19 -7
- package/packages/gui/dist/assets/{index-BwNjgBny.css → index-BnLiWvrh.css} +1 -1
- package/packages/gui/dist/assets/{index-PxWmJjcJ.js → index-BoU6IhQI.js} +1635 -1635
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/layout/app-shell.jsx +2 -0
- package/packages/gui/src/components/layout/breadcrumb-bar.jsx +16 -1
- package/packages/gui/src/components/layout/project-picker.jsx +127 -0
- package/packages/gui/src/components/settings/quick-connect.jsx +22 -15
- package/packages/gui/src/stores/groove.js +39 -2
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
8
8
|
<title>Groove GUI</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-BoU6IhQI.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BnLiWvrh.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -13,6 +13,7 @@ import { DetailPanel } from './detail-panel';
|
|
|
13
13
|
import { CommandPalette } from './command-palette';
|
|
14
14
|
import { ApprovalModal } from '../ui/approval-modal';
|
|
15
15
|
import { QuickConnect } from '../settings/quick-connect';
|
|
16
|
+
import { ProjectPicker } from './project-picker';
|
|
16
17
|
import { TeamTabBar } from '../../views/agents';
|
|
17
18
|
|
|
18
19
|
export function AppShell({ children, detailContent, terminalContent }) {
|
|
@@ -117,6 +118,7 @@ export function AppShell({ children, detailContent, terminalContent }) {
|
|
|
117
118
|
|
|
118
119
|
<CommandPalette />
|
|
119
120
|
<QuickConnect />
|
|
121
|
+
<ProjectPicker />
|
|
120
122
|
<ApprovalModal />
|
|
121
123
|
<ToastContainer />
|
|
122
124
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { useState, useEffect, useRef } from 'react';
|
|
3
|
-
import { Search, ChevronRight, LogIn, LogOut, User, ExternalLink, BookOpen, ChevronDown } from 'lucide-react';
|
|
3
|
+
import { Search, ChevronRight, LogIn, LogOut, User, ExternalLink, BookOpen, ChevronDown, FolderOpen } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../lib/cn';
|
|
5
5
|
import { useGrooveStore } from '../../stores/groove';
|
|
6
6
|
import { isElectron, getPlatform } from '../../lib/electron';
|
|
@@ -130,10 +130,13 @@ const VIEW_LABELS = {
|
|
|
130
130
|
export function BreadcrumbBar({
|
|
131
131
|
activeView,
|
|
132
132
|
connected,
|
|
133
|
+
tunneled,
|
|
133
134
|
daemonHost,
|
|
134
135
|
editorActiveFile,
|
|
135
136
|
onOpenCommandPalette,
|
|
136
137
|
}) {
|
|
138
|
+
const projectDir = useGrooveStore((s) => s.projectDir);
|
|
139
|
+
const toggleProjectPicker = useGrooveStore((s) => s.toggleProjectPicker);
|
|
137
140
|
const crumbs = ['Groove', VIEW_LABELS[activeView] || activeView];
|
|
138
141
|
if (activeView === 'editor' && editorActiveFile) {
|
|
139
142
|
crumbs.push(editorActiveFile.split('/').pop());
|
|
@@ -182,6 +185,18 @@ export function BreadcrumbBar({
|
|
|
182
185
|
</span>
|
|
183
186
|
)}
|
|
184
187
|
|
|
188
|
+
{/* Project dir badge — remote sessions, clickable to change */}
|
|
189
|
+
{tunneled && projectDir && (
|
|
190
|
+
<button
|
|
191
|
+
onClick={toggleProjectPicker}
|
|
192
|
+
className="flex items-center gap-1 text-2xs font-mono font-medium text-text-2 bg-surface-5 px-1.5 py-0.5 rounded flex-shrink-0 hover:bg-surface-4 hover:text-text-0 transition-colors cursor-pointer"
|
|
193
|
+
title={projectDir}
|
|
194
|
+
>
|
|
195
|
+
<FolderOpen size={11} />
|
|
196
|
+
{projectDir.split('/').pop() || '/'}
|
|
197
|
+
</button>
|
|
198
|
+
)}
|
|
199
|
+
|
|
185
200
|
<div className="flex-1 min-w-4" />
|
|
186
201
|
|
|
187
202
|
{/* Breadcrumbs */}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
4
|
+
import { FolderBrowser } from '../agents/folder-browser';
|
|
5
|
+
import { cn } from '../../lib/cn';
|
|
6
|
+
import {
|
|
7
|
+
FolderOpen, FolderClosed, Clock, ChevronRight, Plus, Monitor,
|
|
8
|
+
} from 'lucide-react';
|
|
9
|
+
|
|
10
|
+
function formatTimeAgo(iso) {
|
|
11
|
+
if (!iso) return '';
|
|
12
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
13
|
+
const mins = Math.floor(diff / 60000);
|
|
14
|
+
if (mins < 1) return 'just now';
|
|
15
|
+
if (mins < 60) return `${mins}m ago`;
|
|
16
|
+
const hours = Math.floor(mins / 60);
|
|
17
|
+
if (hours < 24) return `${hours}h ago`;
|
|
18
|
+
const days = Math.floor(hours / 24);
|
|
19
|
+
return `${days}d ago`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ProjectPicker() {
|
|
23
|
+
const show = useGrooveStore((s) => s.showProjectPicker);
|
|
24
|
+
const recentProjects = useGrooveStore((s) => s.recentProjects);
|
|
25
|
+
const setProjectDir = useGrooveStore((s) => s.setProjectDir);
|
|
26
|
+
const [browserOpen, setBrowserOpen] = useState(false);
|
|
27
|
+
const [loading, setLoading] = useState(null);
|
|
28
|
+
|
|
29
|
+
if (!show) return null;
|
|
30
|
+
|
|
31
|
+
async function handleSelect(path) {
|
|
32
|
+
setLoading(path);
|
|
33
|
+
try {
|
|
34
|
+
await setProjectDir(path);
|
|
35
|
+
} catch {
|
|
36
|
+
setLoading(null);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function handleBrowseSelect(path) {
|
|
41
|
+
setBrowserOpen(false);
|
|
42
|
+
await handleSelect(path);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-surface-0/90 backdrop-blur-sm">
|
|
47
|
+
<div className="w-full max-w-lg mx-4">
|
|
48
|
+
{/* Header */}
|
|
49
|
+
<div className="text-center mb-8">
|
|
50
|
+
<div className="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-accent/10 mb-4">
|
|
51
|
+
<Monitor size={28} className="text-accent" />
|
|
52
|
+
</div>
|
|
53
|
+
<h1 className="text-xl font-semibold text-text-0 mb-1">Open a project</h1>
|
|
54
|
+
<p className="text-sm text-text-3">Select a working directory for this session</p>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
{/* Recent projects */}
|
|
58
|
+
{recentProjects.length > 0 && (
|
|
59
|
+
<div className="mb-4">
|
|
60
|
+
<div className="flex items-center gap-2 mb-2 px-1">
|
|
61
|
+
<Clock size={13} className="text-text-4" />
|
|
62
|
+
<span className="text-xs font-medium text-text-3 uppercase tracking-wider">Recent</span>
|
|
63
|
+
</div>
|
|
64
|
+
<div className="bg-surface-2 rounded-xl border border-border overflow-hidden">
|
|
65
|
+
{recentProjects.map((project, i) => (
|
|
66
|
+
<button
|
|
67
|
+
key={project.path}
|
|
68
|
+
onClick={() => handleSelect(project.path)}
|
|
69
|
+
disabled={loading !== null}
|
|
70
|
+
className={cn(
|
|
71
|
+
'w-full flex items-center gap-3 px-4 py-3 text-left cursor-pointer',
|
|
72
|
+
'hover:bg-surface-4 transition-colors',
|
|
73
|
+
'disabled:opacity-50 disabled:cursor-wait',
|
|
74
|
+
i < recentProjects.length - 1 && 'border-b border-border-subtle',
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
<FolderClosed size={18} className="text-warning flex-shrink-0" />
|
|
78
|
+
<div className="flex-1 min-w-0">
|
|
79
|
+
<div className="text-sm font-medium text-text-0 truncate">{project.name}</div>
|
|
80
|
+
<div className="text-xs text-text-3 font-mono truncate">{project.path}</div>
|
|
81
|
+
</div>
|
|
82
|
+
<div className="flex items-center gap-2 flex-shrink-0">
|
|
83
|
+
{project.openedAt && (
|
|
84
|
+
<span className="text-[11px] text-text-4">{formatTimeAgo(project.openedAt)}</span>
|
|
85
|
+
)}
|
|
86
|
+
{loading === project.path ? (
|
|
87
|
+
<div className="w-4 h-4 border-2 border-accent border-t-transparent rounded-full animate-spin" />
|
|
88
|
+
) : (
|
|
89
|
+
<ChevronRight size={14} className="text-text-4" />
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
</button>
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
|
|
98
|
+
{/* Open folder button */}
|
|
99
|
+
<button
|
|
100
|
+
onClick={() => setBrowserOpen(true)}
|
|
101
|
+
disabled={loading !== null}
|
|
102
|
+
className={cn(
|
|
103
|
+
'w-full flex items-center gap-3 px-4 py-3.5 rounded-xl cursor-pointer',
|
|
104
|
+
'bg-surface-2 border border-border border-dashed',
|
|
105
|
+
'hover:bg-surface-4 hover:border-accent/30 transition-colors',
|
|
106
|
+
'disabled:opacity-50 disabled:cursor-wait',
|
|
107
|
+
)}
|
|
108
|
+
>
|
|
109
|
+
<div className="flex items-center justify-center w-9 h-9 rounded-lg bg-accent/10">
|
|
110
|
+
<Plus size={18} className="text-accent" />
|
|
111
|
+
</div>
|
|
112
|
+
<div className="text-left">
|
|
113
|
+
<div className="text-sm font-medium text-text-0">Open Folder</div>
|
|
114
|
+
<div className="text-xs text-text-3">Browse the filesystem</div>
|
|
115
|
+
</div>
|
|
116
|
+
</button>
|
|
117
|
+
|
|
118
|
+
<FolderBrowser
|
|
119
|
+
open={browserOpen}
|
|
120
|
+
onOpenChange={setBrowserOpen}
|
|
121
|
+
currentPath="/home"
|
|
122
|
+
onSelect={handleBrowseSelect}
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useState, useRef } from 'react';
|
|
3
3
|
import { useGrooveStore } from '../../stores/groove';
|
|
4
4
|
import { cn } from '../../lib/cn';
|
|
5
5
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
@@ -17,6 +17,7 @@ export function QuickConnect() {
|
|
|
17
17
|
const addToast = useGrooveStore((s) => s.addToast);
|
|
18
18
|
const [connectingId, setConnectingId] = useState(null);
|
|
19
19
|
const [showWizard, setShowWizard] = useState(false);
|
|
20
|
+
const wizardTunnelId = useRef(null);
|
|
20
21
|
|
|
21
22
|
if (!open) return null;
|
|
22
23
|
|
|
@@ -25,7 +26,9 @@ export function QuickConnect() {
|
|
|
25
26
|
try {
|
|
26
27
|
await useGrooveStore.getState().connectTunnel(id);
|
|
27
28
|
toggle();
|
|
28
|
-
} catch {
|
|
29
|
+
} catch (err) {
|
|
30
|
+
addToast('error', 'Connection failed', err?.message || 'Unknown error');
|
|
31
|
+
}
|
|
29
32
|
setConnectingId(null);
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -81,24 +84,28 @@ export function QuickConnect() {
|
|
|
81
84
|
<SSHWizard
|
|
82
85
|
server={null}
|
|
83
86
|
onSave={async (data) => {
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
const existingId = data.id || wizardTunnelId.current;
|
|
88
|
+
if (existingId) {
|
|
89
|
+
await useGrooveStore.getState().updateTunnel(existingId, data);
|
|
90
|
+
addToast('success', 'Server updated');
|
|
86
91
|
} else {
|
|
87
|
-
await useGrooveStore.getState().saveTunnel(data);
|
|
92
|
+
const result = await useGrooveStore.getState().saveTunnel(data);
|
|
93
|
+
if (result?.id) wizardTunnelId.current = result.id;
|
|
94
|
+
addToast('success', 'Server added');
|
|
88
95
|
}
|
|
89
|
-
addToast('success', data.id ? 'Server updated' : 'Server added');
|
|
90
96
|
}}
|
|
91
97
|
onTest={() => {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
if (last?.id) return useGrooveStore.getState().testTunnel(last.id);
|
|
98
|
+
const id = wizardTunnelId.current;
|
|
99
|
+
if (id) return useGrooveStore.getState().testTunnel(id);
|
|
95
100
|
}}
|
|
96
101
|
onConnect={() => {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
const id = wizardTunnelId.current;
|
|
103
|
+
if (id) return useGrooveStore.getState().connectTunnel(id);
|
|
104
|
+
}}
|
|
105
|
+
onCancel={() => {
|
|
106
|
+
wizardTunnelId.current = null;
|
|
107
|
+
setShowWizard(false);
|
|
100
108
|
}}
|
|
101
|
-
onCancel={() => setShowWizard(false)}
|
|
102
109
|
/>
|
|
103
110
|
) : (
|
|
104
111
|
<>
|
|
@@ -112,7 +119,7 @@ export function QuickConnect() {
|
|
|
112
119
|
<Button
|
|
113
120
|
variant="primary"
|
|
114
121
|
size="sm"
|
|
115
|
-
onClick={() => setShowWizard(true)}
|
|
122
|
+
onClick={() => { wizardTunnelId.current = null; setShowWizard(true); }}
|
|
116
123
|
className="h-8 text-xs gap-1.5 mt-3"
|
|
117
124
|
>
|
|
118
125
|
<Plus size={12} /> Add Connection
|
|
@@ -157,7 +164,7 @@ export function QuickConnect() {
|
|
|
157
164
|
{/* Footer with Add button */}
|
|
158
165
|
<div className="px-4 py-2.5 border-t border-border-subtle">
|
|
159
166
|
<button
|
|
160
|
-
onClick={() => setShowWizard(true)}
|
|
167
|
+
onClick={() => { wizardTunnelId.current = null; setShowWizard(true); }}
|
|
161
168
|
className="flex items-center gap-1.5 text-2xs text-accent hover:text-accent/80 font-sans font-medium cursor-pointer transition-colors"
|
|
162
169
|
>
|
|
163
170
|
<Plus size={10} /> Add new connection
|
|
@@ -107,6 +107,11 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
107
107
|
// ── Toasts ────────────────────────────────────────────────
|
|
108
108
|
toasts: [],
|
|
109
109
|
|
|
110
|
+
// ── Project Directory ───────────────────────────────────────
|
|
111
|
+
projectDir: null,
|
|
112
|
+
recentProjects: [],
|
|
113
|
+
showProjectPicker: false,
|
|
114
|
+
|
|
110
115
|
// ── Tunnels ────────────────────────────────────────────────
|
|
111
116
|
savedTunnels: [],
|
|
112
117
|
activeTunnelId: null,
|
|
@@ -140,9 +145,11 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
140
145
|
const updates = {};
|
|
141
146
|
if (s.host && s.host !== '127.0.0.1') updates.daemonHost = s.host;
|
|
142
147
|
const browserPort = window.location.port || '80';
|
|
143
|
-
|
|
148
|
+
const isTunneled = String(s.port) !== browserPort;
|
|
149
|
+
if (isTunneled) updates.tunneled = true;
|
|
144
150
|
if (s.version) updates.version = s.version;
|
|
145
151
|
if (Object.keys(updates).length > 0) set(updates);
|
|
152
|
+
if (isTunneled) get().fetchProjectDir();
|
|
146
153
|
}).catch(() => {});
|
|
147
154
|
get().fetchTeams();
|
|
148
155
|
get().fetchApprovals();
|
|
@@ -502,6 +509,10 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
502
509
|
}));
|
|
503
510
|
break;
|
|
504
511
|
|
|
512
|
+
case 'project-dir:changed':
|
|
513
|
+
set({ projectDir: msg.data?.projectDir, showProjectPicker: false });
|
|
514
|
+
break;
|
|
515
|
+
|
|
505
516
|
case 'tunnel.connected':
|
|
506
517
|
set({ activeTunnelId: msg.data?.id || null });
|
|
507
518
|
get().fetchTunnels();
|
|
@@ -1034,6 +1045,33 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1034
1045
|
get().fetchImportedRepos();
|
|
1035
1046
|
},
|
|
1036
1047
|
|
|
1048
|
+
// ── Project Directory ────────────────────────────────────
|
|
1049
|
+
|
|
1050
|
+
async fetchProjectDir() {
|
|
1051
|
+
try {
|
|
1052
|
+
const data = await api.get('/project-dir');
|
|
1053
|
+
const isHome = /^\/home\/[^/]+$/.test(data.projectDir) || data.projectDir === '/root';
|
|
1054
|
+
set({
|
|
1055
|
+
projectDir: data.projectDir,
|
|
1056
|
+
recentProjects: data.recentProjects || [],
|
|
1057
|
+
showProjectPicker: isHome || (data.recentProjects || []).length === 0,
|
|
1058
|
+
});
|
|
1059
|
+
} catch {}
|
|
1060
|
+
},
|
|
1061
|
+
|
|
1062
|
+
async setProjectDir(path) {
|
|
1063
|
+
const data = await api.post('/project-dir', { path });
|
|
1064
|
+
set({
|
|
1065
|
+
projectDir: data.projectDir,
|
|
1066
|
+
recentProjects: data.recentProjects || [],
|
|
1067
|
+
showProjectPicker: false,
|
|
1068
|
+
});
|
|
1069
|
+
},
|
|
1070
|
+
|
|
1071
|
+
toggleProjectPicker() {
|
|
1072
|
+
set((s) => ({ showProjectPicker: !s.showProjectPicker }));
|
|
1073
|
+
},
|
|
1074
|
+
|
|
1037
1075
|
// ── Tunnels ──────────────────────────────────────────────
|
|
1038
1076
|
|
|
1039
1077
|
async fetchTunnels() {
|
|
@@ -1068,7 +1106,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1068
1106
|
const result = await api.post(`/tunnels/${encodeURIComponent(id)}/connect`);
|
|
1069
1107
|
set({ activeTunnelId: id });
|
|
1070
1108
|
get().fetchTunnels();
|
|
1071
|
-
if (result.url) window.open(result.url, '_blank');
|
|
1072
1109
|
return result;
|
|
1073
1110
|
},
|
|
1074
1111
|
|