@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
|
@@ -26,6 +26,44 @@ interface CardData {
|
|
|
26
26
|
|
|
27
27
|
const ALL_COLUMNS = COLUMNS.map(c => c.id);
|
|
28
28
|
|
|
29
|
+
const STAGE_TABS = [
|
|
30
|
+
{ key: 'in_progress', label: 'Dev' },
|
|
31
|
+
{ key: 'in_review', label: 'Review' },
|
|
32
|
+
{ key: 'qa', label: 'QA' },
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
const hasStageData = (stage: any): boolean => {
|
|
36
|
+
if (!stage) return false;
|
|
37
|
+
const tc = stage.toolCalls;
|
|
38
|
+
return (tc && tc.total > 0)
|
|
39
|
+
|| stage.numTurns != null
|
|
40
|
+
|| stage.costUsd != null
|
|
41
|
+
|| stage.usage != null
|
|
42
|
+
|| stage.stopReason != null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const formatContextFill = (lastTurnUsage: any, contextWindow: number | null): string => {
|
|
46
|
+
if (!lastTurnUsage) return '';
|
|
47
|
+
const fill = (lastTurnUsage.input_tokens || 0)
|
|
48
|
+
+ (lastTurnUsage.cache_creation_input_tokens || 0)
|
|
49
|
+
+ (lastTurnUsage.cache_read_input_tokens || 0);
|
|
50
|
+
const denom = contextWindow || 200_000;
|
|
51
|
+
const pct = Math.round((fill / denom) * 100);
|
|
52
|
+
return `${pct}%`;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const formatCost = (costUsd: number | null): string => {
|
|
56
|
+
if (costUsd == null) return '';
|
|
57
|
+
return `$${costUsd.toFixed(4)}`;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const formatModel = (model: string | null): string => {
|
|
61
|
+
if (!model) return '';
|
|
62
|
+
// Shorten long model names: "claude-sonnet-4-20250514" → "sonnet-4"
|
|
63
|
+
const match = model.match(/(opus|sonnet|haiku)-(\d[\d.]*)/);
|
|
64
|
+
return match ? `${match[1]}-${match[2]}` : model;
|
|
65
|
+
};
|
|
66
|
+
|
|
29
67
|
export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }: KanbanViewProps) {
|
|
30
68
|
const [kanbanData, setKanbanData] = useState<any>(null);
|
|
31
69
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -35,10 +73,11 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
|
|
|
35
73
|
const [playAllRunning, setPlayAllRunning] = useState(false);
|
|
36
74
|
const [playAllCurrentFeature, setPlayAllCurrentFeature] = useState<string | null>(null);
|
|
37
75
|
const [copiedId, setCopiedId] = useState<string | null>(null);
|
|
76
|
+
const [activeStageTab, setActiveStageTab] = useState<Record<string, string>>({});
|
|
38
77
|
const { showToast } = useToast();
|
|
39
78
|
|
|
40
79
|
const pollersRef = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map());
|
|
41
|
-
const
|
|
80
|
+
const orchestratorPollerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
42
81
|
const kanbanDataRef = useRef(kanbanData);
|
|
43
82
|
kanbanDataRef.current = kanbanData;
|
|
44
83
|
|
|
@@ -62,6 +101,10 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
|
|
|
62
101
|
clearInterval(intervalId);
|
|
63
102
|
}
|
|
64
103
|
pollersRef.current.clear();
|
|
104
|
+
if (orchestratorPollerRef.current) {
|
|
105
|
+
clearInterval(orchestratorPollerRef.current);
|
|
106
|
+
orchestratorPollerRef.current = null;
|
|
107
|
+
}
|
|
65
108
|
};
|
|
66
109
|
}, [fetchKanban]);
|
|
67
110
|
|
|
@@ -204,6 +247,17 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
|
|
|
204
247
|
}
|
|
205
248
|
};
|
|
206
249
|
|
|
250
|
+
const handleResumePipeline = async (featureId: string) => {
|
|
251
|
+
try {
|
|
252
|
+
await apiClient.resumePipeline(featureId);
|
|
253
|
+
startPipelinePolling(featureId);
|
|
254
|
+
await fetchKanban();
|
|
255
|
+
} catch (err: any) {
|
|
256
|
+
console.error('Failed to resume pipeline:', err);
|
|
257
|
+
showToast(err?.message || 'Failed to resume pipeline');
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
207
261
|
const handleUnblock = async (featureId: string) => {
|
|
208
262
|
try {
|
|
209
263
|
await apiClient.unblockCard(featureId);
|
|
@@ -222,95 +276,71 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
|
|
|
222
276
|
} catch { /* ignore */ }
|
|
223
277
|
};
|
|
224
278
|
|
|
225
|
-
//
|
|
279
|
+
// Orchestrator status polling
|
|
280
|
+
const startOrchestratorPolling = useCallback(() => {
|
|
281
|
+
if (orchestratorPollerRef.current) return;
|
|
282
|
+
orchestratorPollerRef.current = setInterval(async () => {
|
|
283
|
+
try {
|
|
284
|
+
const status = await apiClient.getOrchestratorStatus();
|
|
285
|
+
setPlayAllRunning(status.active);
|
|
286
|
+
setPlayAllCurrentFeature(status.currentFeatureId);
|
|
287
|
+
if (status.active) {
|
|
288
|
+
await fetchKanban();
|
|
289
|
+
} else {
|
|
290
|
+
// Orchestrator stopped — clean up poller
|
|
291
|
+
if (orchestratorPollerRef.current) {
|
|
292
|
+
clearInterval(orchestratorPollerRef.current);
|
|
293
|
+
orchestratorPollerRef.current = null;
|
|
294
|
+
}
|
|
295
|
+
await fetchKanban();
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
if (orchestratorPollerRef.current) {
|
|
299
|
+
clearInterval(orchestratorPollerRef.current);
|
|
300
|
+
orchestratorPollerRef.current = null;
|
|
301
|
+
}
|
|
302
|
+
setPlayAllRunning(false);
|
|
303
|
+
setPlayAllCurrentFeature(null);
|
|
304
|
+
}
|
|
305
|
+
}, 5000);
|
|
306
|
+
}, [fetchKanban]);
|
|
307
|
+
|
|
308
|
+
// Check orchestrator status on mount to sync with server state
|
|
309
|
+
useEffect(() => {
|
|
310
|
+
apiClient.getOrchestratorStatus().then(status => {
|
|
311
|
+
setPlayAllRunning(status.active);
|
|
312
|
+
setPlayAllCurrentFeature(status.currentFeatureId);
|
|
313
|
+
if (status.active) {
|
|
314
|
+
startOrchestratorPolling();
|
|
315
|
+
}
|
|
316
|
+
}).catch(() => { /* ignore */ });
|
|
317
|
+
|
|
318
|
+
return () => {
|
|
319
|
+
if (orchestratorPollerRef.current) {
|
|
320
|
+
clearInterval(orchestratorPollerRef.current);
|
|
321
|
+
orchestratorPollerRef.current = null;
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
}, [startOrchestratorPolling]);
|
|
325
|
+
|
|
326
|
+
// Play All — thin wrapper around backend orchestrator
|
|
226
327
|
const startPlayAll = async () => {
|
|
227
328
|
if (playAllRunning) return;
|
|
228
|
-
setPlayAllRunning(true);
|
|
229
|
-
playAllAbortedRef.current = false;
|
|
230
|
-
setPlayAllCurrentFeature(null);
|
|
231
|
-
|
|
232
329
|
try {
|
|
233
|
-
await
|
|
234
|
-
|
|
235
|
-
setPlayAllRunning(false);
|
|
330
|
+
await apiClient.startPlayAll(projectId ?? undefined);
|
|
331
|
+
setPlayAllRunning(true);
|
|
236
332
|
setPlayAllCurrentFeature(null);
|
|
333
|
+
startOrchestratorPolling();
|
|
334
|
+
} catch (err: any) {
|
|
335
|
+
showToast(err?.message || 'Failed to start Play All');
|
|
237
336
|
}
|
|
238
337
|
};
|
|
239
338
|
|
|
240
|
-
const stopPlayAll = () => {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
while (!playAllAbortedRef.current) {
|
|
246
|
-
let freshKanban: any;
|
|
247
|
-
let freshGraph: any;
|
|
248
|
-
try {
|
|
249
|
-
freshKanban = await apiClient.fetchKanban(projectId ?? undefined);
|
|
250
|
-
freshGraph = await apiClient.fetchGraph(projectId ?? undefined);
|
|
251
|
-
setKanbanData(freshKanban);
|
|
252
|
-
} catch { break; }
|
|
253
|
-
|
|
254
|
-
const featureNodes = new Map<string, any>();
|
|
255
|
-
freshGraph.nodes
|
|
256
|
-
.filter((n: any) => n.type === 'feature')
|
|
257
|
-
.forEach((n: any) => featureNodes.set(n.id, n));
|
|
258
|
-
|
|
259
|
-
const todoCards = Object.entries(freshKanban)
|
|
260
|
-
.filter(([id, entry]: [string, any]) => entry.column === 'todo' && featureNodes.has(id) && !entry.dev_blocked)
|
|
261
|
-
.map(([id, entry]: [string, any]) => ({
|
|
262
|
-
id,
|
|
263
|
-
completeness: featureNodes.get(id).completeness || 0,
|
|
264
|
-
}))
|
|
265
|
-
.sort((a, b) => b.completeness - a.completeness);
|
|
266
|
-
|
|
267
|
-
if (todoCards.length === 0) break;
|
|
268
|
-
|
|
269
|
-
let processed = false;
|
|
270
|
-
for (const card of todoCards) {
|
|
271
|
-
if (playAllAbortedRef.current) return;
|
|
272
|
-
|
|
273
|
-
// Check deps
|
|
274
|
-
const deps = freshGraph.edges
|
|
275
|
-
.filter((e: any) => e.from === card.id && e.relation === 'depends_on')
|
|
276
|
-
.map((e: any) => e.to)
|
|
277
|
-
.filter((depId: string) => freshGraph.nodes.some((n: any) => n.id === depId && n.type === 'feature'));
|
|
278
|
-
const blocked = deps.some((depId: string) => !freshKanban[depId] || freshKanban[depId].column !== 'done');
|
|
279
|
-
if (blocked) continue;
|
|
280
|
-
|
|
281
|
-
setPlayAllCurrentFeature(card.id);
|
|
282
|
-
|
|
283
|
-
try {
|
|
284
|
-
await apiClient.startPipeline(card.id);
|
|
285
|
-
} catch (err: any) {
|
|
286
|
-
console.error(`Play All: failed to start pipeline for ${card.id}:`, err);
|
|
287
|
-
showToast(err?.message || `Failed to start pipeline for ${card.id}`);
|
|
288
|
-
break;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Wait for pipeline completion
|
|
292
|
-
await new Promise<void>((resolve) => {
|
|
293
|
-
const poll = async () => {
|
|
294
|
-
if (playAllAbortedRef.current) { resolve(); return; }
|
|
295
|
-
try {
|
|
296
|
-
const status = await apiClient.getPipelineStatus(card.id);
|
|
297
|
-
setPipelineStatuses(prev => ({ ...prev, [card.id]: status }));
|
|
298
|
-
if (['idle', 'completed', 'blocked', 'failed', 'interrupted'].includes(status.status)) {
|
|
299
|
-
resolve();
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
} catch { resolve(); return; }
|
|
303
|
-
setTimeout(poll, 5000);
|
|
304
|
-
};
|
|
305
|
-
poll();
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
if (playAllAbortedRef.current) return;
|
|
309
|
-
processed = true;
|
|
310
|
-
break;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (!processed) break;
|
|
339
|
+
const stopPlayAll = async () => {
|
|
340
|
+
try {
|
|
341
|
+
await apiClient.stopPlayAll();
|
|
342
|
+
} catch (err: any) {
|
|
343
|
+
showToast(err?.message || 'Failed to stop Play All');
|
|
314
344
|
}
|
|
315
345
|
};
|
|
316
346
|
|
|
@@ -356,6 +386,10 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
|
|
|
356
386
|
const pStatus = pipelineStatuses[card.id];
|
|
357
387
|
const isActive = pStatus && !['idle', 'completed', 'blocked', 'failed', 'interrupted'].includes(pStatus.status);
|
|
358
388
|
const isTerminal = pStatus && ['completed', 'failed', 'blocked', 'interrupted'].includes(pStatus.status);
|
|
389
|
+
const pipelineStatusValue = pStatus?.status ?? 'idle';
|
|
390
|
+
const showResumeBtn = ['in_progress', 'in_review'].includes(card.column)
|
|
391
|
+
&& !isActive
|
|
392
|
+
&& ['interrupted', 'failed', 'idle'].includes(pipelineStatusValue);
|
|
359
393
|
|
|
360
394
|
return (
|
|
361
395
|
<div
|
|
@@ -400,6 +434,15 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
|
|
|
400
434
|
{'\u25B6'}
|
|
401
435
|
</button>
|
|
402
436
|
)}
|
|
437
|
+
{showResumeBtn && (
|
|
438
|
+
<button
|
|
439
|
+
className="kanban-play-btn kanban-resume-btn"
|
|
440
|
+
title="Resume pipeline from last completed step"
|
|
441
|
+
onClick={(e) => { e.stopPropagation(); handleResumePipeline(card.id); }}
|
|
442
|
+
>
|
|
443
|
+
{'\u25B6'}
|
|
444
|
+
</button>
|
|
445
|
+
)}
|
|
403
446
|
{card.devBlocked && (
|
|
404
447
|
<span className="kanban-blocked-badge">Blocked</span>
|
|
405
448
|
)}
|
|
@@ -422,18 +465,88 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
|
|
|
422
465
|
</div>
|
|
423
466
|
)}
|
|
424
467
|
|
|
425
|
-
{
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
<
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
468
|
+
{(() => {
|
|
469
|
+
const ss = pStatus?.stageStats;
|
|
470
|
+
const availableTabs = ss
|
|
471
|
+
? STAGE_TABS.filter(t => hasStageData(ss[t.key]))
|
|
472
|
+
: [];
|
|
473
|
+
// Fall back to flat toolCalls for old data without stageStats
|
|
474
|
+
if (availableTabs.length === 0 && pStatus?.toolCalls?.total > 0) {
|
|
475
|
+
return (
|
|
476
|
+
<div className="kanban-tool-calls">
|
|
477
|
+
{[
|
|
478
|
+
{ label: 'Write', count: pStatus.toolCalls.write },
|
|
479
|
+
{ label: 'Edit', count: pStatus.toolCalls.edit },
|
|
480
|
+
{ label: 'Read', count: pStatus.toolCalls.read },
|
|
481
|
+
{ label: 'Bash', count: pStatus.toolCalls.bash },
|
|
482
|
+
].filter(i => i.count > 0).map(i => (
|
|
483
|
+
<span key={i.label} className="kanban-tool-badge">{i.label}: {i.count}</span>
|
|
484
|
+
))}
|
|
485
|
+
</div>
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
if (availableTabs.length === 0) return null;
|
|
489
|
+
|
|
490
|
+
const currentTab = activeStageTab[card.id] || availableTabs[availableTabs.length - 1].key;
|
|
491
|
+
const stage = ss[currentTab];
|
|
492
|
+
const tc = stage?.toolCalls;
|
|
493
|
+
|
|
494
|
+
return (
|
|
495
|
+
<div className="kanban-stage-stats" onClick={(e) => e.stopPropagation()}>
|
|
496
|
+
<div className="kanban-stage-tabs">
|
|
497
|
+
{availableTabs.map(t => (
|
|
498
|
+
<button
|
|
499
|
+
key={t.key}
|
|
500
|
+
className={`kanban-stage-tab${currentTab === t.key ? ' active' : ''}`}
|
|
501
|
+
onClick={(e) => {
|
|
502
|
+
e.stopPropagation();
|
|
503
|
+
setActiveStageTab(prev => ({ ...prev, [card.id]: t.key }));
|
|
504
|
+
}}
|
|
505
|
+
>
|
|
506
|
+
{t.label}
|
|
507
|
+
</button>
|
|
508
|
+
))}
|
|
509
|
+
</div>
|
|
510
|
+
<div className="kanban-stage-body">
|
|
511
|
+
{tc && tc.total > 0 && (
|
|
512
|
+
<div className="kanban-tool-calls">
|
|
513
|
+
{[
|
|
514
|
+
{ label: 'Write', count: tc.write },
|
|
515
|
+
{ label: 'Edit', count: tc.edit },
|
|
516
|
+
{ label: 'Read', count: tc.read },
|
|
517
|
+
{ label: 'Bash', count: tc.bash },
|
|
518
|
+
].filter(i => i.count > 0).map(i => (
|
|
519
|
+
<span key={i.label} className="kanban-tool-badge">{i.label}: {i.count}</span>
|
|
520
|
+
))}
|
|
521
|
+
</div>
|
|
522
|
+
)}
|
|
523
|
+
<div className="kanban-stage-meta">
|
|
524
|
+
{stage?.lastTurnUsage && (
|
|
525
|
+
<span className="kanban-stage-meta-item" title="Peak context window utilization">
|
|
526
|
+
Ctx: {formatContextFill(stage.lastTurnUsage, stage.contextWindow)}
|
|
527
|
+
</span>
|
|
528
|
+
)}
|
|
529
|
+
{stage?.numTurns != null && (
|
|
530
|
+
<span className="kanban-stage-meta-item" title="Agentic turns">
|
|
531
|
+
{stage.numTurns} turns
|
|
532
|
+
</span>
|
|
533
|
+
)}
|
|
534
|
+
{stage?.costUsd != null && (
|
|
535
|
+
<span className="kanban-stage-meta-item" title={`Model: ${stage.model || 'unknown'}`}>
|
|
536
|
+
{formatCost(stage.costUsd)}
|
|
537
|
+
{stage.model && <span className="kanban-stage-model"> {formatModel(stage.model)}</span>}
|
|
538
|
+
</span>
|
|
539
|
+
)}
|
|
540
|
+
{stage?.stopReason && (
|
|
541
|
+
<span className="kanban-stage-meta-item kanban-stop-reason" title="Stop reason">
|
|
542
|
+
{stage.stopReason}
|
|
543
|
+
</span>
|
|
544
|
+
)}
|
|
545
|
+
</div>
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
);
|
|
549
|
+
})()}
|
|
437
550
|
|
|
438
551
|
{card.devBlocked && (
|
|
439
552
|
<button
|
package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx
CHANGED
|
@@ -8,10 +8,11 @@ interface ProjectSelectorProps {
|
|
|
8
8
|
onCreate: (name: string) => Promise<any>;
|
|
9
9
|
onRename: (id: string, name: string) => Promise<void>;
|
|
10
10
|
onArchive: (id: string) => Promise<void>;
|
|
11
|
+
onOpenGitModal?: (project: Project) => void;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function ProjectSelector({
|
|
14
|
-
projects, selectedProjectId, onSelect, onCreate, onRename, onArchive,
|
|
15
|
+
projects, selectedProjectId, onSelect, onCreate, onRename, onArchive, onOpenGitModal,
|
|
15
16
|
}: ProjectSelectorProps) {
|
|
16
17
|
const [open, setOpen] = useState(false);
|
|
17
18
|
const [creating, setCreating] = useState(false);
|
|
@@ -123,6 +124,21 @@ export function ProjectSelector({
|
|
|
123
124
|
{project.name}
|
|
124
125
|
</button>
|
|
125
126
|
<div className="project-selector-item-actions">
|
|
127
|
+
{onOpenGitModal && (
|
|
128
|
+
<button
|
|
129
|
+
className={`project-selector-action-btn project-selector-git-btn${project.repoUrl ? ' connected' : ''}`}
|
|
130
|
+
title="Git Repository"
|
|
131
|
+
onClick={(e) => {
|
|
132
|
+
e.stopPropagation();
|
|
133
|
+
setOpen(false);
|
|
134
|
+
onOpenGitModal(project);
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor">
|
|
138
|
+
<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z" />
|
|
139
|
+
</svg>
|
|
140
|
+
</button>
|
|
141
|
+
)}
|
|
126
142
|
<button
|
|
127
143
|
className="project-selector-action-btn"
|
|
128
144
|
title="Rename"
|