claudeck 1.0.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/LICENSE +21 -0
- package/README.md +233 -0
- package/cli.js +2 -0
- package/config/agent-chains.json +16 -0
- package/config/agent-dags.json +16 -0
- package/config/agents.json +46 -0
- package/config/bot-prompt.json +3 -0
- package/config/folders.json +66 -0
- package/config/prompts.json +92 -0
- package/config/repos.json +86 -0
- package/config/telegram-config.json +17 -0
- package/config/workflows.json +90 -0
- package/db.js +1198 -0
- package/package.json +55 -0
- package/plugins/claude-editor/client.css +171 -0
- package/plugins/claude-editor/client.js +183 -0
- package/plugins/event-stream/client.css +207 -0
- package/plugins/event-stream/client.js +271 -0
- package/plugins/linear/client.css +345 -0
- package/plugins/linear/client.js +380 -0
- package/plugins/linear/config.json +5 -0
- package/plugins/linear/server.js +312 -0
- package/plugins/repos/client.css +549 -0
- package/plugins/repos/client.js +663 -0
- package/plugins/repos/server.js +232 -0
- package/plugins/sudoku/client.css +196 -0
- package/plugins/sudoku/client.js +329 -0
- package/plugins/tasks/client.css +414 -0
- package/plugins/tasks/client.js +394 -0
- package/plugins/tasks/server.js +116 -0
- package/plugins/tic-tac-toe/client.css +167 -0
- package/plugins/tic-tac-toe/client.js +241 -0
- package/public/css/core/components.css +232 -0
- package/public/css/core/layout.css +330 -0
- package/public/css/core/print.css +18 -0
- package/public/css/core/reset.css +36 -0
- package/public/css/core/responsive.css +378 -0
- package/public/css/core/theme.css +116 -0
- package/public/css/core/variables.css +93 -0
- package/public/css/features/agent-monitor.css +297 -0
- package/public/css/features/agent-sidebar.css +525 -0
- package/public/css/features/agents.css +996 -0
- package/public/css/features/analytics.css +181 -0
- package/public/css/features/background-sessions.css +321 -0
- package/public/css/features/cost-dashboard.css +168 -0
- package/public/css/features/home.css +313 -0
- package/public/css/features/retro-terminal.css +88 -0
- package/public/css/features/telegram.css +127 -0
- package/public/css/features/tour.css +148 -0
- package/public/css/features/voice-input.css +60 -0
- package/public/css/features/welcome.css +241 -0
- package/public/css/panels/assistant-bot.css +442 -0
- package/public/css/panels/dev-docs.css +292 -0
- package/public/css/panels/file-explorer.css +322 -0
- package/public/css/panels/git-panel.css +221 -0
- package/public/css/panels/mcp-manager.css +199 -0
- package/public/css/panels/tips-feed.css +353 -0
- package/public/css/ui/commands.css +273 -0
- package/public/css/ui/context-gauge.css +76 -0
- package/public/css/ui/file-picker.css +69 -0
- package/public/css/ui/image-attachments.css +106 -0
- package/public/css/ui/messages.css +884 -0
- package/public/css/ui/modals.css +122 -0
- package/public/css/ui/parallel.css +217 -0
- package/public/css/ui/permissions.css +110 -0
- package/public/css/ui/right-panel.css +481 -0
- package/public/css/ui/sessions.css +689 -0
- package/public/css/ui/status-bar.css +425 -0
- package/public/css/ui/toolbox.css +206 -0
- package/public/data/tips.json +218 -0
- package/public/icons/favicon.png +0 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/icons/whaly.png +0 -0
- package/public/index.html +1140 -0
- package/public/js/core/api.js +591 -0
- package/public/js/core/constants.js +3 -0
- package/public/js/core/dom.js +270 -0
- package/public/js/core/events.js +10 -0
- package/public/js/core/plugin-loader.js +153 -0
- package/public/js/core/store.js +39 -0
- package/public/js/core/utils.js +25 -0
- package/public/js/core/ws.js +64 -0
- package/public/js/features/agent-monitor.js +222 -0
- package/public/js/features/agents.js +1209 -0
- package/public/js/features/analytics.js +397 -0
- package/public/js/features/attachments.js +251 -0
- package/public/js/features/background-sessions.js +475 -0
- package/public/js/features/chat.js +589 -0
- package/public/js/features/cost-dashboard.js +152 -0
- package/public/js/features/dag-editor.js +399 -0
- package/public/js/features/easter-egg.js +46 -0
- package/public/js/features/home.js +270 -0
- package/public/js/features/projects.js +372 -0
- package/public/js/features/prompts.js +228 -0
- package/public/js/features/sessions.js +332 -0
- package/public/js/features/telegram.js +131 -0
- package/public/js/features/tour.js +210 -0
- package/public/js/features/voice-input.js +185 -0
- package/public/js/features/welcome.js +43 -0
- package/public/js/features/workflows.js +277 -0
- package/public/js/main.js +51 -0
- package/public/js/panels/assistant-bot.js +445 -0
- package/public/js/panels/dev-docs.js +380 -0
- package/public/js/panels/file-explorer.js +486 -0
- package/public/js/panels/git-panel.js +285 -0
- package/public/js/panels/mcp-manager.js +311 -0
- package/public/js/panels/tips-feed.js +303 -0
- package/public/js/ui/commands.js +114 -0
- package/public/js/ui/context-gauge.js +100 -0
- package/public/js/ui/diff.js +124 -0
- package/public/js/ui/disabled-tools.js +36 -0
- package/public/js/ui/export.js +74 -0
- package/public/js/ui/formatting.js +206 -0
- package/public/js/ui/header-dropdowns.js +72 -0
- package/public/js/ui/input-meta.js +71 -0
- package/public/js/ui/max-turns.js +21 -0
- package/public/js/ui/messages.js +387 -0
- package/public/js/ui/model-selector.js +20 -0
- package/public/js/ui/notifications.js +232 -0
- package/public/js/ui/parallel.js +176 -0
- package/public/js/ui/permissions.js +168 -0
- package/public/js/ui/right-panel.js +173 -0
- package/public/js/ui/shortcuts.js +143 -0
- package/public/js/ui/sidebar-toggle.js +29 -0
- package/public/js/ui/status-bar.js +172 -0
- package/public/js/ui/tab-sdk.js +623 -0
- package/public/js/ui/theme.js +38 -0
- package/public/manifest.json +13 -0
- package/public/offline.html +190 -0
- package/public/style.css +42 -0
- package/public/sw.js +91 -0
- package/server/agent-loop.js +385 -0
- package/server/dag-executor.js +265 -0
- package/server/orchestrator.js +514 -0
- package/server/paths.js +61 -0
- package/server/plugin-mount.js +56 -0
- package/server/push-sender.js +31 -0
- package/server/routes/agents.js +294 -0
- package/server/routes/bot.js +45 -0
- package/server/routes/exec.js +35 -0
- package/server/routes/files.js +218 -0
- package/server/routes/mcp.js +82 -0
- package/server/routes/messages.js +36 -0
- package/server/routes/notifications.js +37 -0
- package/server/routes/projects.js +207 -0
- package/server/routes/prompts.js +53 -0
- package/server/routes/sessions.js +103 -0
- package/server/routes/stats.js +143 -0
- package/server/routes/telegram.js +71 -0
- package/server/routes/tips.js +135 -0
- package/server/routes/workflows.js +81 -0
- package/server/summarizer.js +55 -0
- package/server/telegram-poller.js +205 -0
- package/server/telegram-sender.js +304 -0
- package/server/ws-handler.js +926 -0
- package/server.js +179 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
// Repos Tab — Tab SDK plugin for managing git repositories
|
|
2
|
+
import { registerTab } from '/js/ui/tab-sdk.js';
|
|
3
|
+
import { escapeHtml } from '/js/core/utils.js';
|
|
4
|
+
|
|
5
|
+
// ── SVG Icons ────────────────────────────────────────
|
|
6
|
+
const ICONS = {
|
|
7
|
+
chevron: `<svg class="repos-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>`,
|
|
8
|
+
folderClosed: `<svg class="repos-tree-icon folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`,
|
|
9
|
+
folderOpen: `<svg class="repos-tree-icon folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 19a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4l2 3h9a2 2 0 0 1 2 2v1"/><path d="M21 12H8a2 2 0 0 0-2 2l-1 5h16l-1-5a2 2 0 0 0-2-2z"/></svg>`,
|
|
10
|
+
repo: `<svg class="repos-tree-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>`,
|
|
11
|
+
search: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`,
|
|
12
|
+
plus: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>`,
|
|
13
|
+
folderPlus: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/><line x1="12" y1="11" x2="12" y2="17"/><line x1="9" y1="14" x2="15" y2="14"/></svg>`,
|
|
14
|
+
refresh: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>`,
|
|
15
|
+
edit: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`,
|
|
16
|
+
trash: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>`,
|
|
17
|
+
link: `<svg class="repos-meta-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`,
|
|
18
|
+
path: `<svg class="repos-meta-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
registerTab({
|
|
22
|
+
id: 'repos',
|
|
23
|
+
title: 'Repos',
|
|
24
|
+
icon: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65S8.93 17.38 9 18v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>',
|
|
25
|
+
lazy: true,
|
|
26
|
+
|
|
27
|
+
init(ctx) {
|
|
28
|
+
let groups = [];
|
|
29
|
+
let repos = [];
|
|
30
|
+
let expandedGroups = new Set();
|
|
31
|
+
let searchQuery = '';
|
|
32
|
+
let editingId = null;
|
|
33
|
+
let dragItem = null;
|
|
34
|
+
|
|
35
|
+
// ── Build DOM ─────────────────────────────────────
|
|
36
|
+
const root = document.createElement('div');
|
|
37
|
+
root.className = 'repos-tab';
|
|
38
|
+
root.style.cssText = 'display:flex;flex-direction:column;flex:1;overflow:hidden;';
|
|
39
|
+
|
|
40
|
+
root.innerHTML = `
|
|
41
|
+
<div class="repos-toolbar">
|
|
42
|
+
<div class="repos-search-wrap">
|
|
43
|
+
${ICONS.search}
|
|
44
|
+
<input type="text" placeholder="Search repos..." autocomplete="off" class="repos-search">
|
|
45
|
+
</div>
|
|
46
|
+
<div class="repos-toolbar-actions">
|
|
47
|
+
<button class="repos-toolbar-btn repos-add-group-btn" title="New group">${ICONS.folderPlus}</button>
|
|
48
|
+
<button class="repos-toolbar-btn repos-add-repo-btn" title="Add repo">${ICONS.plus}</button>
|
|
49
|
+
<button class="repos-toolbar-btn repos-refresh-btn" title="Refresh">${ICONS.refresh}</button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="repos-list"></div>
|
|
53
|
+
<div class="repos-footer">
|
|
54
|
+
<span class="repos-count">0 repos</span>
|
|
55
|
+
</div>
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
const listEl = root.querySelector('.repos-list');
|
|
59
|
+
const countEl = root.querySelector('.repos-count');
|
|
60
|
+
const searchEl = root.querySelector('.repos-search');
|
|
61
|
+
|
|
62
|
+
// ── API helpers ──────────────────────────────────
|
|
63
|
+
async function loadData() {
|
|
64
|
+
const refreshBtn = root.querySelector('.repos-refresh-btn');
|
|
65
|
+
refreshBtn.classList.add('spinning');
|
|
66
|
+
try {
|
|
67
|
+
const data = await ctx.api.fetchRepos();
|
|
68
|
+
groups = data.groups || [];
|
|
69
|
+
repos = data.repos || [];
|
|
70
|
+
render();
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error('Failed to load repos:', err);
|
|
73
|
+
listEl.innerHTML = '<div class="repos-empty">Failed to load repos</div>';
|
|
74
|
+
} finally {
|
|
75
|
+
refreshBtn.classList.remove('spinning');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Tree building ────────────────────────────────
|
|
80
|
+
function getChildGroups(parentId) {
|
|
81
|
+
return groups.filter(g => (g.parentId || null) === parentId);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getGroupRepos(groupId) {
|
|
85
|
+
return repos.filter(r => (r.groupId || null) === groupId);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function matchesSearch(item) {
|
|
89
|
+
if (!searchQuery) return true;
|
|
90
|
+
const q = searchQuery.toLowerCase();
|
|
91
|
+
if (item.name && item.name.toLowerCase().includes(q)) return true;
|
|
92
|
+
if (item.path && item.path.toLowerCase().includes(q)) return true;
|
|
93
|
+
if (item.url && item.url.toLowerCase().includes(q)) return true;
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function groupHasMatchingDescendants(groupId) {
|
|
98
|
+
if (getGroupRepos(groupId).some(r => matchesSearch(r))) return true;
|
|
99
|
+
for (const child of getChildGroups(groupId)) {
|
|
100
|
+
if (matchesSearch(child) || groupHasMatchingDescendants(child.id)) return true;
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Render ────────────────────────────────────────
|
|
106
|
+
function render() {
|
|
107
|
+
listEl.innerHTML = '';
|
|
108
|
+
|
|
109
|
+
if (searchQuery) {
|
|
110
|
+
expandedGroups = new Set(groups.map(g => g.id));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const hasContent = groups.length > 0 || repos.length > 0;
|
|
114
|
+
if (!hasContent) {
|
|
115
|
+
listEl.innerHTML = `<div class="repos-empty">
|
|
116
|
+
<div class="repos-empty-icon">${ICONS.repo}</div>
|
|
117
|
+
<div>No repositories yet</div>
|
|
118
|
+
<div class="repos-empty-hint">Click + to add one, or right-click for options</div>
|
|
119
|
+
</div>`;
|
|
120
|
+
updateCount();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
renderLevel(listEl, null, 0);
|
|
125
|
+
|
|
126
|
+
// If search active and nothing matched
|
|
127
|
+
if (searchQuery && listEl.children.length === 0) {
|
|
128
|
+
listEl.innerHTML = `<div class="repos-empty">No matches for "${escapeHtml(searchQuery)}"</div>`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
updateCount();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function renderLevel(container, parentId, depth) {
|
|
135
|
+
const childGroups = getChildGroups(parentId);
|
|
136
|
+
const childRepos = getGroupRepos(parentId);
|
|
137
|
+
|
|
138
|
+
for (const group of childGroups) {
|
|
139
|
+
if (searchQuery && !matchesSearch(group) && !groupHasMatchingDescendants(group.id)) continue;
|
|
140
|
+
renderGroup(container, group, depth);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const repo of childRepos) {
|
|
144
|
+
if (searchQuery && !matchesSearch(repo)) continue;
|
|
145
|
+
renderRepo(container, repo, depth);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function renderGroup(container, group, depth) {
|
|
150
|
+
const isExpanded = expandedGroups.has(group.id);
|
|
151
|
+
const row = document.createElement('div');
|
|
152
|
+
row.className = 'repos-item repos-group-item';
|
|
153
|
+
row.dataset.id = group.id;
|
|
154
|
+
row.dataset.type = 'group';
|
|
155
|
+
row.style.paddingLeft = `${8 + depth * 16}px`;
|
|
156
|
+
row.draggable = true;
|
|
157
|
+
|
|
158
|
+
const childCount = countDescendantRepos(group.id);
|
|
159
|
+
|
|
160
|
+
if (editingId === group.id) {
|
|
161
|
+
row.innerHTML = `
|
|
162
|
+
${ICONS.chevron}
|
|
163
|
+
${isExpanded ? ICONS.folderOpen : ICONS.folderClosed}
|
|
164
|
+
<input type="text" class="repos-inline-input" value="${escapeHtml(group.name)}" data-id="${group.id}" data-kind="group">
|
|
165
|
+
`;
|
|
166
|
+
row.querySelector('.repos-inline-input').addEventListener('keydown', handleInlineEditKey);
|
|
167
|
+
row.querySelector('.repos-inline-input').addEventListener('blur', handleInlineEditBlur);
|
|
168
|
+
setTimeout(() => row.querySelector('.repos-inline-input')?.focus(), 0);
|
|
169
|
+
} else {
|
|
170
|
+
row.innerHTML = `
|
|
171
|
+
${ICONS.chevron}
|
|
172
|
+
${isExpanded ? ICONS.folderOpen : ICONS.folderClosed}
|
|
173
|
+
<span class="repos-group-name">${escapeHtml(group.name)}</span>
|
|
174
|
+
<span class="repos-group-badge">${childCount}</span>
|
|
175
|
+
<div class="repos-item-actions">
|
|
176
|
+
<button class="repos-item-btn repos-edit-btn" data-id="${group.id}" data-kind="group" title="Rename">${ICONS.edit}</button>
|
|
177
|
+
<button class="repos-item-btn repos-delete-btn" data-id="${group.id}" data-kind="group" title="Delete">${ICONS.trash}</button>
|
|
178
|
+
</div>
|
|
179
|
+
`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const chevron = row.querySelector('.repos-chevron');
|
|
183
|
+
if (isExpanded) chevron.classList.add('expanded');
|
|
184
|
+
|
|
185
|
+
row.addEventListener('click', (e) => {
|
|
186
|
+
if (e.target.closest('.repos-item-actions') || e.target.closest('.repos-inline-input')) return;
|
|
187
|
+
if (expandedGroups.has(group.id)) expandedGroups.delete(group.id);
|
|
188
|
+
else expandedGroups.add(group.id);
|
|
189
|
+
render();
|
|
190
|
+
});
|
|
191
|
+
row.addEventListener('contextmenu', (e) => showGroupContextMenu(e, group));
|
|
192
|
+
row.addEventListener('dragstart', (e) => {
|
|
193
|
+
dragItem = { type: 'group', id: group.id };
|
|
194
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
195
|
+
row.classList.add('dragging');
|
|
196
|
+
});
|
|
197
|
+
row.addEventListener('dragend', () => { row.classList.remove('dragging'); dragItem = null; clearDropTargets(); });
|
|
198
|
+
row.addEventListener('dragover', handleDragOver);
|
|
199
|
+
row.addEventListener('dragleave', handleDragLeave);
|
|
200
|
+
row.addEventListener('drop', (e) => handleDrop(e, 'group', group.id));
|
|
201
|
+
|
|
202
|
+
container.appendChild(row);
|
|
203
|
+
|
|
204
|
+
if (isExpanded) {
|
|
205
|
+
const children = document.createElement('div');
|
|
206
|
+
children.className = 'repos-group-children expanded';
|
|
207
|
+
renderLevel(children, group.id, depth + 1);
|
|
208
|
+
container.appendChild(children);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function renderRepo(container, repo, depth) {
|
|
213
|
+
const row = document.createElement('div');
|
|
214
|
+
row.className = 'repos-item repos-repo-item';
|
|
215
|
+
row.dataset.id = repo.id;
|
|
216
|
+
row.dataset.type = 'repo';
|
|
217
|
+
row.style.paddingLeft = `${8 + depth * 16}px`;
|
|
218
|
+
row.draggable = true;
|
|
219
|
+
|
|
220
|
+
if (editingId === repo.id) {
|
|
221
|
+
row.innerHTML = `
|
|
222
|
+
<span class="repos-chevron-spacer"></span>
|
|
223
|
+
${ICONS.repo}
|
|
224
|
+
<input type="text" class="repos-inline-input" value="${escapeHtml(repo.name)}" data-id="${repo.id}" data-kind="repo">
|
|
225
|
+
`;
|
|
226
|
+
row.querySelector('.repos-inline-input').addEventListener('keydown', handleInlineEditKey);
|
|
227
|
+
row.querySelector('.repos-inline-input').addEventListener('blur', handleInlineEditBlur);
|
|
228
|
+
setTimeout(() => row.querySelector('.repos-inline-input')?.focus(), 0);
|
|
229
|
+
} else {
|
|
230
|
+
const pathShort = repo.path ? repo.path.replace(/^\/Users\/[^/]+/, '~') : '';
|
|
231
|
+
row.innerHTML = `
|
|
232
|
+
<span class="repos-chevron-spacer"></span>
|
|
233
|
+
${ICONS.repo}
|
|
234
|
+
<div class="repos-repo-info">
|
|
235
|
+
<span class="repos-repo-name">${escapeHtml(repo.name)}</span>
|
|
236
|
+
${pathShort ? `<span class="repos-repo-meta">${ICONS.path}<span>${escapeHtml(pathShort)}</span></span>` : ''}
|
|
237
|
+
${repo.url ? `<span class="repos-repo-meta">${ICONS.link}<span>${escapeHtml(repo.url)}</span></span>` : ''}
|
|
238
|
+
</div>
|
|
239
|
+
<div class="repos-item-actions">
|
|
240
|
+
<button class="repos-item-btn repos-edit-btn" data-id="${repo.id}" data-kind="repo" title="Rename">${ICONS.edit}</button>
|
|
241
|
+
<button class="repos-item-btn repos-delete-btn" data-id="${repo.id}" data-kind="repo" title="Delete">${ICONS.trash}</button>
|
|
242
|
+
</div>
|
|
243
|
+
`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
row.addEventListener('contextmenu', (e) => showRepoContextMenu(e, repo));
|
|
247
|
+
|
|
248
|
+
if (repo.path) {
|
|
249
|
+
row.addEventListener('dblclick', () => ctx.api.execCommand('code .', repo.path));
|
|
250
|
+
} else if (repo.url) {
|
|
251
|
+
row.addEventListener('dblclick', () => window.open(repo.url, '_blank'));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
row.addEventListener('dragstart', (e) => {
|
|
255
|
+
dragItem = { type: 'repo', id: repo.id };
|
|
256
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
257
|
+
row.classList.add('dragging');
|
|
258
|
+
});
|
|
259
|
+
row.addEventListener('dragend', () => { row.classList.remove('dragging'); dragItem = null; clearDropTargets(); });
|
|
260
|
+
|
|
261
|
+
container.appendChild(row);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function countDescendantRepos(groupId) {
|
|
265
|
+
let count = getGroupRepos(groupId).length;
|
|
266
|
+
for (const child of getChildGroups(groupId)) {
|
|
267
|
+
count += countDescendantRepos(child.id);
|
|
268
|
+
}
|
|
269
|
+
return count;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function updateCount() {
|
|
273
|
+
const n = repos.length;
|
|
274
|
+
const g = groups.length;
|
|
275
|
+
countEl.textContent = `${n} repo${n !== 1 ? 's' : ''}${g > 0 ? ` \u00b7 ${g} group${g !== 1 ? 's' : ''}` : ''}`;
|
|
276
|
+
ctx.showBadge(n);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── Drag & drop helpers ─────────────────────────
|
|
280
|
+
function handleDragOver(e) {
|
|
281
|
+
e.preventDefault();
|
|
282
|
+
if (!dragItem) return;
|
|
283
|
+
e.dataTransfer.dropEffect = 'move';
|
|
284
|
+
e.currentTarget.classList.add('drop-target');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function handleDragLeave(e) {
|
|
288
|
+
e.currentTarget.classList.remove('drop-target');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function clearDropTargets() {
|
|
292
|
+
root.querySelectorAll('.drop-target').forEach(el => el.classList.remove('drop-target'));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function handleDrop(e, targetType, targetId) {
|
|
296
|
+
e.preventDefault();
|
|
297
|
+
e.stopPropagation();
|
|
298
|
+
clearDropTargets();
|
|
299
|
+
if (!dragItem || dragItem.id === targetId) return;
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
if (dragItem.type === 'repo') {
|
|
303
|
+
const newGroupId = targetType === 'group' ? targetId : null;
|
|
304
|
+
await ctx.api.updateRepo(dragItem.id, { groupId: newGroupId });
|
|
305
|
+
} else if (dragItem.type === 'group') {
|
|
306
|
+
if (targetType === 'group' && targetId !== dragItem.id) {
|
|
307
|
+
await ctx.api.updateRepoGroup(dragItem.id, { parentId: targetId });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
await loadData();
|
|
311
|
+
} catch (err) {
|
|
312
|
+
console.error('Drop failed:', err);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ── Inline editing ──────────────────────────────
|
|
317
|
+
function handleInlineEditKey(e) {
|
|
318
|
+
if (e.key === 'Enter') {
|
|
319
|
+
e.target.blur();
|
|
320
|
+
} else if (e.key === 'Escape') {
|
|
321
|
+
editingId = null;
|
|
322
|
+
render();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function handleInlineEditBlur(e) {
|
|
327
|
+
const id = e.target.dataset.id;
|
|
328
|
+
const kind = e.target.dataset.kind;
|
|
329
|
+
const newName = e.target.value.trim();
|
|
330
|
+
editingId = null;
|
|
331
|
+
|
|
332
|
+
if (!newName) { render(); return; }
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
if (kind === 'group') {
|
|
336
|
+
await ctx.api.updateRepoGroup(id, { name: newName });
|
|
337
|
+
} else {
|
|
338
|
+
await ctx.api.updateRepo(id, { name: newName });
|
|
339
|
+
}
|
|
340
|
+
await loadData();
|
|
341
|
+
} catch (err) {
|
|
342
|
+
console.error('Rename failed:', err);
|
|
343
|
+
render();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ── Add repo dialog ─────────────────────────────
|
|
348
|
+
function showAddRepoDialog(targetGroupId = null) {
|
|
349
|
+
const overlay = document.createElement('div');
|
|
350
|
+
overlay.className = 'repos-dialog-overlay';
|
|
351
|
+
overlay.innerHTML = `
|
|
352
|
+
<div class="repos-dialog">
|
|
353
|
+
<div class="repos-dialog-title">Add Repository</div>
|
|
354
|
+
<label class="repos-dialog-label">Name *
|
|
355
|
+
<input type="text" class="repos-dialog-input" name="name" placeholder="my-project" autocomplete="off">
|
|
356
|
+
</label>
|
|
357
|
+
<label class="repos-dialog-label">Local Path
|
|
358
|
+
<input type="text" class="repos-dialog-input" name="path" placeholder="/path/to/repo" autocomplete="off">
|
|
359
|
+
</label>
|
|
360
|
+
<label class="repos-dialog-label">Remote URL
|
|
361
|
+
<input type="text" class="repos-dialog-input" name="url" placeholder="https://github.com/..." autocomplete="off">
|
|
362
|
+
</label>
|
|
363
|
+
<label class="repos-dialog-label">Group
|
|
364
|
+
<select class="repos-dialog-input" name="groupId">
|
|
365
|
+
<option value="">None</option>
|
|
366
|
+
${groups.map(g => `<option value="${g.id}"${g.id === targetGroupId ? ' selected' : ''}>${escapeHtml(g.name)}</option>`).join('')}
|
|
367
|
+
</select>
|
|
368
|
+
</label>
|
|
369
|
+
<div class="repos-dialog-actions">
|
|
370
|
+
<button class="repos-btn repos-dialog-cancel">Cancel</button>
|
|
371
|
+
<button class="repos-btn repos-dialog-save">Add</button>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
`;
|
|
375
|
+
|
|
376
|
+
overlay.querySelector('.repos-dialog-cancel').addEventListener('click', () => overlay.remove());
|
|
377
|
+
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
|
378
|
+
|
|
379
|
+
overlay.querySelector('.repos-dialog-save').addEventListener('click', async () => {
|
|
380
|
+
const name = overlay.querySelector('[name="name"]').value.trim();
|
|
381
|
+
const path = overlay.querySelector('[name="path"]').value.trim();
|
|
382
|
+
const url = overlay.querySelector('[name="url"]').value.trim();
|
|
383
|
+
const groupId = overlay.querySelector('[name="groupId"]').value || null;
|
|
384
|
+
|
|
385
|
+
if (!name) {
|
|
386
|
+
overlay.querySelector('[name="name"]').style.borderColor = 'var(--error)';
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
await ctx.api.addRepo(name, path || null, groupId, url || null);
|
|
392
|
+
overlay.remove();
|
|
393
|
+
await loadData();
|
|
394
|
+
} catch (err) {
|
|
395
|
+
const errEl = overlay.querySelector('.repos-dialog-error') || document.createElement('div');
|
|
396
|
+
errEl.className = 'repos-dialog-error';
|
|
397
|
+
errEl.textContent = err.message;
|
|
398
|
+
overlay.querySelector('.repos-dialog-actions').before(errEl);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
overlay.querySelectorAll('.repos-dialog-input').forEach(input => {
|
|
403
|
+
input.addEventListener('keydown', (e) => {
|
|
404
|
+
if (e.key === 'Enter') overlay.querySelector('.repos-dialog-save').click();
|
|
405
|
+
if (e.key === 'Escape') overlay.remove();
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
root.appendChild(overlay);
|
|
410
|
+
overlay.querySelector('[name="name"]').focus();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ── Add group dialog ────────────────────────────
|
|
414
|
+
function showAddGroupDialog(targetParentId = null) {
|
|
415
|
+
const overlay = document.createElement('div');
|
|
416
|
+
overlay.className = 'repos-dialog-overlay';
|
|
417
|
+
overlay.innerHTML = `
|
|
418
|
+
<div class="repos-dialog">
|
|
419
|
+
<div class="repos-dialog-title">New Group</div>
|
|
420
|
+
<label class="repos-dialog-label">Name *
|
|
421
|
+
<input type="text" class="repos-dialog-input" name="name" placeholder="backend" autocomplete="off">
|
|
422
|
+
</label>
|
|
423
|
+
<label class="repos-dialog-label">Parent Group
|
|
424
|
+
<select class="repos-dialog-input" name="parentId">
|
|
425
|
+
<option value="">None (root)</option>
|
|
426
|
+
${groups.map(g => `<option value="${g.id}"${g.id === targetParentId ? ' selected' : ''}>${escapeHtml(g.name)}</option>`).join('')}
|
|
427
|
+
</select>
|
|
428
|
+
</label>
|
|
429
|
+
<div class="repos-dialog-actions">
|
|
430
|
+
<button class="repos-btn repos-dialog-cancel">Cancel</button>
|
|
431
|
+
<button class="repos-btn repos-dialog-save">Create</button>
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
`;
|
|
435
|
+
|
|
436
|
+
overlay.querySelector('.repos-dialog-cancel').addEventListener('click', () => overlay.remove());
|
|
437
|
+
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
|
438
|
+
|
|
439
|
+
overlay.querySelector('.repos-dialog-save').addEventListener('click', async () => {
|
|
440
|
+
const name = overlay.querySelector('[name="name"]').value.trim();
|
|
441
|
+
const parentId = overlay.querySelector('[name="parentId"]').value || null;
|
|
442
|
+
|
|
443
|
+
if (!name) {
|
|
444
|
+
overlay.querySelector('[name="name"]').style.borderColor = 'var(--error)';
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
await ctx.api.createRepoGroup(name, parentId);
|
|
450
|
+
overlay.remove();
|
|
451
|
+
await loadData();
|
|
452
|
+
} catch (err) {
|
|
453
|
+
console.error('Create group failed:', err);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
overlay.querySelectorAll('.repos-dialog-input').forEach(input => {
|
|
458
|
+
input.addEventListener('keydown', (e) => {
|
|
459
|
+
if (e.key === 'Enter') overlay.querySelector('.repos-dialog-save').click();
|
|
460
|
+
if (e.key === 'Escape') overlay.remove();
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
root.appendChild(overlay);
|
|
465
|
+
overlay.querySelector('[name="name"]').focus();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ── Context menu ─────────────────────────────────
|
|
469
|
+
let ctxMenu = null;
|
|
470
|
+
|
|
471
|
+
function hideContextMenu() {
|
|
472
|
+
if (ctxMenu) { ctxMenu.remove(); ctxMenu = null; }
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function positionMenu(menu, x, y) {
|
|
476
|
+
menu.style.left = x + 'px';
|
|
477
|
+
menu.style.top = y + 'px';
|
|
478
|
+
document.body.appendChild(menu);
|
|
479
|
+
const rect = menu.getBoundingClientRect();
|
|
480
|
+
if (rect.right > window.innerWidth) menu.style.left = (x - rect.width) + 'px';
|
|
481
|
+
if (rect.bottom > window.innerHeight) menu.style.top = (y - rect.height) + 'px';
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function createMenuItem(label, action) {
|
|
485
|
+
const btn = document.createElement('button');
|
|
486
|
+
btn.textContent = label;
|
|
487
|
+
btn.addEventListener('click', () => { hideContextMenu(); action(); });
|
|
488
|
+
return btn;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function collectPathsInGroup(groupId) {
|
|
492
|
+
const paths = getGroupRepos(groupId).filter(r => r.path).map(r => r.path);
|
|
493
|
+
for (const child of getChildGroups(groupId)) {
|
|
494
|
+
paths.push(...collectPathsInGroup(child.id));
|
|
495
|
+
}
|
|
496
|
+
return paths;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function showRepoContextMenu(e, repo) {
|
|
500
|
+
e.preventDefault();
|
|
501
|
+
e.stopPropagation();
|
|
502
|
+
hideContextMenu();
|
|
503
|
+
|
|
504
|
+
ctxMenu = document.createElement('div');
|
|
505
|
+
ctxMenu.className = 'repos-ctx-menu';
|
|
506
|
+
|
|
507
|
+
if (repo.url) {
|
|
508
|
+
ctxMenu.appendChild(createMenuItem('Open in Browser', () => window.open(repo.url, '_blank')));
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (repo.path) {
|
|
512
|
+
ctxMenu.appendChild(createMenuItem('Open in VS Code', () => ctx.api.execCommand('code .', repo.path)));
|
|
513
|
+
ctxMenu.appendChild(createMenuItem('Open in Terminal', () => ctx.api.execCommand('open -a Terminal .', repo.path)));
|
|
514
|
+
ctxMenu.appendChild(createMenuItem('Copy Path', () => navigator.clipboard.writeText(repo.path)));
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
ctxMenu.appendChild(createMenuItem(repo.url ? 'Edit URL' : 'Set URL', async () => {
|
|
518
|
+
const url = prompt('GitHub URL:', repo.url || '');
|
|
519
|
+
if (url === null) return;
|
|
520
|
+
try {
|
|
521
|
+
await ctx.api.updateRepo(repo.id, { url: url.trim() || null });
|
|
522
|
+
await loadData();
|
|
523
|
+
} catch (err) { console.error('Failed to set URL:', err); }
|
|
524
|
+
}));
|
|
525
|
+
|
|
526
|
+
if (groups.length > 0 || repo.groupId) {
|
|
527
|
+
const wrapper = document.createElement('div');
|
|
528
|
+
wrapper.className = 'repos-ctx-submenu-wrapper';
|
|
529
|
+
|
|
530
|
+
const moveBtn = document.createElement('button');
|
|
531
|
+
moveBtn.className = 'repos-ctx-has-submenu';
|
|
532
|
+
moveBtn.innerHTML = 'Move to Group <span class="repos-ctx-arrow">›</span>';
|
|
533
|
+
wrapper.appendChild(moveBtn);
|
|
534
|
+
|
|
535
|
+
const submenu = document.createElement('div');
|
|
536
|
+
submenu.className = 'repos-ctx-submenu';
|
|
537
|
+
|
|
538
|
+
if (repo.groupId) {
|
|
539
|
+
submenu.appendChild(createMenuItem('Ungrouped', async () => {
|
|
540
|
+
await ctx.api.updateRepo(repo.id, { groupId: null });
|
|
541
|
+
await loadData();
|
|
542
|
+
}));
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
for (const group of groups) {
|
|
546
|
+
if (group.id === repo.groupId) continue;
|
|
547
|
+
submenu.appendChild(createMenuItem(group.name, async () => {
|
|
548
|
+
await ctx.api.updateRepo(repo.id, { groupId: group.id });
|
|
549
|
+
await loadData();
|
|
550
|
+
}));
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
wrapper.appendChild(submenu);
|
|
554
|
+
ctxMenu.appendChild(wrapper);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
ctxMenu.appendChild(createMenuItem('Rename', () => { editingId = repo.id; render(); }));
|
|
558
|
+
|
|
559
|
+
const removeBtn = createMenuItem('Remove', async () => {
|
|
560
|
+
try { await ctx.api.deleteRepo(repo.id); await loadData(); }
|
|
561
|
+
catch (err) { console.error('Delete failed:', err); }
|
|
562
|
+
});
|
|
563
|
+
removeBtn.classList.add('repos-ctx-danger');
|
|
564
|
+
ctxMenu.appendChild(removeBtn);
|
|
565
|
+
|
|
566
|
+
positionMenu(ctxMenu, e.clientX, e.clientY);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function showGroupContextMenu(e, group) {
|
|
570
|
+
e.preventDefault();
|
|
571
|
+
e.stopPropagation();
|
|
572
|
+
hideContextMenu();
|
|
573
|
+
|
|
574
|
+
ctxMenu = document.createElement('div');
|
|
575
|
+
ctxMenu.className = 'repos-ctx-menu';
|
|
576
|
+
|
|
577
|
+
ctxMenu.appendChild(createMenuItem('Add Repo Here', () => showAddRepoDialog(group.id)));
|
|
578
|
+
ctxMenu.appendChild(createMenuItem('New Sub-group', () => showAddGroupDialog(group.id)));
|
|
579
|
+
|
|
580
|
+
const paths = collectPathsInGroup(group.id);
|
|
581
|
+
if (paths.length > 0) {
|
|
582
|
+
ctxMenu.appendChild(createMenuItem('Open All in VS Code', () => {
|
|
583
|
+
for (const p of paths) ctx.api.execCommand('code .', p);
|
|
584
|
+
}));
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
ctxMenu.appendChild(createMenuItem('Rename', () => { editingId = group.id; render(); }));
|
|
588
|
+
|
|
589
|
+
const removeBtn = createMenuItem('Delete Group', async () => {
|
|
590
|
+
try { await ctx.api.deleteRepoGroup(group.id); await loadData(); }
|
|
591
|
+
catch (err) { console.error('Delete failed:', err); }
|
|
592
|
+
});
|
|
593
|
+
removeBtn.classList.add('repos-ctx-danger');
|
|
594
|
+
ctxMenu.appendChild(removeBtn);
|
|
595
|
+
|
|
596
|
+
positionMenu(ctxMenu, e.clientX, e.clientY);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
document.addEventListener('click', (e) => {
|
|
600
|
+
if (ctxMenu && !ctxMenu.contains(e.target)) hideContextMenu();
|
|
601
|
+
});
|
|
602
|
+
document.addEventListener('keydown', (e) => {
|
|
603
|
+
if (e.key === 'Escape') hideContextMenu();
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// ── Event delegation ─────────────────────────────
|
|
607
|
+
listEl.addEventListener('click', async (e) => {
|
|
608
|
+
const editBtn = e.target.closest('.repos-edit-btn');
|
|
609
|
+
if (editBtn) {
|
|
610
|
+
e.stopPropagation();
|
|
611
|
+
editingId = editBtn.dataset.id;
|
|
612
|
+
render();
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const deleteBtn = e.target.closest('.repos-delete-btn');
|
|
617
|
+
if (deleteBtn) {
|
|
618
|
+
e.stopPropagation();
|
|
619
|
+
const id = deleteBtn.dataset.id;
|
|
620
|
+
const kind = deleteBtn.dataset.kind;
|
|
621
|
+
try {
|
|
622
|
+
if (kind === 'group') {
|
|
623
|
+
await ctx.api.deleteRepoGroup(id);
|
|
624
|
+
} else {
|
|
625
|
+
await ctx.api.deleteRepo(id);
|
|
626
|
+
}
|
|
627
|
+
await loadData();
|
|
628
|
+
} catch (err) {
|
|
629
|
+
console.error('Delete failed:', err);
|
|
630
|
+
}
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// Drop on empty area = move to root
|
|
636
|
+
listEl.addEventListener('dragover', (e) => {
|
|
637
|
+
if (!dragItem) return;
|
|
638
|
+
e.preventDefault();
|
|
639
|
+
e.dataTransfer.dropEffect = 'move';
|
|
640
|
+
});
|
|
641
|
+
listEl.addEventListener('drop', (e) => handleDrop(e, 'root', null));
|
|
642
|
+
|
|
643
|
+
// Toolbar buttons
|
|
644
|
+
root.querySelector('.repos-add-group-btn').addEventListener('click', () => showAddGroupDialog());
|
|
645
|
+
root.querySelector('.repos-add-repo-btn').addEventListener('click', () => showAddRepoDialog());
|
|
646
|
+
root.querySelector('.repos-refresh-btn').addEventListener('click', loadData);
|
|
647
|
+
|
|
648
|
+
// Search
|
|
649
|
+
let searchTimer = null;
|
|
650
|
+
searchEl.addEventListener('input', () => {
|
|
651
|
+
clearTimeout(searchTimer);
|
|
652
|
+
searchTimer = setTimeout(() => {
|
|
653
|
+
searchQuery = searchEl.value.trim();
|
|
654
|
+
render();
|
|
655
|
+
}, 200);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// ── Initial load ─────────────────────────────────
|
|
659
|
+
loadData();
|
|
660
|
+
|
|
661
|
+
return root;
|
|
662
|
+
},
|
|
663
|
+
});
|