@assistkick/create 1.2.0 → 1.3.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.
- package/package.json +2 -1
- package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +231 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/kanban.ts +4 -4
- package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +49 -2
- package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +82 -0
- package/templates/assistkick-product-system/packages/backend/src/server.ts +19 -6
- package/templates/assistkick-product-system/packages/backend/src/services/github_app_service.ts +146 -0
- package/templates/assistkick-product-system/packages/backend/src/services/init.ts +69 -2
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +71 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +87 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +194 -0
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +88 -17
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +114 -39
- package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +28 -14
- package/templates/assistkick-product-system/packages/frontend/src/App.tsx +1 -1
- package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +151 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/GitRepoModal.tsx +352 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +208 -95
- package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +17 -1
- package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +238 -105
- package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +15 -13
- package/templates/assistkick-product-system/packages/frontend/src/constants/graph.ts +1 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +4 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/dashboard.tsx +22 -4
- package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +486 -38
- package/templates/assistkick-product-system/packages/shared/db/migrations/0001_vengeful_wallop.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0002_greedy_excalibur.sql +4 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0001_snapshot.json +826 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0002_snapshot.json +854 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +14 -0
- package/templates/assistkick-product-system/packages/shared/db/schema.ts +5 -0
- package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +54 -1
- package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +25 -0
- package/templates/assistkick-product-system/packages/shared/lib/pipeline-state-store.ts +4 -0
- package/templates/assistkick-product-system/packages/shared/lib/pipeline.ts +329 -89
- package/templates/assistkick-product-system/packages/shared/lib/pipeline_orchestrator.ts +186 -0
- package/templates/assistkick-product-system/packages/shared/tools/db_explorer.ts +275 -0
- package/templates/assistkick-product-system/packages/shared/tools/get_kanban.ts +2 -1
- package/templates/assistkick-product-system/packages/shared/tools/move_card.ts +3 -2
- package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -2
- package/templates/assistkick-product-system/tests/kanban.test.ts +1 -1
- package/templates/assistkick-product-system/tests/pipeline_stats_all_cards.test.ts +1 -1
- package/templates/assistkick-product-system/tests/web_terminal.test.ts +189 -150
- package/templates/skills/assistkick-bootstrap/SKILL.md +33 -25
- package/templates/skills/assistkick-code-reviewer/SKILL.md +23 -15
- package/templates/skills/assistkick-db-explorer/SKILL.md +86 -0
- package/templates/skills/assistkick-debugger/SKILL.md +30 -22
- package/templates/skills/assistkick-developer/SKILL.md +37 -29
- package/templates/skills/assistkick-interview/SKILL.md +34 -26
package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Terminal view
|
|
3
|
-
*
|
|
2
|
+
* Terminal view — multi-session manager with chat-like layout.
|
|
3
|
+
* Left sidebar lists all terminal sessions; right panel shows the active session's terminal.
|
|
4
|
+
* Each session is permanently bound to a project chosen at creation time.
|
|
5
|
+
* Admin-only access.
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
import React, { useEffect, useRef, useCallback, useState } from 'react';
|
|
@@ -8,78 +10,80 @@ import { Terminal } from '@xterm/xterm';
|
|
|
8
10
|
import { FitAddon } from '@xterm/addon-fit';
|
|
9
11
|
import { WebLinksAddon } from '@xterm/addon-web-links';
|
|
10
12
|
import '@xterm/xterm/css/xterm.css';
|
|
13
|
+
import { apiClient } from '../api/client';
|
|
14
|
+
import type { Project } from '../hooks/useProjects';
|
|
11
15
|
|
|
12
16
|
interface TerminalViewProps {
|
|
13
17
|
visible: boolean;
|
|
18
|
+
projects: Project[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface SessionInfo {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
projectId: string;
|
|
25
|
+
projectName: string;
|
|
26
|
+
state: 'idle' | 'running';
|
|
27
|
+
createdAt: string;
|
|
14
28
|
}
|
|
15
29
|
|
|
16
30
|
type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'error';
|
|
17
31
|
|
|
18
|
-
|
|
32
|
+
interface ActiveConnection {
|
|
33
|
+
sessionId: string;
|
|
34
|
+
ws: WebSocket;
|
|
35
|
+
terminal: Terminal;
|
|
36
|
+
fitAddon: FitAddon;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function TerminalView({ visible, projects }: TerminalViewProps) {
|
|
19
40
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
41
|
+
const connectionRef = useRef<ActiveConnection | null>(null);
|
|
42
|
+
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
|
43
|
+
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
|
23
44
|
const [status, setStatus] = useState<ConnectionStatus>('disconnected');
|
|
24
45
|
const [errorMsg, setErrorMsg] = useState('');
|
|
46
|
+
const [newSessionProjectId, setNewSessionProjectId] = useState<string>('');
|
|
47
|
+
const [creating, setCreating] = useState(false);
|
|
25
48
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
setStatus('connecting');
|
|
33
|
-
setErrorMsg('');
|
|
34
|
-
|
|
35
|
-
const ws = new WebSocket(wsUrl);
|
|
36
|
-
wsRef.current = ws;
|
|
37
|
-
|
|
38
|
-
ws.onopen = () => {
|
|
39
|
-
setStatus('connected');
|
|
40
|
-
|
|
41
|
-
// Send initial resize
|
|
42
|
-
if (terminalRef.current && fitAddonRef.current) {
|
|
43
|
-
fitAddonRef.current.fit();
|
|
44
|
-
const { cols, rows } = terminalRef.current;
|
|
45
|
-
ws.send(JSON.stringify({ type: 'resize', cols, rows }));
|
|
46
|
-
}
|
|
47
|
-
};
|
|
49
|
+
// Default new session project picker to first project
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (projects.length > 0 && !newSessionProjectId) {
|
|
52
|
+
setNewSessionProjectId(projects[0].id);
|
|
53
|
+
}
|
|
54
|
+
}, [projects, newSessionProjectId]);
|
|
48
55
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
};
|
|
56
|
+
const fetchSessions = useCallback(async () => {
|
|
57
|
+
try {
|
|
58
|
+
const data = await apiClient.listTerminalSessions();
|
|
59
|
+
setSessions(data.sessions);
|
|
60
|
+
return data.sessions as SessionInfo[];
|
|
61
|
+
} catch {
|
|
62
|
+
return [] as SessionInfo[];
|
|
63
|
+
}
|
|
64
|
+
}, []);
|
|
59
65
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
setStatus('error');
|
|
67
|
-
setErrorMsg('Access denied. Admin privileges required.');
|
|
68
|
-
} else {
|
|
69
|
-
setStatus('disconnected');
|
|
70
|
-
}
|
|
71
|
-
};
|
|
66
|
+
// Load sessions when tab becomes visible
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (visible) {
|
|
69
|
+
fetchSessions();
|
|
70
|
+
}
|
|
71
|
+
}, [visible, fetchSessions]);
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
const disconnectCurrent = useCallback(() => {
|
|
74
|
+
const conn = connectionRef.current;
|
|
75
|
+
if (!conn) return;
|
|
76
|
+
conn.ws.close();
|
|
77
|
+
conn.terminal.dispose();
|
|
78
|
+
connectionRef.current = null;
|
|
79
|
+
setStatus('disconnected');
|
|
80
|
+
setErrorMsg('');
|
|
78
81
|
}, []);
|
|
79
82
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
+
const connectToSession = useCallback((sessionId: string) => {
|
|
84
|
+
disconnectCurrent();
|
|
85
|
+
|
|
86
|
+
if (!containerRef.current) return;
|
|
83
87
|
|
|
84
88
|
const terminal = new Terminal({
|
|
85
89
|
cursorBlink: true,
|
|
@@ -111,55 +115,129 @@ export function TerminalView({ visible }: TerminalViewProps) {
|
|
|
111
115
|
|
|
112
116
|
const fitAddon = new FitAddon();
|
|
113
117
|
const webLinksAddon = new WebLinksAddon();
|
|
114
|
-
|
|
115
118
|
terminal.loadAddon(fitAddon);
|
|
116
119
|
terminal.loadAddon(webLinksAddon);
|
|
117
120
|
terminal.open(containerRef.current);
|
|
118
|
-
|
|
119
121
|
fitAddon.fit();
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
|
|
123
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
124
|
+
const wsUrl = `${protocol}//${window.location.host}/api/terminal?sessionId=${encodeURIComponent(sessionId)}`;
|
|
125
|
+
|
|
126
|
+
setStatus('connecting');
|
|
127
|
+
setErrorMsg('');
|
|
128
|
+
|
|
129
|
+
const ws = new WebSocket(wsUrl);
|
|
130
|
+
connectionRef.current = { sessionId, ws, terminal, fitAddon };
|
|
131
|
+
|
|
132
|
+
ws.onopen = () => {
|
|
133
|
+
setStatus('connected');
|
|
134
|
+
fitAddon.fit();
|
|
135
|
+
const { cols, rows } = terminal;
|
|
136
|
+
ws.send(JSON.stringify({ type: 'resize', cols, rows }));
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
ws.onmessage = (event) => {
|
|
140
|
+
try {
|
|
141
|
+
const msg = JSON.parse(event.data);
|
|
142
|
+
if (msg.type === 'output') {
|
|
143
|
+
terminal.write(msg.data);
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// Ignore malformed messages
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
ws.onclose = (event) => {
|
|
151
|
+
if (connectionRef.current?.sessionId === sessionId) {
|
|
152
|
+
connectionRef.current = null;
|
|
153
|
+
}
|
|
154
|
+
if (event.code === 4001) {
|
|
155
|
+
setStatus('error');
|
|
156
|
+
setErrorMsg('Authentication required. Please log in.');
|
|
157
|
+
} else if (event.code === 4003) {
|
|
158
|
+
setStatus('error');
|
|
159
|
+
setErrorMsg('Access denied. Admin privileges required.');
|
|
160
|
+
} else if (event.code === 4004) {
|
|
161
|
+
setStatus('error');
|
|
162
|
+
setErrorMsg('Session not found. It may have been killed.');
|
|
163
|
+
fetchSessions();
|
|
164
|
+
} else {
|
|
165
|
+
setStatus('disconnected');
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
ws.onerror = () => {
|
|
170
|
+
setStatus('error');
|
|
171
|
+
setErrorMsg('Failed to connect to terminal session.');
|
|
172
|
+
if (connectionRef.current?.sessionId === sessionId) {
|
|
173
|
+
connectionRef.current = null;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
122
176
|
|
|
123
177
|
// Forward user input to WebSocket
|
|
124
178
|
terminal.onData((data) => {
|
|
125
|
-
|
|
126
|
-
if (ws?.readyState === WebSocket.OPEN) {
|
|
179
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
127
180
|
ws.send(JSON.stringify({ type: 'input', data }));
|
|
128
181
|
}
|
|
129
182
|
});
|
|
183
|
+
}, [disconnectCurrent, fetchSessions]);
|
|
130
184
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
};
|
|
136
|
-
}, []);
|
|
185
|
+
const handleSelectSession = useCallback((sessionId: string) => {
|
|
186
|
+
setActiveSessionId(sessionId);
|
|
187
|
+
connectToSession(sessionId);
|
|
188
|
+
}, [connectToSession]);
|
|
137
189
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
|
|
190
|
+
const handleCreateSession = useCallback(async () => {
|
|
191
|
+
const projectId = newSessionProjectId || projects[0]?.id;
|
|
192
|
+
if (!projectId) return;
|
|
193
|
+
|
|
194
|
+
const project = projects.find(p => p.id === projectId) || projects[0];
|
|
195
|
+
if (!project) return;
|
|
196
|
+
|
|
197
|
+
setCreating(true);
|
|
198
|
+
try {
|
|
199
|
+
const data = await apiClient.createTerminalSession(project.id, project.name);
|
|
200
|
+
const newSession: SessionInfo = data.session;
|
|
201
|
+
const updated = await fetchSessions();
|
|
202
|
+
const created = updated.find(s => s.id === newSession.id) || newSession;
|
|
203
|
+
setActiveSessionId(created.id);
|
|
204
|
+
connectToSession(created.id);
|
|
205
|
+
} catch {
|
|
206
|
+
// silently ignore — session list stays as-is
|
|
207
|
+
} finally {
|
|
208
|
+
setCreating(false);
|
|
142
209
|
}
|
|
143
|
-
}, [
|
|
210
|
+
}, [newSessionProjectId, projects, fetchSessions, connectToSession]);
|
|
144
211
|
|
|
145
|
-
|
|
212
|
+
const handleKillSession = useCallback(async (sessionId: string, e: React.MouseEvent) => {
|
|
213
|
+
e.stopPropagation();
|
|
214
|
+
try {
|
|
215
|
+
await apiClient.killTerminalSession(sessionId);
|
|
216
|
+
if (activeSessionId === sessionId) {
|
|
217
|
+
disconnectCurrent();
|
|
218
|
+
setActiveSessionId(null);
|
|
219
|
+
}
|
|
220
|
+
await fetchSessions();
|
|
221
|
+
} catch {
|
|
222
|
+
// silently ignore
|
|
223
|
+
}
|
|
224
|
+
}, [activeSessionId, disconnectCurrent, fetchSessions]);
|
|
225
|
+
|
|
226
|
+
// Handle window resize — refit when visible
|
|
146
227
|
useEffect(() => {
|
|
147
228
|
if (!visible) return;
|
|
148
229
|
|
|
149
230
|
const handleResize = () => {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
231
|
+
const conn = connectionRef.current;
|
|
232
|
+
if (!conn) return;
|
|
233
|
+
conn.fitAddon.fit();
|
|
234
|
+
const { cols, rows } = conn.terminal;
|
|
235
|
+
if (conn.ws.readyState === WebSocket.OPEN) {
|
|
236
|
+
conn.ws.send(JSON.stringify({ type: 'resize', cols, rows }));
|
|
157
237
|
}
|
|
158
238
|
};
|
|
159
239
|
|
|
160
|
-
// Fit on visibility change
|
|
161
240
|
handleResize();
|
|
162
|
-
|
|
163
241
|
window.addEventListener('resize', handleResize);
|
|
164
242
|
return () => window.removeEventListener('resize', handleResize);
|
|
165
243
|
}, [visible]);
|
|
@@ -167,34 +245,89 @@ export function TerminalView({ visible }: TerminalViewProps) {
|
|
|
167
245
|
// Clean up WebSocket on unmount
|
|
168
246
|
useEffect(() => {
|
|
169
247
|
return () => {
|
|
170
|
-
|
|
171
|
-
wsRef.current = null;
|
|
248
|
+
disconnectCurrent();
|
|
172
249
|
};
|
|
173
|
-
}, []);
|
|
250
|
+
}, [disconnectCurrent]);
|
|
251
|
+
|
|
252
|
+
const activeSession = sessions.find(s => s.id === activeSessionId);
|
|
174
253
|
|
|
175
254
|
return (
|
|
176
255
|
<div className="terminal-view" style={{ display: visible ? 'flex' : 'none' }}>
|
|
177
|
-
{
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
</button>
|
|
256
|
+
{/* Left sidebar — session list */}
|
|
257
|
+
<div className="terminal-sidebar">
|
|
258
|
+
<div className="terminal-sidebar-header">Sessions</div>
|
|
259
|
+
<div className="terminal-session-list">
|
|
260
|
+
{sessions.length === 0 && (
|
|
261
|
+
<div className="terminal-no-sessions">No sessions yet</div>
|
|
184
262
|
)}
|
|
263
|
+
{sessions.map(session => (
|
|
264
|
+
<div
|
|
265
|
+
key={session.id}
|
|
266
|
+
className={`terminal-session-item${session.id === activeSessionId ? ' terminal-session-item--active' : ''}`}
|
|
267
|
+
onClick={() => handleSelectSession(session.id)}
|
|
268
|
+
>
|
|
269
|
+
<div className="terminal-session-name">{session.name}</div>
|
|
270
|
+
<div className="terminal-session-project">{session.projectName}</div>
|
|
271
|
+
<button
|
|
272
|
+
className="terminal-session-kill"
|
|
273
|
+
onClick={(e) => handleKillSession(session.id, e)}
|
|
274
|
+
title="Kill session"
|
|
275
|
+
>
|
|
276
|
+
✕
|
|
277
|
+
</button>
|
|
278
|
+
</div>
|
|
279
|
+
))}
|
|
185
280
|
</div>
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
<div className="terminal-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
281
|
+
|
|
282
|
+
{/* New session controls */}
|
|
283
|
+
<div className="terminal-new-session">
|
|
284
|
+
<select
|
|
285
|
+
className="terminal-project-picker"
|
|
286
|
+
value={newSessionProjectId}
|
|
287
|
+
onChange={e => setNewSessionProjectId(e.target.value)}
|
|
288
|
+
disabled={creating || projects.length === 0}
|
|
289
|
+
>
|
|
290
|
+
{projects.map(p => (
|
|
291
|
+
<option key={p.id} value={p.id}>{p.name}</option>
|
|
292
|
+
))}
|
|
293
|
+
</select>
|
|
294
|
+
<button
|
|
295
|
+
className="terminal-new-session-btn"
|
|
296
|
+
onClick={handleCreateSession}
|
|
297
|
+
disabled={creating || projects.length === 0}
|
|
298
|
+
>
|
|
299
|
+
{creating ? '…' : '+ New'}
|
|
195
300
|
</button>
|
|
196
301
|
</div>
|
|
197
|
-
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
{/* Right panel — terminal content */}
|
|
305
|
+
<div className="terminal-panel">
|
|
306
|
+
{!activeSessionId && (
|
|
307
|
+
<div className="terminal-empty-state">
|
|
308
|
+
Select a session or create a new one
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
{activeSession && errorMsg && (
|
|
312
|
+
<div className="terminal-error">
|
|
313
|
+
<span>{errorMsg}</span>
|
|
314
|
+
<button
|
|
315
|
+
className="terminal-reconnect-btn"
|
|
316
|
+
onClick={() => connectToSession(activeSessionId!)}
|
|
317
|
+
>
|
|
318
|
+
Reconnect
|
|
319
|
+
</button>
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
322
|
+
{activeSession && status === 'connecting' && (
|
|
323
|
+
<div className="terminal-status">Connecting…</div>
|
|
324
|
+
)}
|
|
325
|
+
<div
|
|
326
|
+
ref={containerRef}
|
|
327
|
+
className="terminal-container"
|
|
328
|
+
style={{ display: activeSessionId ? 'flex' : 'none' }}
|
|
329
|
+
/>
|
|
330
|
+
</div>
|
|
198
331
|
</div>
|
|
199
332
|
);
|
|
200
333
|
}
|
|
@@ -19,32 +19,24 @@ interface ToolbarProps {
|
|
|
19
19
|
onProjectCreate: (name: string) => Promise<any>;
|
|
20
20
|
onProjectRename: (id: string, name: string) => Promise<void>;
|
|
21
21
|
onProjectArchive: (id: string) => Promise<void>;
|
|
22
|
+
onOpenGitModal?: (project: Project) => void;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export function Toolbar({
|
|
25
26
|
activeTab, onTabChange, completeness, onFit,
|
|
26
27
|
onSettingsToggle, settingsOpen, theme, onThemeToggle, isAdmin, onLogout,
|
|
27
|
-
projects, selectedProjectId, onProjectSelect, onProjectCreate, onProjectRename, onProjectArchive,
|
|
28
|
+
projects, selectedProjectId, onProjectSelect, onProjectCreate, onProjectRename, onProjectArchive, onOpenGitModal,
|
|
28
29
|
}: ToolbarProps) {
|
|
29
|
-
const tabs = ['
|
|
30
|
+
const tabs = ['kanban', 'terminal', 'graph', 'coherence'];
|
|
30
31
|
if (isAdmin) {
|
|
31
32
|
tabs.push('users');
|
|
32
|
-
tabs.push('terminal');
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
const tabLabels: Record<string, string> = { terminal: 'Chat' };
|
|
36
|
+
|
|
35
37
|
return (
|
|
36
38
|
<div className="toolbar">
|
|
37
39
|
<div className="tab-bar">
|
|
38
|
-
{tabs.map(tab => (
|
|
39
|
-
<button
|
|
40
|
-
key={tab}
|
|
41
|
-
className={`tab-btn${activeTab === tab ? ' active' : ''}`}
|
|
42
|
-
onClick={() => onTabChange(tab)}
|
|
43
|
-
>
|
|
44
|
-
{tab.charAt(0).toUpperCase() + tab.slice(1)}
|
|
45
|
-
</button>
|
|
46
|
-
))}
|
|
47
|
-
<div className="tab-bar-separator" />
|
|
48
40
|
<ProjectSelector
|
|
49
41
|
projects={projects}
|
|
50
42
|
selectedProjectId={selectedProjectId}
|
|
@@ -52,7 +44,17 @@ export function Toolbar({
|
|
|
52
44
|
onCreate={onProjectCreate}
|
|
53
45
|
onRename={onProjectRename}
|
|
54
46
|
onArchive={onProjectArchive}
|
|
47
|
+
onOpenGitModal={onOpenGitModal}
|
|
55
48
|
/>
|
|
49
|
+
{tabs.map(tab => (
|
|
50
|
+
<button
|
|
51
|
+
key={tab}
|
|
52
|
+
className={`tab-btn${activeTab === tab ? ' active' : ''}`}
|
|
53
|
+
onClick={() => onTabChange(tab)}
|
|
54
|
+
>
|
|
55
|
+
{tabLabels[tab] || tab.charAt(0).toUpperCase() + tab.slice(1)}
|
|
56
|
+
</button>
|
|
57
|
+
))}
|
|
56
58
|
</div>
|
|
57
59
|
|
|
58
60
|
<div className="toolbar-spacer" />
|
|
@@ -8,6 +8,10 @@ export interface Project {
|
|
|
8
8
|
name: string;
|
|
9
9
|
isDefault: number;
|
|
10
10
|
archivedAt: string | null;
|
|
11
|
+
repoUrl: string | null;
|
|
12
|
+
githubInstallationId: string | null;
|
|
13
|
+
githubRepoFullName: string | null;
|
|
14
|
+
baseBranch: string | null;
|
|
11
15
|
createdAt: string;
|
|
12
16
|
updatedAt: string;
|
|
13
17
|
}
|
|
@@ -11,6 +11,7 @@ import { TerminalView } from '../components/TerminalView';
|
|
|
11
11
|
import { UsersView } from '../components/UsersView';
|
|
12
12
|
import { SidePanel, openSidePanel, closeSidePanel } from '../components/SidePanel';
|
|
13
13
|
import { QaIssueSheet } from '../components/QaIssueSheet';
|
|
14
|
+
import { GitRepoModal } from '../components/GitRepoModal';
|
|
14
15
|
import { useTheme } from '../hooks/useTheme';
|
|
15
16
|
import { useGraph } from '../hooks/useGraph';
|
|
16
17
|
import { useAuth } from '../hooks/useAuth';
|
|
@@ -22,7 +23,7 @@ const VALID_TABS = new Set(['graph', 'kanban', 'coherence', 'users', 'terminal']
|
|
|
22
23
|
|
|
23
24
|
function tabFromPath(pathname: string): string {
|
|
24
25
|
const seg = pathname.replace(/^\//, '');
|
|
25
|
-
return VALID_TABS.has(seg) ? seg : '
|
|
26
|
+
return VALID_TABS.has(seg) ? seg : 'kanban';
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export function DashboardRoute() {
|
|
@@ -33,7 +34,7 @@ export function DashboardRoute() {
|
|
|
33
34
|
const isAdmin = user?.role === 'admin';
|
|
34
35
|
const {
|
|
35
36
|
projects, selectedProjectId, selectProject,
|
|
36
|
-
createProject, renameProject, archiveProject,
|
|
37
|
+
createProject, renameProject, archiveProject, refetchProjects,
|
|
37
38
|
} = useProjects();
|
|
38
39
|
const { graphData, error: graphError } = useGraph(selectedProjectId);
|
|
39
40
|
const graphRef = useRef<GraphViewHandle>(null);
|
|
@@ -42,6 +43,13 @@ export function DashboardRoute() {
|
|
|
42
43
|
const [legendVisible, setLegendVisible] = useState(true);
|
|
43
44
|
const [hiddenTypes, setHiddenTypes] = useState<Set<string>>(new Set());
|
|
44
45
|
|
|
46
|
+
// Git Repo Modal state
|
|
47
|
+
const [gitModalProject, setGitModalProject] = useState<any>(null);
|
|
48
|
+
|
|
49
|
+
const handleOpenGitModal = useCallback((project: any) => {
|
|
50
|
+
setGitModalProject(project);
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
45
53
|
// QA Issue Sheet state
|
|
46
54
|
const [qaOpen, setQaOpen] = useState(false);
|
|
47
55
|
const [qaFeatureId, setQaFeatureId] = useState<string | null>(null);
|
|
@@ -146,6 +154,7 @@ export function DashboardRoute() {
|
|
|
146
154
|
onProjectCreate={createProject}
|
|
147
155
|
onProjectRename={renameProject}
|
|
148
156
|
onProjectArchive={archiveProject}
|
|
157
|
+
onOpenGitModal={handleOpenGitModal}
|
|
149
158
|
/>
|
|
150
159
|
|
|
151
160
|
<div className="main-content">
|
|
@@ -196,8 +205,8 @@ export function DashboardRoute() {
|
|
|
196
205
|
{/* Users View (admin-only) */}
|
|
197
206
|
{isAdmin && <UsersView visible={activeTab === 'users'} />}
|
|
198
207
|
|
|
199
|
-
{/*
|
|
200
|
-
|
|
208
|
+
{/* Chat View */}
|
|
209
|
+
<TerminalView visible={activeTab === 'terminal'} projects={projects} />
|
|
201
210
|
|
|
202
211
|
{/* Side Panel */}
|
|
203
212
|
<SidePanel graphData={graphData} onEdgeClick={handleEdgeClick} />
|
|
@@ -215,6 +224,15 @@ export function DashboardRoute() {
|
|
|
215
224
|
/>
|
|
216
225
|
</div>
|
|
217
226
|
|
|
227
|
+
{/* Git Repo Modal */}
|
|
228
|
+
{gitModalProject && (
|
|
229
|
+
<GitRepoModal
|
|
230
|
+
project={gitModalProject}
|
|
231
|
+
onClose={() => setGitModalProject(null)}
|
|
232
|
+
onProjectUpdated={refetchProjects}
|
|
233
|
+
/>
|
|
234
|
+
)}
|
|
235
|
+
|
|
218
236
|
</div>
|
|
219
237
|
</ToastProvider>
|
|
220
238
|
);
|