vibeteam 0.5.4 → 0.6.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/dist/server/server/index.js +686 -26
- package/dist/server/server/openapi.json +89 -2
- package/package.json +1 -1
- package/public/assets/ActivityFeedPanel-B-RPEJq-.js +1 -0
- package/public/assets/ActivityFeedPanel-CoNIUfJR.css +1 -0
- package/public/assets/IdeationPanel-BD3UuuBF.css +1 -0
- package/public/assets/IdeationPanel-D2zwkzNM.js +1 -0
- package/public/assets/{KanbanPanel-uEFWKdFE.js → KanbanPanel-CWMEBU0G.js} +18 -18
- package/public/assets/{ProjectStatePanel-AjZahRKr.js → ProjectStatePanel-CLa4i0kf.js} +1 -1
- package/public/assets/index--QskQwUD.js +26 -0
- package/public/assets/{index-BRWzflLj.css → index-B6oqkH-M.css} +1 -1
- package/public/index.html +2 -2
- package/public/assets/ActivityFeedPanel-B9vC498v.js +0 -1
- package/public/assets/ActivityFeedPanel-r2bME9T5.css +0 -1
- package/public/assets/IdeationPanel-BOrX5vnk.css +0 -1
- package/public/assets/IdeationPanel-BYEjrhDi.js +0 -1
- package/public/assets/index-DQO9MIWG.js +0 -21
|
@@ -1412,6 +1412,110 @@ function broadcastKanbanTasks(projectId) {
|
|
|
1412
1412
|
|
|
1413
1413
|
const IDEATION_DIR = resolve(expandHome('~/.vibeteam/data/ideation'));
|
|
1414
1414
|
|
|
1415
|
+
// ============================================================================
|
|
1416
|
+
// Custom Ideation Categories
|
|
1417
|
+
// ============================================================================
|
|
1418
|
+
|
|
1419
|
+
const DEFAULT_CATEGORIES = [
|
|
1420
|
+
{ id: 'code_improvements', label: 'Code Improvements', color: '#4ade80', icon: '🔧', isDefault: true },
|
|
1421
|
+
{ id: 'ui_ux', label: 'UI/UX', color: '#60a5fa', icon: '🎨', isDefault: true },
|
|
1422
|
+
{ id: 'security', label: 'Security', color: '#f87171', icon: '🔒', isDefault: true },
|
|
1423
|
+
{ id: 'performance', label: 'Performance', color: '#fbbf24', icon: '⚡', isDefault: true },
|
|
1424
|
+
{ id: 'documentation', label: 'Documentation', color: '#a78bfa', icon: '📝', isDefault: true },
|
|
1425
|
+
{ id: 'code_quality', label: 'Code Quality', color: '#22d3d8', icon: '✨', isDefault: true },
|
|
1426
|
+
{ id: 'new_features', label: 'New Features', color: '#a855f7', icon: '🚀', isDefault: true },
|
|
1427
|
+
];
|
|
1428
|
+
|
|
1429
|
+
const CUSTOM_CATEGORIES_FILE = resolve(expandHome('~/.vibeteam/data/custom-categories.json'));
|
|
1430
|
+
|
|
1431
|
+
class CustomCategoryManager {
|
|
1432
|
+
constructor() {
|
|
1433
|
+
this.categories = []; // Array of { id, label, color, icon, isCustom: true }
|
|
1434
|
+
this.load();
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
load() {
|
|
1438
|
+
try {
|
|
1439
|
+
if (existsSync(CUSTOM_CATEGORIES_FILE)) {
|
|
1440
|
+
this.categories = JSON.parse(readFileSync(CUSTOM_CATEGORIES_FILE, 'utf8'));
|
|
1441
|
+
}
|
|
1442
|
+
} catch (e) {
|
|
1443
|
+
log(`Failed to load custom categories: ${e.message}`);
|
|
1444
|
+
this.categories = [];
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
save() {
|
|
1449
|
+
try {
|
|
1450
|
+
const dir = resolve(expandHome('~/.vibeteam/data'));
|
|
1451
|
+
if (!existsSync(dir)) {
|
|
1452
|
+
mkdirSync(dir, { recursive: true });
|
|
1453
|
+
}
|
|
1454
|
+
writeFileSync(CUSTOM_CATEGORIES_FILE, JSON.stringify(this.categories, null, 2));
|
|
1455
|
+
} catch (e) {
|
|
1456
|
+
log(`Failed to save custom categories: ${e.message}`);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
getAll() {
|
|
1461
|
+
return [...DEFAULT_CATEGORIES, ...this.categories];
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
_slugify(label) {
|
|
1465
|
+
return label
|
|
1466
|
+
.toLowerCase()
|
|
1467
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
1468
|
+
.replace(/^_|_$/g, '');
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
addCategory(category) {
|
|
1472
|
+
const { label, color, icon } = category;
|
|
1473
|
+
if (!label || !color || !icon) {
|
|
1474
|
+
throw new Error('label, color, and icon are required');
|
|
1475
|
+
}
|
|
1476
|
+
const slug = this._slugify(label);
|
|
1477
|
+
const suffix = Math.random().toString(36).substring(2, 6);
|
|
1478
|
+
const id = `${slug}_${suffix}`;
|
|
1479
|
+
const newCat = { id, label, color, icon, isCustom: true };
|
|
1480
|
+
this.categories.push(newCat);
|
|
1481
|
+
this.save();
|
|
1482
|
+
return newCat;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
updateCategory(id, updates) {
|
|
1486
|
+
// Can't update default categories
|
|
1487
|
+
if (DEFAULT_CATEGORIES.some(c => c.id === id)) {
|
|
1488
|
+
throw new Error('Cannot update default categories');
|
|
1489
|
+
}
|
|
1490
|
+
const cat = this.categories.find(c => c.id === id);
|
|
1491
|
+
if (!cat) {
|
|
1492
|
+
throw new Error('Category not found');
|
|
1493
|
+
}
|
|
1494
|
+
if (updates.label !== undefined) cat.label = updates.label;
|
|
1495
|
+
if (updates.color !== undefined) cat.color = updates.color;
|
|
1496
|
+
if (updates.icon !== undefined) cat.icon = updates.icon;
|
|
1497
|
+
this.save();
|
|
1498
|
+
return cat;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
deleteCategory(id) {
|
|
1502
|
+
if (DEFAULT_CATEGORIES.some(c => c.id === id)) {
|
|
1503
|
+
throw new Error('Cannot delete default categories');
|
|
1504
|
+
}
|
|
1505
|
+
const idx = this.categories.findIndex(c => c.id === id);
|
|
1506
|
+
if (idx === -1) {
|
|
1507
|
+
throw new Error('Category not found');
|
|
1508
|
+
}
|
|
1509
|
+
const removed = this.categories.splice(idx, 1)[0];
|
|
1510
|
+
this.save();
|
|
1511
|
+
return removed;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
isValidCategory(categoryId) {
|
|
1515
|
+
return this.getAll().some(c => c.id === categoryId);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1415
1519
|
class IdeationManager {
|
|
1416
1520
|
constructor() {
|
|
1417
1521
|
this.sessions = new Map(); // projectId -> session
|
|
@@ -1450,6 +1554,44 @@ class IdeationManager {
|
|
|
1450
1554
|
}
|
|
1451
1555
|
}
|
|
1452
1556
|
|
|
1557
|
+
loadAllSessions() {
|
|
1558
|
+
try {
|
|
1559
|
+
if (!existsSync(IDEATION_DIR)) return;
|
|
1560
|
+
const files = readdirSync(IDEATION_DIR).filter(f => f.endsWith('.json'));
|
|
1561
|
+
let loaded = 0;
|
|
1562
|
+
for (const file of files) {
|
|
1563
|
+
try {
|
|
1564
|
+
const filePath = join(IDEATION_DIR, file);
|
|
1565
|
+
const data = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
1566
|
+
const projectId = file.replace(/\.json$/, '');
|
|
1567
|
+
this.sessions.set(projectId, data);
|
|
1568
|
+
loaded++;
|
|
1569
|
+
} catch (e) {
|
|
1570
|
+
log(`Failed to load ideation file ${file}: ${e.message}`);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
// Reset any sessions stuck in generating/streaming from a previous server run
|
|
1574
|
+
let reset = 0;
|
|
1575
|
+
for (const [pid, sess] of this.sessions) {
|
|
1576
|
+
if (sess.status === 'generating' || sess.status === 'streaming') {
|
|
1577
|
+
sess.status = 'error';
|
|
1578
|
+
sess.error = 'Generation was interrupted (server restart). Please retry.';
|
|
1579
|
+
sess.updatedAt = Date.now();
|
|
1580
|
+
this.save(pid);
|
|
1581
|
+
reset++;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
if (reset > 0) {
|
|
1585
|
+
log(`Reset ${reset} stale ideation session(s) from generating/streaming to error`);
|
|
1586
|
+
}
|
|
1587
|
+
if (loaded > 0) {
|
|
1588
|
+
log(`Loaded ${loaded} ideation session(s) from disk`);
|
|
1589
|
+
}
|
|
1590
|
+
} catch (e) {
|
|
1591
|
+
log(`Failed to load ideation sessions: ${e.message}`);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1453
1595
|
getSession(projectId) {
|
|
1454
1596
|
if (!this.sessions.has(projectId)) {
|
|
1455
1597
|
this.loadSession(projectId);
|
|
@@ -1504,10 +1646,7 @@ class IdeationManager {
|
|
|
1504
1646
|
id: sessionId,
|
|
1505
1647
|
projectId,
|
|
1506
1648
|
ideas: [],
|
|
1507
|
-
enabledCategories: enabledCategories ||
|
|
1508
|
-
'code_improvements', 'ui_ux', 'security', 'performance',
|
|
1509
|
-
'documentation', 'code_quality', 'new_features'
|
|
1510
|
-
],
|
|
1649
|
+
enabledCategories: enabledCategories || DEFAULT_CATEGORIES.map(c => c.id),
|
|
1511
1650
|
status: 'generating',
|
|
1512
1651
|
createdAt: Date.now(),
|
|
1513
1652
|
updatedAt: Date.now(),
|
|
@@ -1527,15 +1666,63 @@ class IdeationManager {
|
|
|
1527
1666
|
});
|
|
1528
1667
|
|
|
1529
1668
|
try {
|
|
1530
|
-
const
|
|
1669
|
+
const enabledCats = session.enabledCategories || DEFAULT_CATEGORIES.map(c => c.id);
|
|
1670
|
+
const allCategories = customCategoryManager.getAll();
|
|
1671
|
+
const enabledCategoryDetails = enabledCats.map(catId => {
|
|
1672
|
+
const cat = allCategories.find(c => c.id === catId);
|
|
1673
|
+
return cat ? `${catId} (${cat.label})` : catId;
|
|
1674
|
+
});
|
|
1675
|
+
const categoryList = enabledCats.join(', ');
|
|
1676
|
+
const categoryDetailList = enabledCategoryDetails.join(', ');
|
|
1677
|
+
// Try to read CLAUDE.md or README for project context
|
|
1678
|
+
let projectContext = '';
|
|
1679
|
+
try {
|
|
1680
|
+
const possibleDocs = ['CLAUDE.md', 'README.md', 'readme.md'];
|
|
1681
|
+
for (const docFile of possibleDocs) {
|
|
1682
|
+
const docPath = join(projectPath, docFile);
|
|
1683
|
+
if (existsSync(docPath)) {
|
|
1684
|
+
const content = readFileSync(docPath, 'utf8').slice(0, 3000);
|
|
1685
|
+
projectContext = `\n\nProject documentation (${docFile}):\n${content}\n`;
|
|
1686
|
+
break;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
} catch (e) {
|
|
1690
|
+
// ignore
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// Detect project type from common markers
|
|
1694
|
+
let projectTypeHint = '';
|
|
1695
|
+
try {
|
|
1696
|
+
const entries = readdirSync(projectPath);
|
|
1697
|
+
if (entries.includes('Assets') && entries.includes('ProjectSettings')) {
|
|
1698
|
+
projectTypeHint = '\nThis is a Unity project. Focus ONLY on files in Assets/ (especially Scripts/). IGNORE: Library/, Logs/, Temp/, Packages/, obj/, .vs/, *.meta files, and any auto-generated Unity files.';
|
|
1699
|
+
} else if (entries.includes('Cargo.toml')) {
|
|
1700
|
+
projectTypeHint = '\nThis is a Rust project. Focus on src/ directory.';
|
|
1701
|
+
} else if (entries.includes('go.mod')) {
|
|
1702
|
+
projectTypeHint = '\nThis is a Go project. Focus on .go files.';
|
|
1703
|
+
} else if (entries.includes('Podfile') || entries.some(e => e.endsWith('.xcodeproj'))) {
|
|
1704
|
+
projectTypeHint = '\nThis is an iOS/macOS project. Focus on source code, not Pods/ or build artifacts.';
|
|
1705
|
+
} else if (entries.includes('build.gradle') || entries.includes('build.gradle.kts')) {
|
|
1706
|
+
projectTypeHint = '\nThis is a Java/Kotlin/Android project. Focus on src/ directories, not build/ or .gradle/.';
|
|
1707
|
+
}
|
|
1708
|
+
} catch (e) {
|
|
1709
|
+
// ignore
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1531
1712
|
const prompt = `You are an expert code analyst. Analyze this project's codebase and generate improvement ideas.
|
|
1713
|
+
${projectContext}${projectTypeHint}
|
|
1714
|
+
Categories to analyze: ${categoryDetailList}
|
|
1532
1715
|
|
|
1533
|
-
|
|
1716
|
+
IMPORTANT INSTRUCTIONS:
|
|
1717
|
+
1. First, if there is a CLAUDE.md file, read it to understand the project structure and conventions.
|
|
1718
|
+
2. Explore ONLY the project's source code directories — skip libraries, dependencies, build outputs, and auto-generated files.
|
|
1719
|
+
3. Read a few key source files to understand patterns and quality.
|
|
1720
|
+
4. Then output your analysis.
|
|
1534
1721
|
|
|
1535
1722
|
You MUST output your response as a valid JSON array of idea objects. No other text, no markdown, no code blocks. ONLY a JSON array.
|
|
1536
1723
|
|
|
1537
1724
|
Each idea object must have these fields:
|
|
1538
|
-
- "category": one of:
|
|
1725
|
+
- "category": one of: ${categoryList}
|
|
1539
1726
|
- "title": short descriptive title (under 80 chars)
|
|
1540
1727
|
- "description": 1-3 sentence description of the specific improvement
|
|
1541
1728
|
- "effort": one of: trivial, small, medium, large, complex
|
|
@@ -1543,12 +1730,10 @@ Each idea object must have these fields:
|
|
|
1543
1730
|
|
|
1544
1731
|
Rules:
|
|
1545
1732
|
- Generate 15-30 ideas total across the requested categories
|
|
1546
|
-
- Only generate ideas for the categories listed above
|
|
1733
|
+
- Only generate ideas for the categories listed above, using the EXACT category IDs provided
|
|
1547
1734
|
- Focus on actionable, specific improvements (reference real files/functions when possible)
|
|
1548
1735
|
- effort: trivial (<1hr), small (1-4hr), medium (1-2 days), large (3-5 days), complex (1+ weeks)
|
|
1549
|
-
- impact: low (nice to have), medium (noticeable improvement), high (significant value), critical (essential/blocking)
|
|
1550
|
-
|
|
1551
|
-
Start by exploring the project structure and reading key files, then output the JSON array.`;
|
|
1736
|
+
- impact: low (nice to have), medium (noticeable improvement), high (significant value), critical (essential/blocking)`;
|
|
1552
1737
|
|
|
1553
1738
|
// Use claude CLI in --print mode for reliable output capture
|
|
1554
1739
|
// spawn() with array args prevents shell injection
|
|
@@ -1558,6 +1743,7 @@ Start by exploring the project structure and reading key files, then output the
|
|
|
1558
1743
|
'--output-format', 'text',
|
|
1559
1744
|
'--dangerously-skip-permissions',
|
|
1560
1745
|
'--model', 'sonnet',
|
|
1746
|
+
'--max-turns', '5',
|
|
1561
1747
|
prompt,
|
|
1562
1748
|
];
|
|
1563
1749
|
|
|
@@ -1587,10 +1773,12 @@ Start by exploring the project structure and reading key files, then output the
|
|
|
1587
1773
|
});
|
|
1588
1774
|
|
|
1589
1775
|
// Track this generation with 5-minute timeout
|
|
1776
|
+
let timedOut = false;
|
|
1590
1777
|
const generationTimeout = setTimeout(() => {
|
|
1591
1778
|
log(`Ideation generation timed out for project ${projectId}`);
|
|
1779
|
+
timedOut = true;
|
|
1592
1780
|
child.kill('SIGTERM');
|
|
1593
|
-
|
|
1781
|
+
// Don't call _finishGeneration here - let the close handler do it
|
|
1594
1782
|
}, 5 * 60 * 1000);
|
|
1595
1783
|
|
|
1596
1784
|
this.activeGenerations.set(projectId, {
|
|
@@ -1611,15 +1799,49 @@ Start by exploring the project structure and reading key files, then output the
|
|
|
1611
1799
|
});
|
|
1612
1800
|
|
|
1613
1801
|
child.on('close', (code) => {
|
|
1614
|
-
log(`Ideation: claude --print exited with code ${code} for project ${projectId}`);
|
|
1802
|
+
log(`Ideation: claude --print exited with code ${code} for project ${projectId} (timedOut=${timedOut})`);
|
|
1803
|
+
|
|
1804
|
+
if (timedOut) {
|
|
1805
|
+
// If we got partial output before timeout, try to parse it
|
|
1806
|
+
if (stdout.trim()) {
|
|
1807
|
+
this._parseAndAddIdeas(projectId, stdout);
|
|
1808
|
+
const session = this.sessions.get(projectId);
|
|
1809
|
+
const ideaCount = session?.ideas?.length || 0;
|
|
1810
|
+
if (ideaCount > 0) {
|
|
1811
|
+
this._finishGeneration(projectId, 'done');
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
this._finishGeneration(projectId, 'error', 'Generation timed out — the project may be too large. Try selecting fewer categories.');
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1615
1818
|
|
|
1616
|
-
if (code !== 0
|
|
1617
|
-
|
|
1819
|
+
if (code !== 0) {
|
|
1820
|
+
// Non-zero exit: try to salvage ideas from partial stdout
|
|
1821
|
+
if (stdout.trim()) {
|
|
1822
|
+
this._parseAndAddIdeas(projectId, stdout);
|
|
1823
|
+
const session = this.sessions.get(projectId);
|
|
1824
|
+
const ideaCount = session?.ideas?.length || 0;
|
|
1825
|
+
if (ideaCount > 0) {
|
|
1826
|
+
log(`Ideation: Claude exited with code ${code} but salvaged ${ideaCount} ideas`);
|
|
1827
|
+
this._finishGeneration(projectId, 'done');
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
const hint = code === 143 ? ' (process was killed — may need more time or memory)' : '';
|
|
1832
|
+
this._finishGeneration(projectId, 'error', `Claude exited with code ${code}${hint}: ${stderr.slice(0, 200)}`);
|
|
1618
1833
|
return;
|
|
1619
1834
|
}
|
|
1620
1835
|
|
|
1621
1836
|
// Final parse of complete output
|
|
1622
1837
|
this._parseAndAddIdeas(projectId, stdout);
|
|
1838
|
+
const session = this.sessions.get(projectId);
|
|
1839
|
+
const ideaCount = session?.ideas?.length || 0;
|
|
1840
|
+
if (ideaCount === 0) {
|
|
1841
|
+
log(`Ideation: Claude returned no parseable ideas. stdout length=${stdout.length}, stderr=${stderr.slice(0, 300)}`);
|
|
1842
|
+
this._finishGeneration(projectId, 'error', 'Claude did not return any ideas. This may happen if the AI response was not in the expected format. Try again.');
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1623
1845
|
this._finishGeneration(projectId, 'done');
|
|
1624
1846
|
});
|
|
1625
1847
|
|
|
@@ -1713,9 +1935,13 @@ Start by exploring the project structure and reading key files, then output the
|
|
|
1713
1935
|
return;
|
|
1714
1936
|
}
|
|
1715
1937
|
|
|
1716
|
-
|
|
1717
|
-
if (!
|
|
1718
|
-
|
|
1938
|
+
// Validate against all categories (default + custom)
|
|
1939
|
+
if (!customCategoryManager.isValidCategory(ideaData.category)) {
|
|
1940
|
+
// Also accept if category matches an enabled category in the session (in case custom was added during generation)
|
|
1941
|
+
const session = this.sessions.get(projectId);
|
|
1942
|
+
if (!session?.enabledCategories?.includes(ideaData.category)) {
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1719
1945
|
}
|
|
1720
1946
|
|
|
1721
1947
|
const idea = {
|
|
@@ -1777,6 +2003,7 @@ Start by exploring the project structure and reading key files, then output the
|
|
|
1777
2003
|
}
|
|
1778
2004
|
|
|
1779
2005
|
const ideationManager = new IdeationManager();
|
|
2006
|
+
const customCategoryManager = new CustomCategoryManager();
|
|
1780
2007
|
|
|
1781
2008
|
// ============================================================================
|
|
1782
2009
|
// Planning Pipeline Manager
|
|
@@ -1978,8 +2205,15 @@ class PlanningPipelineManager {
|
|
|
1978
2205
|
async createPipeline(taskId, projectId, projectPath, title, description, attachments) {
|
|
1979
2206
|
// Check for existing pipeline for this task
|
|
1980
2207
|
for (const p of this.pipelines.values()) {
|
|
1981
|
-
if (p.taskId === taskId
|
|
1982
|
-
|
|
2208
|
+
if (p.taskId === taskId) {
|
|
2209
|
+
if (p.status === 'running') {
|
|
2210
|
+
throw new Error(`Task already has an active pipeline`);
|
|
2211
|
+
}
|
|
2212
|
+
// Remove old completed/failed/cancelled pipeline to allow re-execution
|
|
2213
|
+
log(`Pipeline ${taskId.slice(0, 8)}: Removing old pipeline (status: ${p.status}) for re-execution`);
|
|
2214
|
+
this.pipelines.delete(p.id);
|
|
2215
|
+
this.save();
|
|
2216
|
+
break;
|
|
1983
2217
|
}
|
|
1984
2218
|
}
|
|
1985
2219
|
|
|
@@ -3505,11 +3739,20 @@ function pollTerminalForClient(ws, sessionId, tmuxSession, forceUpdate = false)
|
|
|
3505
3739
|
lastTerminalOutput.set(sessionId, stdout);
|
|
3506
3740
|
|
|
3507
3741
|
if (ws.readyState === WebSocket.OPEN) {
|
|
3742
|
+
// Compute delta: append if new output starts with old, otherwise full
|
|
3743
|
+
let delta;
|
|
3744
|
+
if (!forceUpdate && lastOutput && stdout.startsWith(lastOutput)) {
|
|
3745
|
+
delta = { type: 'append', data: stdout.slice(lastOutput.length) };
|
|
3746
|
+
} else {
|
|
3747
|
+
delta = { type: 'full', data: stdout };
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3508
3750
|
ws.send(JSON.stringify({
|
|
3509
3751
|
type: 'terminal_output',
|
|
3510
3752
|
payload: {
|
|
3511
3753
|
sessionId,
|
|
3512
|
-
|
|
3754
|
+
version: 2,
|
|
3755
|
+
delta
|
|
3513
3756
|
}
|
|
3514
3757
|
}));
|
|
3515
3758
|
}
|
|
@@ -3557,11 +3800,20 @@ function pollAllTerminals(onEachComplete, terminalSnapshot) {
|
|
|
3557
3800
|
lastTerminalOutput.set(sessionId, stdout);
|
|
3558
3801
|
|
|
3559
3802
|
if (ws.readyState === WebSocket.OPEN) {
|
|
3803
|
+
// Compute delta: append if new output starts with old, otherwise full
|
|
3804
|
+
let delta;
|
|
3805
|
+
if (lastOutput && stdout.startsWith(lastOutput)) {
|
|
3806
|
+
delta = { type: 'append', data: stdout.slice(lastOutput.length) };
|
|
3807
|
+
} else {
|
|
3808
|
+
delta = { type: 'full', data: stdout };
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3560
3811
|
ws.send(JSON.stringify({
|
|
3561
3812
|
type: 'terminal_output',
|
|
3562
3813
|
payload: {
|
|
3563
3814
|
sessionId,
|
|
3564
|
-
|
|
3815
|
+
version: 2,
|
|
3816
|
+
delta
|
|
3565
3817
|
}
|
|
3566
3818
|
}));
|
|
3567
3819
|
}
|
|
@@ -4911,6 +5163,72 @@ function watchEventsFile() {
|
|
|
4911
5163
|
});
|
|
4912
5164
|
log(`Watching events file: ${EVENTS_FILE}`);
|
|
4913
5165
|
}
|
|
5166
|
+
// ============================================================================
|
|
5167
|
+
// Relay Connection (Mobile Remote Control)
|
|
5168
|
+
// ============================================================================
|
|
5169
|
+
let relayWs = null;
|
|
5170
|
+
let relaySessionToken = null;
|
|
5171
|
+
let relayConnected = false;
|
|
5172
|
+
|
|
5173
|
+
// Send message to relay (wrapping in envelope)
|
|
5174
|
+
function sendToRelay(data) {
|
|
5175
|
+
if (relayWs && relayWs.readyState === 1) {
|
|
5176
|
+
try {
|
|
5177
|
+
relayWs.send(JSON.stringify({
|
|
5178
|
+
type: 'relay_message',
|
|
5179
|
+
from: 'host',
|
|
5180
|
+
payload: data,
|
|
5181
|
+
timestamp: Date.now(),
|
|
5182
|
+
seq: 0,
|
|
5183
|
+
}));
|
|
5184
|
+
} catch (e) {
|
|
5185
|
+
console.error('[relay] Failed to send:', e);
|
|
5186
|
+
}
|
|
5187
|
+
}
|
|
5188
|
+
}
|
|
5189
|
+
|
|
5190
|
+
// Handle REST-over-relay requests from mobile
|
|
5191
|
+
async function handleRelayRequest(request, ws) {
|
|
5192
|
+
const { method, path, body } = request;
|
|
5193
|
+
if (!method || !path) return;
|
|
5194
|
+
|
|
5195
|
+
try {
|
|
5196
|
+
// Route the request internally
|
|
5197
|
+
const url = `http://localhost:${PORT}${path}`;
|
|
5198
|
+
const options = {
|
|
5199
|
+
method,
|
|
5200
|
+
headers: { 'Content-Type': 'application/json' },
|
|
5201
|
+
};
|
|
5202
|
+
if (body && method !== 'GET') {
|
|
5203
|
+
options.body = JSON.stringify(body);
|
|
5204
|
+
}
|
|
5205
|
+
|
|
5206
|
+
const response = await fetch(url, options);
|
|
5207
|
+
const responseBody = await response.json().catch(() => null);
|
|
5208
|
+
|
|
5209
|
+
// Send response back through relay
|
|
5210
|
+
if (ws.readyState === 1) {
|
|
5211
|
+
ws.send(JSON.stringify({
|
|
5212
|
+
type: 'relay_message',
|
|
5213
|
+
from: 'host',
|
|
5214
|
+
payload: { status: response.status, body: responseBody },
|
|
5215
|
+
timestamp: Date.now(),
|
|
5216
|
+
seq: 0,
|
|
5217
|
+
}));
|
|
5218
|
+
}
|
|
5219
|
+
} catch (err) {
|
|
5220
|
+
if (ws.readyState === 1) {
|
|
5221
|
+
ws.send(JSON.stringify({
|
|
5222
|
+
type: 'relay_message',
|
|
5223
|
+
from: 'host',
|
|
5224
|
+
payload: { status: 500, body: { error: err.message } },
|
|
5225
|
+
timestamp: Date.now(),
|
|
5226
|
+
seq: 0,
|
|
5227
|
+
}));
|
|
5228
|
+
}
|
|
5229
|
+
}
|
|
5230
|
+
}
|
|
5231
|
+
|
|
4914
5232
|
// ============================================================================
|
|
4915
5233
|
// WebSocket
|
|
4916
5234
|
// ============================================================================
|
|
@@ -4921,6 +5239,8 @@ function broadcast(message) {
|
|
|
4921
5239
|
client.send(data);
|
|
4922
5240
|
}
|
|
4923
5241
|
}
|
|
5242
|
+
// Forward to relay for mobile clients
|
|
5243
|
+
sendToRelay(message);
|
|
4924
5244
|
}
|
|
4925
5245
|
function handleClientMessage(ws, message) {
|
|
4926
5246
|
switch (message.type) {
|
|
@@ -5997,6 +6317,25 @@ async function handleHttpRequest(req, res) {
|
|
|
5997
6317
|
res.end(JSON.stringify({ ok: false, error: 'Task not found' }));
|
|
5998
6318
|
return;
|
|
5999
6319
|
}
|
|
6320
|
+
// When moving back to ideas, reset pipeline fields on the task and remove old pipeline
|
|
6321
|
+
if (toColumn === 'ideas') {
|
|
6322
|
+
const existingPipeline = pipelineManager.pipelines.get(taskId);
|
|
6323
|
+
if (existingPipeline && existingPipeline.status !== 'running') {
|
|
6324
|
+
log(`Task ${taskId.slice(0, 8)} moved to ideas: clearing old pipeline (status: ${existingPipeline.status})`);
|
|
6325
|
+
pipelineManager.pipelines.delete(taskId);
|
|
6326
|
+
pipelineManager.save();
|
|
6327
|
+
broadcast({ type: 'pipeline_removed', payload: { pipelineId: taskId } });
|
|
6328
|
+
// Reset pipeline fields on the task
|
|
6329
|
+
kanbanManager.updateTask(taskId, {
|
|
6330
|
+
pipelineId: undefined,
|
|
6331
|
+
pipelinePhase: null,
|
|
6332
|
+
pipelineStatus: undefined,
|
|
6333
|
+
pipelineLog: undefined,
|
|
6334
|
+
reviewFeedback: undefined,
|
|
6335
|
+
reviewStatus: undefined,
|
|
6336
|
+
});
|
|
6337
|
+
}
|
|
6338
|
+
}
|
|
6000
6339
|
broadcast({ type: 'kanban_task_moved', payload: { taskId, toColumn, position: task.position, task } });
|
|
6001
6340
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
6002
6341
|
res.end(JSON.stringify({ ok: true, task }));
|
|
@@ -7164,6 +7503,193 @@ async function handleHttpRequest(req, res) {
|
|
|
7164
7503
|
// Ideation Endpoints
|
|
7165
7504
|
// ============================================================================
|
|
7166
7505
|
|
|
7506
|
+
// GET /ideation/categories - Get all categories (default + custom)
|
|
7507
|
+
if (req.method === 'GET' && req.url === '/ideation/categories') {
|
|
7508
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
7509
|
+
res.end(JSON.stringify({ ok: true, categories: customCategoryManager.getAll() }));
|
|
7510
|
+
return;
|
|
7511
|
+
}
|
|
7512
|
+
|
|
7513
|
+
// POST /ideation/categories - Create a custom category
|
|
7514
|
+
if (req.method === 'POST' && req.url === '/ideation/categories') {
|
|
7515
|
+
collectRequestBody(req).then((rawBody) => {
|
|
7516
|
+
try {
|
|
7517
|
+
const body = JSON.parse(rawBody);
|
|
7518
|
+
const { label, color, icon } = body;
|
|
7519
|
+
if (!label || !color || !icon) {
|
|
7520
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
7521
|
+
res.end(JSON.stringify({ ok: false, error: 'label, color, and icon are required' }));
|
|
7522
|
+
return;
|
|
7523
|
+
}
|
|
7524
|
+
const category = customCategoryManager.addCategory({ label, color, icon });
|
|
7525
|
+
broadcast({ type: 'ideation_categories', payload: { categories: customCategoryManager.getAll() } });
|
|
7526
|
+
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
7527
|
+
res.end(JSON.stringify({ ok: true, category }));
|
|
7528
|
+
} catch (e) {
|
|
7529
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
7530
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
7531
|
+
}
|
|
7532
|
+
}).catch(() => {
|
|
7533
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
7534
|
+
res.end(JSON.stringify({ error: 'Request body too large' }));
|
|
7535
|
+
});
|
|
7536
|
+
return;
|
|
7537
|
+
}
|
|
7538
|
+
|
|
7539
|
+
// POST /ideation/categories/suggest - AI-powered category suggestions
|
|
7540
|
+
if (req.method === 'POST' && req.url === '/ideation/categories/suggest') {
|
|
7541
|
+
collectRequestBody(req).then(async (rawBody) => {
|
|
7542
|
+
try {
|
|
7543
|
+
const body = JSON.parse(rawBody);
|
|
7544
|
+
const { projectId, projectPath } = body;
|
|
7545
|
+
if (!projectId || !projectPath) {
|
|
7546
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
7547
|
+
res.end(JSON.stringify({ ok: false, error: 'projectId and projectPath are required' }));
|
|
7548
|
+
return;
|
|
7549
|
+
}
|
|
7550
|
+
|
|
7551
|
+
const existingLabels = customCategoryManager.getAll().map(c => c.label).join(', ');
|
|
7552
|
+
const suggestPrompt = `You are a project analyst. Analyze this project and suggest 5-8 domain-specific ideation categories that would be useful for generating improvement ideas.
|
|
7553
|
+
|
|
7554
|
+
The project is located at: ${projectPath}
|
|
7555
|
+
|
|
7556
|
+
Existing categories (do NOT suggest duplicates of these): ${existingLabels}
|
|
7557
|
+
|
|
7558
|
+
You MUST output your response as a valid JSON array. No other text, no markdown, no code blocks. ONLY a JSON array.
|
|
7559
|
+
|
|
7560
|
+
Each category object must have:
|
|
7561
|
+
- "label": descriptive name for the category (2-4 words, Title Case)
|
|
7562
|
+
- "color": hex color string (e.g., "#ff6b6b") - pick visually distinct colors
|
|
7563
|
+
- "icon": a single emoji that represents the category
|
|
7564
|
+
|
|
7565
|
+
Consider what kind of project this is (web app, API, game, mobile app, CLI tool, library, etc.) and suggest categories specific to its domain. For example:
|
|
7566
|
+
- A game project might get: Game Mechanics, Level Design, Player Experience
|
|
7567
|
+
- A web app might get: Accessibility, SEO, State Management
|
|
7568
|
+
- An API might get: Rate Limiting, Error Handling, API Design
|
|
7569
|
+
|
|
7570
|
+
Explore the project structure and key files to understand what it does, then output the JSON array.`;
|
|
7571
|
+
|
|
7572
|
+
const claudePath = 'claude';
|
|
7573
|
+
const args = [
|
|
7574
|
+
'--print',
|
|
7575
|
+
'--output-format', 'text',
|
|
7576
|
+
'--dangerously-skip-permissions',
|
|
7577
|
+
'--model', 'sonnet',
|
|
7578
|
+
suggestPrompt,
|
|
7579
|
+
];
|
|
7580
|
+
|
|
7581
|
+
const childEnv = { ...process.env, CLAUDE_CODE_ENTRYPOINT: 'vibeteam-category-suggest' };
|
|
7582
|
+
delete childEnv.CLAUDECODE;
|
|
7583
|
+
|
|
7584
|
+
const child = spawn(claudePath, args, {
|
|
7585
|
+
cwd: projectPath,
|
|
7586
|
+
env: childEnv,
|
|
7587
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
7588
|
+
});
|
|
7589
|
+
|
|
7590
|
+
let stdout = '';
|
|
7591
|
+
let stderr = '';
|
|
7592
|
+
|
|
7593
|
+
child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
7594
|
+
child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
7595
|
+
|
|
7596
|
+
const timeout = setTimeout(() => {
|
|
7597
|
+
child.kill('SIGTERM');
|
|
7598
|
+
}, 3 * 60 * 1000);
|
|
7599
|
+
|
|
7600
|
+
child.on('close', (code) => {
|
|
7601
|
+
clearTimeout(timeout);
|
|
7602
|
+
if (code !== 0 && !stdout.trim()) {
|
|
7603
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
7604
|
+
res.end(JSON.stringify({ ok: false, error: `Claude exited with code ${code}: ${stderr.slice(0, 200)}` }));
|
|
7605
|
+
return;
|
|
7606
|
+
}
|
|
7607
|
+
|
|
7608
|
+
try {
|
|
7609
|
+
let output = stdout.trim();
|
|
7610
|
+
output = output.replace(/^```(?:json)?\s*\n?/i, '').replace(/\n?```\s*$/i, '');
|
|
7611
|
+
const arrayStart = output.indexOf('[');
|
|
7612
|
+
const arrayEnd = output.lastIndexOf(']');
|
|
7613
|
+
if (arrayStart !== -1 && arrayEnd !== -1 && arrayEnd > arrayStart) {
|
|
7614
|
+
output = output.substring(arrayStart, arrayEnd + 1);
|
|
7615
|
+
}
|
|
7616
|
+
const suggestions = JSON.parse(output);
|
|
7617
|
+
if (!Array.isArray(suggestions)) {
|
|
7618
|
+
throw new Error('Output is not an array');
|
|
7619
|
+
}
|
|
7620
|
+
// Validate and clean suggestions
|
|
7621
|
+
const cleaned = suggestions
|
|
7622
|
+
.filter(s => s.label && s.color && s.icon)
|
|
7623
|
+
.map(s => ({ label: s.label, color: s.color, icon: s.icon }));
|
|
7624
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
7625
|
+
// Add temporary IDs so frontend can track them
|
|
7626
|
+
const withIds = cleaned.map(s => ({
|
|
7627
|
+
...s,
|
|
7628
|
+
id: `suggestion_${Math.random().toString(36).substring(2, 8)}`,
|
|
7629
|
+
isCustom: true,
|
|
7630
|
+
}));
|
|
7631
|
+
res.end(JSON.stringify({ ok: true, categories: withIds }));
|
|
7632
|
+
} catch (e) {
|
|
7633
|
+
log(`Category suggest: Failed to parse output: ${e.message}`);
|
|
7634
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
7635
|
+
res.end(JSON.stringify({ ok: false, error: 'Failed to parse AI suggestions' }));
|
|
7636
|
+
}
|
|
7637
|
+
});
|
|
7638
|
+
|
|
7639
|
+
child.on('error', (err) => {
|
|
7640
|
+
clearTimeout(timeout);
|
|
7641
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
7642
|
+
res.end(JSON.stringify({ ok: false, error: err.message }));
|
|
7643
|
+
});
|
|
7644
|
+
} catch (e) {
|
|
7645
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
7646
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
7647
|
+
}
|
|
7648
|
+
}).catch(() => {
|
|
7649
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
7650
|
+
res.end(JSON.stringify({ error: 'Request body too large' }));
|
|
7651
|
+
});
|
|
7652
|
+
return;
|
|
7653
|
+
}
|
|
7654
|
+
|
|
7655
|
+
// PUT /ideation/categories/:id - Update a custom category
|
|
7656
|
+
if (req.method === 'PUT' && req.url?.match(/^\/ideation\/categories\/[^/]+$/)) {
|
|
7657
|
+
const id = req.url.split('/ideation/categories/')[1]?.split('?')[0];
|
|
7658
|
+
collectRequestBody(req).then((rawBody) => {
|
|
7659
|
+
try {
|
|
7660
|
+
const body = JSON.parse(rawBody);
|
|
7661
|
+
const category = customCategoryManager.updateCategory(id, body);
|
|
7662
|
+
broadcast({ type: 'ideation_categories', payload: { categories: customCategoryManager.getAll() } });
|
|
7663
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
7664
|
+
res.end(JSON.stringify({ ok: true, category }));
|
|
7665
|
+
} catch (e) {
|
|
7666
|
+
const status = e.message.includes('not found') ? 404 : e.message.includes('Cannot update') ? 403 : 500;
|
|
7667
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
7668
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
7669
|
+
}
|
|
7670
|
+
}).catch(() => {
|
|
7671
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
7672
|
+
res.end(JSON.stringify({ error: 'Request body too large' }));
|
|
7673
|
+
});
|
|
7674
|
+
return;
|
|
7675
|
+
}
|
|
7676
|
+
|
|
7677
|
+
// DELETE /ideation/categories/:id - Delete a custom category
|
|
7678
|
+
if (req.method === 'DELETE' && req.url?.match(/^\/ideation\/categories\/[^/]+$/)) {
|
|
7679
|
+
const id = req.url.split('/ideation/categories/')[1]?.split('?')[0];
|
|
7680
|
+
try {
|
|
7681
|
+
customCategoryManager.deleteCategory(id);
|
|
7682
|
+
broadcast({ type: 'ideation_categories', payload: { categories: customCategoryManager.getAll() } });
|
|
7683
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
7684
|
+
res.end(JSON.stringify({ ok: true }));
|
|
7685
|
+
} catch (e) {
|
|
7686
|
+
const status = e.message.includes('not found') ? 404 : e.message.includes('Cannot delete') ? 403 : 500;
|
|
7687
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
7688
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
7689
|
+
}
|
|
7690
|
+
return;
|
|
7691
|
+
}
|
|
7692
|
+
|
|
7167
7693
|
// POST /ideation/generate - Generate ideas for a project
|
|
7168
7694
|
if (req.method === 'POST' && req.url === '/ideation/generate') {
|
|
7169
7695
|
collectRequestBody(req).then(async (rawBody) => {
|
|
@@ -7303,10 +7829,7 @@ async function handleHttpRequest(req, res) {
|
|
|
7303
7829
|
id: sessionId,
|
|
7304
7830
|
projectId,
|
|
7305
7831
|
ideas: [],
|
|
7306
|
-
enabledCategories:
|
|
7307
|
-
'code_improvements', 'ui_ux', 'security', 'performance',
|
|
7308
|
-
'documentation', 'code_quality', 'new_features'
|
|
7309
|
-
],
|
|
7832
|
+
enabledCategories: DEFAULT_CATEGORIES.map(c => c.id),
|
|
7310
7833
|
status: 'done',
|
|
7311
7834
|
createdAt: Date.now(),
|
|
7312
7835
|
updatedAt: Date.now(),
|
|
@@ -7496,6 +8019,139 @@ async function handleHttpRequest(req, res) {
|
|
|
7496
8019
|
}
|
|
7497
8020
|
|
|
7498
8021
|
// API Documentation
|
|
8022
|
+
// ========================================================================
|
|
8023
|
+
// Relay connection management (Mobile Remote Control)
|
|
8024
|
+
// ========================================================================
|
|
8025
|
+
if (req.method === 'POST' && req.url === '/relay/connect') {
|
|
8026
|
+
collectRequestBody(req).then(body => {
|
|
8027
|
+
try {
|
|
8028
|
+
const { code, relayUrl = 'wss://vibeteam-relay.fly.dev/connect' } = JSON.parse(body) || {};
|
|
8029
|
+
|
|
8030
|
+
if (!code) {
|
|
8031
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
8032
|
+
res.end(JSON.stringify({ error: 'Pairing code required' }));
|
|
8033
|
+
return;
|
|
8034
|
+
}
|
|
8035
|
+
|
|
8036
|
+
// Close existing relay connection
|
|
8037
|
+
if (relayWs) {
|
|
8038
|
+
try { relayWs.close(); } catch {}
|
|
8039
|
+
relayWs = null;
|
|
8040
|
+
}
|
|
8041
|
+
|
|
8042
|
+
try {
|
|
8043
|
+
const url = `${relayUrl}?code=${encodeURIComponent(code)}&role=host`;
|
|
8044
|
+
const ws = new WebSocket(url);
|
|
8045
|
+
|
|
8046
|
+
ws.on('open', () => {
|
|
8047
|
+
console.log('[relay] Connected to relay server');
|
|
8048
|
+
relayConnected = true;
|
|
8049
|
+
broadcast({ type: 'relay_status', payload: { connected: true } });
|
|
8050
|
+
});
|
|
8051
|
+
|
|
8052
|
+
ws.on('message', (data) => {
|
|
8053
|
+
try {
|
|
8054
|
+
const msg = JSON.parse(data.toString());
|
|
8055
|
+
|
|
8056
|
+
// Handle relay control messages
|
|
8057
|
+
if (msg.type === 'paired') {
|
|
8058
|
+
relaySessionToken = msg.payload?.sessionToken;
|
|
8059
|
+
console.log('[relay] Paired with mobile device');
|
|
8060
|
+
broadcast({ type: 'relay_status', payload: { connected: true, paired: true } });
|
|
8061
|
+
|
|
8062
|
+
// Send current state to mobile via relay
|
|
8063
|
+
const sessionsData = getSessions();
|
|
8064
|
+
sendToRelay({ type: 'sessions', payload: sessionsData });
|
|
8065
|
+
|
|
8066
|
+
// Send projects
|
|
8067
|
+
sendToRelay({ type: 'projects', payload: projectsManager.getProjects() });
|
|
8068
|
+
return;
|
|
8069
|
+
}
|
|
8070
|
+
|
|
8071
|
+
if (msg.type === 'waiting') {
|
|
8072
|
+
console.log('[relay] Waiting for mobile device to connect...');
|
|
8073
|
+
if (msg.payload?.sessionToken) {
|
|
8074
|
+
relaySessionToken = msg.payload.sessionToken;
|
|
8075
|
+
}
|
|
8076
|
+
broadcast({ type: 'relay_status', payload: { connected: true, paired: false, waiting: true } });
|
|
8077
|
+
return;
|
|
8078
|
+
}
|
|
8079
|
+
|
|
8080
|
+
if (msg.type === 'ping') {
|
|
8081
|
+
if (ws.readyState === 1) {
|
|
8082
|
+
ws.send(JSON.stringify({ type: 'pong', from: 'host', timestamp: Date.now(), seq: 0 }));
|
|
8083
|
+
}
|
|
8084
|
+
return;
|
|
8085
|
+
}
|
|
8086
|
+
|
|
8087
|
+
if (msg.type === 'mobile_disconnected') {
|
|
8088
|
+
console.log('[relay] Mobile device disconnected');
|
|
8089
|
+
broadcast({ type: 'relay_status', payload: { connected: true, paired: false } });
|
|
8090
|
+
return;
|
|
8091
|
+
}
|
|
8092
|
+
|
|
8093
|
+
// Handle relay_message from mobile (REST-over-relay)
|
|
8094
|
+
if (msg.type === 'relay_message' && msg.payload) {
|
|
8095
|
+
handleRelayRequest(msg.payload, ws);
|
|
8096
|
+
return;
|
|
8097
|
+
}
|
|
8098
|
+
} catch (e) {
|
|
8099
|
+
console.error('[relay] Error processing message:', e);
|
|
8100
|
+
}
|
|
8101
|
+
});
|
|
8102
|
+
|
|
8103
|
+
ws.on('close', () => {
|
|
8104
|
+
console.log('[relay] Disconnected from relay server');
|
|
8105
|
+
relayWs = null;
|
|
8106
|
+
relayConnected = false;
|
|
8107
|
+
relaySessionToken = null;
|
|
8108
|
+
broadcast({ type: 'relay_status', payload: { connected: false } });
|
|
8109
|
+
});
|
|
8110
|
+
|
|
8111
|
+
ws.on('error', (err) => {
|
|
8112
|
+
console.error('[relay] WebSocket error:', err.message);
|
|
8113
|
+
});
|
|
8114
|
+
|
|
8115
|
+
relayWs = ws;
|
|
8116
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
8117
|
+
res.end(JSON.stringify({ success: true, message: 'Connecting to relay...' }));
|
|
8118
|
+
} catch (err) {
|
|
8119
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
8120
|
+
res.end(JSON.stringify({ error: 'Failed to connect to relay', details: err.message }));
|
|
8121
|
+
}
|
|
8122
|
+
} catch (e) {
|
|
8123
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
8124
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
8125
|
+
}
|
|
8126
|
+
}).catch(() => {
|
|
8127
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
8128
|
+
res.end(JSON.stringify({ error: 'Request body too large' }));
|
|
8129
|
+
});
|
|
8130
|
+
return;
|
|
8131
|
+
}
|
|
8132
|
+
|
|
8133
|
+
if (req.method === 'GET' && req.url === '/relay/status') {
|
|
8134
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
8135
|
+
res.end(JSON.stringify({
|
|
8136
|
+
connected: relayConnected,
|
|
8137
|
+
sessionToken: relaySessionToken ? '***' : null,
|
|
8138
|
+
}));
|
|
8139
|
+
return;
|
|
8140
|
+
}
|
|
8141
|
+
|
|
8142
|
+
if (req.method === 'POST' && req.url === '/relay/disconnect') {
|
|
8143
|
+
if (relayWs) {
|
|
8144
|
+
try { relayWs.close(); } catch {}
|
|
8145
|
+
relayWs = null;
|
|
8146
|
+
}
|
|
8147
|
+
relayConnected = false;
|
|
8148
|
+
relaySessionToken = null;
|
|
8149
|
+
broadcast({ type: 'relay_status', payload: { connected: false } });
|
|
8150
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
8151
|
+
res.end(JSON.stringify({ success: true }));
|
|
8152
|
+
return;
|
|
8153
|
+
}
|
|
8154
|
+
|
|
7499
8155
|
if (req.method === 'GET' && req.url === '/api/docs/openapi.json') {
|
|
7500
8156
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
7501
8157
|
res.end(JSON.stringify(openapiSpec));
|
|
@@ -7642,6 +8298,8 @@ function main() {
|
|
|
7642
8298
|
loadSessions();
|
|
7643
8299
|
// Load saved text tiles
|
|
7644
8300
|
loadTiles();
|
|
8301
|
+
// Load saved ideation sessions
|
|
8302
|
+
ideationManager.loadAllSessions();
|
|
7645
8303
|
// Start git status tracking with adaptive polling
|
|
7646
8304
|
gitStatusManager.setUpdateHandler(({ sessionId, status }) => {
|
|
7647
8305
|
const session = managedSessions.get(sessionId);
|
|
@@ -7707,6 +8365,8 @@ function main() {
|
|
|
7707
8365
|
payload: { tasks: kanbanManager.getTasks() },
|
|
7708
8366
|
};
|
|
7709
8367
|
ws.send(JSON.stringify(kanbanMsg));
|
|
8368
|
+
// Send ideation categories (default + custom)
|
|
8369
|
+
ws.send(JSON.stringify({ type: 'ideation_categories', payload: { categories: customCategoryManager.getAll() } }));
|
|
7710
8370
|
// Send ideation sessions for all projects
|
|
7711
8371
|
for (const [, session] of ideationManager.sessions) {
|
|
7712
8372
|
ws.send(JSON.stringify({ type: 'ideation_session', payload: { session } }));
|