code-context-control 2.28.0__py3-none-any.whl
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.
- cli/__init__.py +1 -0
- cli/_hook_utils.py +99 -0
- cli/c3.py +6152 -0
- cli/commands/__init__.py +1 -0
- cli/commands/common.py +312 -0
- cli/commands/parser.py +286 -0
- cli/docs.html +3178 -0
- cli/edits.html +878 -0
- cli/hook_auto_snapshot.py +142 -0
- cli/hook_c3_signal.py +61 -0
- cli/hook_c3read.py +116 -0
- cli/hook_edit_ledger.py +213 -0
- cli/hook_edit_unlock.py +170 -0
- cli/hook_filter.py +130 -0
- cli/hook_ghost_files.py +238 -0
- cli/hook_pretool_enforce.py +334 -0
- cli/hook_read.py +200 -0
- cli/hook_session_stats.py +62 -0
- cli/hook_terse_advisor.py +190 -0
- cli/hub.html +3764 -0
- cli/hub_server.py +1619 -0
- cli/mcp_proxy.py +428 -0
- cli/mcp_server.py +660 -0
- cli/server.py +2985 -0
- cli/tools/__init__.py +4 -0
- cli/tools/_helpers.py +65 -0
- cli/tools/agent.py +1165 -0
- cli/tools/compress.py +215 -0
- cli/tools/delegate.py +1184 -0
- cli/tools/edit.py +313 -0
- cli/tools/edits.py +118 -0
- cli/tools/filter.py +285 -0
- cli/tools/impact.py +163 -0
- cli/tools/memory.py +469 -0
- cli/tools/read.py +224 -0
- cli/tools/search.py +337 -0
- cli/tools/session.py +95 -0
- cli/tools/shell.py +193 -0
- cli/tools/status.py +306 -0
- cli/tools/validate.py +310 -0
- cli/ui/api.js +36 -0
- cli/ui/app.js +207 -0
- cli/ui/components/chat.js +758 -0
- cli/ui/components/dashboard.js +689 -0
- cli/ui/components/edits.js +220 -0
- cli/ui/components/instructions.js +481 -0
- cli/ui/components/memory.js +626 -0
- cli/ui/components/sessions.js +606 -0
- cli/ui/components/settings.js +1404 -0
- cli/ui/components/sidebar.js +156 -0
- cli/ui/icons.js +51 -0
- cli/ui/shared.js +119 -0
- cli/ui/theme.js +22 -0
- cli/ui.html +168 -0
- cli/ui_legacy.html +6797 -0
- cli/ui_nano.html +503 -0
- code_context_control-2.28.0.dist-info/METADATA +248 -0
- code_context_control-2.28.0.dist-info/RECORD +150 -0
- code_context_control-2.28.0.dist-info/WHEEL +5 -0
- code_context_control-2.28.0.dist-info/entry_points.txt +4 -0
- code_context_control-2.28.0.dist-info/licenses/LICENSE +201 -0
- code_context_control-2.28.0.dist-info/top_level.txt +5 -0
- core/__init__.py +75 -0
- core/config.py +269 -0
- core/ide.py +188 -0
- oracle/__init__.py +1 -0
- oracle/config.py +75 -0
- oracle/oracle.html +3900 -0
- oracle/oracle_server.py +663 -0
- oracle/services/__init__.py +1 -0
- oracle/services/c3_bridge.py +210 -0
- oracle/services/chat_engine.py +1103 -0
- oracle/services/chat_store.py +155 -0
- oracle/services/cross_memory.py +154 -0
- oracle/services/federated_graph.py +463 -0
- oracle/services/health_checker.py +117 -0
- oracle/services/insight_engine.py +307 -0
- oracle/services/memory_reader.py +106 -0
- oracle/services/memory_writer.py +182 -0
- oracle/services/ollama_bridge.py +332 -0
- oracle/services/project_scanner.py +87 -0
- oracle/services/review_agent.py +206 -0
- services/__init__.py +1 -0
- services/activity_log.py +93 -0
- services/agent_base.py +124 -0
- services/agents.py +1529 -0
- services/auto_memory.py +407 -0
- services/bench/__init__.py +6 -0
- services/bench/external/__init__.py +29 -0
- services/bench/external/aider_polyglot.py +405 -0
- services/bench/external/swe_bench.py +485 -0
- services/benchmark_dashboard.py +596 -0
- services/claude_md.py +785 -0
- services/compressor.py +592 -0
- services/context_snapshot.py +356 -0
- services/conversation_store.py +870 -0
- services/doc_index.py +537 -0
- services/e2e_benchmark.py +2884 -0
- services/e2e_evaluator.py +396 -0
- services/e2e_tasks.py +743 -0
- services/edit_ledger.py +459 -0
- services/embedding_index.py +341 -0
- services/error_reporting.py +123 -0
- services/file_memory.py +734 -0
- services/hub_service.py +585 -0
- services/indexer.py +712 -0
- services/memory.py +318 -0
- services/memory_consolidator.py +538 -0
- services/memory_graph.py +382 -0
- services/memory_grounder.py +304 -0
- services/memory_scorer.py +246 -0
- services/metrics.py +86 -0
- services/notifications.py +209 -0
- services/ollama_client.py +201 -0
- services/output_filter.py +488 -0
- services/parser.py +1238 -0
- services/project_manager.py +579 -0
- services/protocol.py +306 -0
- services/proxy_state.py +152 -0
- services/retrieval_broker.py +129 -0
- services/router.py +414 -0
- services/runtime.py +326 -0
- services/session_benchmark.py +1945 -0
- services/session_manager.py +1026 -0
- services/session_preloader.py +251 -0
- services/text_index.py +90 -0
- services/tool_classifier.py +176 -0
- services/transcript_index.py +340 -0
- services/validation_cache.py +155 -0
- services/vector_store.py +299 -0
- services/version_tracker.py +271 -0
- services/watcher.py +192 -0
- tui/__init__.py +0 -0
- tui/backend.py +59 -0
- tui/main.py +145 -0
- tui/screens/__init__.py +1 -0
- tui/screens/benchmark_view.py +109 -0
- tui/screens/claudemd_view.py +46 -0
- tui/screens/compress_view.py +52 -0
- tui/screens/index_view.py +74 -0
- tui/screens/init_view.py +82 -0
- tui/screens/mcp_view.py +73 -0
- tui/screens/optimize_view.py +41 -0
- tui/screens/pipe_view.py +46 -0
- tui/screens/projects_view.py +355 -0
- tui/screens/search_view.py +55 -0
- tui/screens/session_view.py +143 -0
- tui/screens/stats.py +158 -0
- tui/screens/ui_view.py +54 -0
- tui/theme.tcss +335 -0
cli/hub.html
ADDED
|
@@ -0,0 +1,3764 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>C3 Project Hub</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0a0a0b;
|
|
10
|
+
--bg2: #141416;
|
|
11
|
+
--bg3: #1c1c1f;
|
|
12
|
+
--border: #27272a;
|
|
13
|
+
--accent: #3b82f6;
|
|
14
|
+
--green: #22c55e;
|
|
15
|
+
--red: #ef4444;
|
|
16
|
+
--yellow: #eab308;
|
|
17
|
+
--purple: #a855f7;
|
|
18
|
+
--muted: #3f3f46;
|
|
19
|
+
--text: #fafafa;
|
|
20
|
+
--text2: #71717a;
|
|
21
|
+
}
|
|
22
|
+
body[data-theme="light"] {
|
|
23
|
+
--bg: #fafafa;
|
|
24
|
+
--bg2: #ffffff;
|
|
25
|
+
--bg3: #f4f4f5;
|
|
26
|
+
--border: #e4e4e7;
|
|
27
|
+
--accent: #2563eb;
|
|
28
|
+
--green: #16a34a;
|
|
29
|
+
--red: #dc2626;
|
|
30
|
+
--yellow: #ca8a04;
|
|
31
|
+
--purple: #9333ea;
|
|
32
|
+
--muted: #d4d4d8;
|
|
33
|
+
--text: #09090b;
|
|
34
|
+
--text2: #71717a;
|
|
35
|
+
}
|
|
36
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
37
|
+
body { font-family: 'Inter', system-ui, -apple-system, sans-serif; font-size: 14px; background: var(--bg); color: var(--text); min-height: 100vh; }
|
|
38
|
+
|
|
39
|
+
/* ── Header ── */
|
|
40
|
+
header {
|
|
41
|
+
background: var(--bg2); border-bottom: 1px solid var(--border);
|
|
42
|
+
padding: 0 24px; height: 48px; display: flex; align-items: center;
|
|
43
|
+
justify-content: space-between; position: sticky; top: 0; z-index: 10;
|
|
44
|
+
}
|
|
45
|
+
.logo { font-size: 16px; font-weight: 700; color: var(--text); letter-spacing: .5px; }
|
|
46
|
+
.logo small { color: var(--text2); font-weight: 400; font-size: 12px; margin-left: 8px; }
|
|
47
|
+
.header-right { display: flex; align-items: center; gap: 6px; }
|
|
48
|
+
.hub-version { font-size: 11px; color: var(--text2); padding: 3px 10px; background: var(--bg3); border: 1px solid var(--border); border-radius: 9999px; }
|
|
49
|
+
.hub-port { font-size: 11px; color: var(--accent); padding: 3px 10px; background: var(--bg3); border: 1px solid var(--border); border-radius: 9999px; cursor: default; }
|
|
50
|
+
.badge { background: var(--bg3); border: 1px solid var(--border); border-radius: 9999px; padding: 3px 12px; font-size: 12px; color: var(--text2); }
|
|
51
|
+
.badge .dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--muted); margin-right: 5px; }
|
|
52
|
+
.badge.has-active .dot { background: var(--green); }
|
|
53
|
+
|
|
54
|
+
/* ── Buttons ── */
|
|
55
|
+
.btn { border: none; border-radius: 6px; padding: 6px 14px; font-size: 12px; font-weight: 500; cursor: pointer; transition: all .15s ease; white-space: nowrap; }
|
|
56
|
+
.btn:hover:not(:disabled) { opacity: .85; }
|
|
57
|
+
.btn:active { transform: scale(.97); }
|
|
58
|
+
.btn:disabled { opacity: .4; cursor: default; }
|
|
59
|
+
.btn-xs { padding: 4px 10px; font-size: 11px; }
|
|
60
|
+
.btn-sm { padding: 5px 12px; font-size: 12px; }
|
|
61
|
+
.btn-primary { background: var(--accent); color: #fff; }
|
|
62
|
+
.btn-danger { background: transparent; border: 1px solid var(--red); color: var(--red); }
|
|
63
|
+
.btn-danger:hover:not(:disabled) { background: var(--red); color: #fff; opacity: 1; }
|
|
64
|
+
.btn-ghost { background: transparent; border: 1px solid var(--border); color: var(--text2); }
|
|
65
|
+
.btn-ghost:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); opacity: 1; }
|
|
66
|
+
.btn-green { background: transparent; border: 1px solid var(--green); color: var(--green); }
|
|
67
|
+
.btn-green:hover:not(:disabled) { background: var(--green); color: #fff; opacity: 1; }
|
|
68
|
+
.btn-yellow { background: transparent; border: 1px solid var(--yellow); color: var(--yellow); }
|
|
69
|
+
.btn-yellow:hover:not(:disabled) { background: var(--yellow); color: #000; opacity: 1; }
|
|
70
|
+
.btn-purple { background: transparent; border: 1px solid var(--purple); color: var(--purple); }
|
|
71
|
+
.btn-purple:hover:not(:disabled) { background: var(--purple); color: #fff; opacity: 1; }
|
|
72
|
+
.btn-refresh { background: transparent; border: 1px solid var(--border); color: var(--text2); border-radius: 6px; padding: 4px 12px; font-size: 13px; cursor: pointer; transition: all .15s ease; }
|
|
73
|
+
.btn-refresh:hover { border-color: var(--accent); color: var(--accent); }
|
|
74
|
+
.btn-restart { background: transparent; border: 1px solid var(--border); color: var(--text2); border-radius: 6px; padding: 4px 12px; font-size: 13px; cursor: pointer; transition: all .15s ease; }
|
|
75
|
+
.btn-restart:hover { border-color: var(--yellow); color: var(--yellow); }
|
|
76
|
+
.btn-restart:disabled { opacity: .4; cursor: default; }
|
|
77
|
+
.btn-settings { background: transparent; border: 1px solid var(--border); color: var(--text2); border-radius: 6px; padding: 4px 10px; font-size: 13px; cursor: pointer; transition: all .15s ease; }
|
|
78
|
+
.btn-settings:hover { border-color: var(--accent); color: var(--accent); }
|
|
79
|
+
.btn-view, .btn-theme { background: transparent; border: 1px solid var(--border); color: var(--text2); border-radius: 9999px; padding: 4px 10px; font-size: 12px; cursor: pointer; transition: all .15s ease; }
|
|
80
|
+
.btn-view.active, .btn-view:hover, .btn-theme:hover { border-color: var(--accent); color: var(--accent); }
|
|
81
|
+
|
|
82
|
+
/* ── Sidebar ── */
|
|
83
|
+
main { display: flex; margin: 0; padding: 0; }
|
|
84
|
+
.main-content { flex: 1; min-width: 0; max-width: 1100px; margin: 0 auto; padding: 20px 24px; }
|
|
85
|
+
body.with-session-drawer .main-content { padding-bottom: 320px; }
|
|
86
|
+
|
|
87
|
+
.sidebar { width: 220px; min-width: 220px; background: var(--bg2); border-right: 1px solid var(--border); height: calc(100vh - 48px); position: sticky; top: 48px; overflow-y: auto; transition: width .2s ease, min-width .2s ease, opacity .2s ease; flex-shrink: 0; }
|
|
88
|
+
main.sidebar-collapsed .sidebar { width: 0; min-width: 0; overflow: hidden; border-right: none; opacity: 0; }
|
|
89
|
+
.sidebar-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 14px 10px; border-bottom: 1px solid var(--border); }
|
|
90
|
+
.sidebar-title { font-size: 11px; font-weight: 600; color: var(--text2); text-transform: uppercase; letter-spacing: .6px; }
|
|
91
|
+
.sidebar-toggle { background: transparent; border: none; color: var(--text2); cursor: pointer; font-size: 12px; padding: 2px 6px; border-radius: 4px; transition: all .15s ease; }
|
|
92
|
+
.sidebar-toggle:hover { color: var(--accent); background: var(--bg3); }
|
|
93
|
+
.sidebar-expand { display: none; position: fixed; left: 0; top: 56px; background: var(--bg2); border: 1px solid var(--border); border-left: none; border-radius: 0 6px 6px 0; color: var(--text2); cursor: pointer; padding: 6px 8px; font-size: 12px; z-index: 5; transition: all .15s ease; }
|
|
94
|
+
.sidebar-expand:hover { color: var(--accent); border-color: var(--accent); }
|
|
95
|
+
main.sidebar-collapsed .sidebar-expand { display: block; }
|
|
96
|
+
.sidebar-nav { padding: 8px 0; }
|
|
97
|
+
.sidebar-item { display: flex; align-items: center; justify-content: space-between; padding: 7px 14px; font-size: 12px; color: var(--text2); cursor: pointer; transition: all .12s ease; border-left: 2px solid transparent; user-select: none; }
|
|
98
|
+
.sidebar-item:hover { background: var(--bg3); color: var(--text); }
|
|
99
|
+
.sidebar-item.active { background: var(--bg3); color: var(--accent); border-left-color: var(--accent); }
|
|
100
|
+
.sidebar-item-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
101
|
+
.sidebar-item-count { font-size: 10px; color: var(--text2); background: var(--bg3); border: 1px solid var(--border); border-radius: 9999px; padding: 1px 7px; flex-shrink: 0; margin-left: 6px; }
|
|
102
|
+
.sidebar-item.active .sidebar-item-count { color: var(--accent); border-color: rgba(59,130,246,.25); background: rgba(59,130,246,.08); }
|
|
103
|
+
.sidebar-divider { height: 1px; background: var(--border); margin: 6px 14px; }
|
|
104
|
+
.sidebar-item[data-depth="1"] { padding-left: 28px; }
|
|
105
|
+
.sidebar-item[data-depth="2"] { padding-left: 42px; }
|
|
106
|
+
.sidebar-item[data-depth="3"] { padding-left: 56px; }
|
|
107
|
+
.sidebar-folder-toggle { display: inline-flex; align-items: center; justify-content: center; width: 14px; font-size: 9px; margin-right: 3px; color: var(--text2); flex-shrink: 0; cursor: pointer; }
|
|
108
|
+
@media (max-width: 900px) { .sidebar { position: fixed; left: 0; top: 48px; z-index: 15; box-shadow: 4px 0 16px rgba(0,0,0,.2); } }
|
|
109
|
+
@media (max-width: 640px) { .sidebar { width: 0; min-width: 0; overflow: hidden; border-right: none; opacity: 0; } }
|
|
110
|
+
|
|
111
|
+
/* ── Add card ── */
|
|
112
|
+
.add-card { background: var(--bg2); border: 1px solid var(--border); border-radius: 6px; padding: 16px 20px; margin-bottom: 20px; box-shadow: 0 1px 2px rgba(0,0,0,.05); }
|
|
113
|
+
.add-card h2 { font-size: 11px; font-weight: 600; color: var(--text2); text-transform: uppercase; letter-spacing: .6px; margin-bottom: 10px; }
|
|
114
|
+
.add-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
|
115
|
+
.add-row input { background: var(--bg3); border: 1px solid var(--border); border-radius: 6px; color: var(--text); padding: 7px 12px; font-size: 13px; outline: none; transition: border-color .15s ease; }
|
|
116
|
+
.add-row input:focus { border-color: var(--accent); }
|
|
117
|
+
.input-path { flex: 1; min-width: 220px; }
|
|
118
|
+
.input-name { width: 160px; }
|
|
119
|
+
|
|
120
|
+
/* ── Summary bar ── */
|
|
121
|
+
.summary-bar { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; flex-wrap: wrap; }
|
|
122
|
+
.summary-bar h1 { font-size: 14px; font-weight: 600; color: var(--text); flex: 1; }
|
|
123
|
+
.search-input { background: var(--bg3); border: 1px solid var(--border); border-radius: 6px; color: var(--text); padding: 6px 12px; font-size: 12px; outline: none; width: 200px; transition: border-color .15s ease; }
|
|
124
|
+
.search-input:focus { border-color: var(--accent); }
|
|
125
|
+
.filter-row { display: flex; gap: 4px; }
|
|
126
|
+
.filter-btn { background: transparent; border: 1px solid var(--border); border-radius: 9999px; color: var(--text2); padding: 4px 14px; font-size: 12px; cursor: pointer; transition: all .15s ease; }
|
|
127
|
+
.filter-btn.active, .filter-btn:hover { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
128
|
+
.view-row { display: flex; gap: 4px; }
|
|
129
|
+
|
|
130
|
+
/* ── Project cards ── */
|
|
131
|
+
#projects-grid { display: flex; flex-direction: column; gap: 10px; }
|
|
132
|
+
#projects-grid.grid-view { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 14px; align-items: start; }
|
|
133
|
+
|
|
134
|
+
.project-card { background: var(--bg2); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; transition: all .15s ease; box-shadow: 0 1px 2px rgba(0,0,0,.05); }
|
|
135
|
+
.project-card:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,.1); border-color: color-mix(in srgb, var(--border) 70%, var(--accent)); }
|
|
136
|
+
#projects-grid.grid-view .project-card { height: 100%; }
|
|
137
|
+
.project-card.is-active { border-left: 3px solid var(--green); }
|
|
138
|
+
.project-card.is-launching { border-left: 3px solid var(--yellow); }
|
|
139
|
+
.project-card.is-launching .status-pill.stopped { color: var(--yellow); border-color: rgba(234,179,8,.35); background: rgba(234,179,8,.07); }
|
|
140
|
+
|
|
141
|
+
/* Zone 1: Identity */
|
|
142
|
+
.card-zone-identity { padding: 14px 18px 10px; }
|
|
143
|
+
.project-name-row { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; flex-wrap: wrap; }
|
|
144
|
+
.project-name { font-size: 14px; font-weight: 600; color: var(--text); }
|
|
145
|
+
.status-pill { font-size: 10px; font-weight: 600; padding: 2px 8px; border-radius: 9999px; letter-spacing: .3px; flex-shrink: 0; }
|
|
146
|
+
.status-pill.active { background: rgba(34,197,94,.1); color: var(--green); border: 1px solid rgba(34,197,94,.25); }
|
|
147
|
+
.status-pill.stopped { background: var(--bg3); color: var(--text2); border: 1px solid var(--border); }
|
|
148
|
+
body[data-theme="light"] .status-pill.active { background: rgba(22,163,74,.08); border-color: rgba(22,163,74,.2); }
|
|
149
|
+
|
|
150
|
+
.ver-badge { font-size: 10px; padding: 1px 7px; border-radius: 9999px; border: 1px solid var(--border); color: var(--text2); background: var(--bg3); flex-shrink: 0; }
|
|
151
|
+
.ver-badge.outdated { border-color: var(--yellow); color: var(--yellow); background: rgba(234,179,8,.08); }
|
|
152
|
+
.ver-badge.uninit { border-color: var(--red); color: var(--red); background: rgba(239,68,68,.08); }
|
|
153
|
+
body[data-theme="light"] .ver-badge.outdated { background: rgba(202,138,4,.06); }
|
|
154
|
+
body[data-theme="light"] .ver-badge.uninit { background: rgba(220,38,38,.06); }
|
|
155
|
+
|
|
156
|
+
.mcp-badge { font-size: 10px; padding: 1px 7px; border-radius: 9999px; flex-shrink: 0; }
|
|
157
|
+
.mcp-badge.ok { background: rgba(59,130,246,.08); border: 1px solid rgba(59,130,246,.2); color: var(--accent); }
|
|
158
|
+
.mcp-badge.none { background: var(--bg3); border: 1px solid var(--border); color: var(--text2); }
|
|
159
|
+
body[data-theme="light"] .mcp-badge.ok { background: rgba(37,99,235,.06); border-color: rgba(37,99,235,.15); }
|
|
160
|
+
|
|
161
|
+
.budget-badge { font-size: 10px; padding: 1px 8px; border-radius: 9999px; border: 1px solid var(--border); color: var(--text2); background: var(--bg3); display: flex; align-items: center; gap: 6px; }
|
|
162
|
+
.budget-bar { width: 36px; height: 4px; background: var(--muted); border-radius: 2px; overflow: hidden; display: inline-block; vertical-align: middle; }
|
|
163
|
+
.budget-fill { height: 100%; background: var(--green); transition: width .3s ease; }
|
|
164
|
+
.budget-fill.warning { background: var(--yellow); }
|
|
165
|
+
.budget-fill.critical { background: var(--red); }
|
|
166
|
+
|
|
167
|
+
.project-path { font-size: 11px; color: var(--text2); font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace; margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 680px; }
|
|
168
|
+
#projects-grid.grid-view .project-path { white-space: normal; word-break: break-word; max-width: none; }
|
|
169
|
+
|
|
170
|
+
.tags-row { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; min-height: 16px; }
|
|
171
|
+
.tag-chip { font-size: 10px; padding: 1px 7px; border-radius: 9999px; background: var(--bg3); border: 1px solid var(--border); color: var(--text2); }
|
|
172
|
+
.note-icon { font-size: 12px; cursor: default; color: var(--yellow); }
|
|
173
|
+
|
|
174
|
+
/* Zone 2: Metadata */
|
|
175
|
+
.card-zone-meta { padding: 8px 18px; border-top: 1px solid var(--border); }
|
|
176
|
+
.info-row { display: flex; gap: 14px; flex-wrap: wrap; }
|
|
177
|
+
#projects-grid.grid-view .info-row { gap: 10px 12px; }
|
|
178
|
+
.info-item { font-size: 11px; color: var(--text2); display: flex; gap: 3px; align-items: center; }
|
|
179
|
+
.info-label { color: var(--text2); opacity: .6; }
|
|
180
|
+
.port-link { color: var(--accent); cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
|
|
181
|
+
.mcp-expand-link { color: var(--text2); cursor: pointer; font-size: 11px; text-decoration: none; border: none; background: transparent; padding: 0; }
|
|
182
|
+
.mcp-expand-link:hover { color: var(--accent); }
|
|
183
|
+
|
|
184
|
+
/* Zone 3: Actions */
|
|
185
|
+
.card-zone-actions { display: flex; gap: 5px; flex-wrap: wrap; padding: 10px 18px; border-top: 1px solid var(--border); align-items: center; }
|
|
186
|
+
.card-actions-primary { display: flex; gap: 5px; flex: 1; flex-wrap: wrap; }
|
|
187
|
+
.card-actions-secondary { display: flex; gap: 5px; flex-wrap: wrap; }
|
|
188
|
+
|
|
189
|
+
/* ── MCP Expand ── */
|
|
190
|
+
.expand-cfg-path { font-size: 10px; font-family: monospace; color: var(--muted); margin-bottom: 8px; word-break: break-all; }
|
|
191
|
+
.mcp-table { width: 100%; border-collapse: collapse; font-size: 11px; }
|
|
192
|
+
.mcp-table th { color: var(--muted); font-weight: 600; text-align: left; padding: 3px 8px 5px 0; border-bottom: 1px solid var(--border); text-transform: uppercase; font-size: 10px; letter-spacing: .4px; }
|
|
193
|
+
.mcp-table td { color: var(--text2); padding: 4px 8px 4px 0; border-bottom: 1px solid var(--border); font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace; }
|
|
194
|
+
.mcp-table td:first-child { color: var(--accent); font-weight: 600; }
|
|
195
|
+
.no-mcp-msg { font-size: 12px; color: var(--text2); }
|
|
196
|
+
|
|
197
|
+
/* ── Empty state ── */
|
|
198
|
+
.empty-state { text-align: center; padding: 50px 20px; color: var(--text2); }
|
|
199
|
+
.empty-state .icon { font-size: 40px; margin-bottom: 10px; }
|
|
200
|
+
.empty-state p { font-size: 13px; }
|
|
201
|
+
|
|
202
|
+
/* ── Modal ── */
|
|
203
|
+
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.5); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); z-index: 50; display: flex; align-items: center; justify-content: center; padding: 20px; opacity: 0; transition: opacity .15s ease; pointer-events: none; }
|
|
204
|
+
.modal-backdrop.hidden { display: none; }
|
|
205
|
+
.modal-backdrop.modal-visible { opacity: 1; pointer-events: auto; }
|
|
206
|
+
.modal { background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; width: 100%; max-width: 580px; display: flex; flex-direction: column; max-height: 90vh; box-shadow: 0 16px 48px rgba(0,0,0,.2); transform: scale(.95); opacity: 0; transition: transform .15s ease, opacity .15s ease; }
|
|
207
|
+
.modal-backdrop.modal-visible .modal { transform: scale(1); opacity: 1; }
|
|
208
|
+
.modal.modal-lg { max-width: 1080px; }
|
|
209
|
+
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; border-bottom: 1px solid var(--border); flex-shrink: 0; }
|
|
210
|
+
.modal-header h3 { font-size: 15px; font-weight: 600; color: var(--text); }
|
|
211
|
+
.modal-close { background: transparent; border: none; color: var(--text2); cursor: pointer; font-size: 18px; line-height: 1; padding: 4px 8px; border-radius: 6px; transition: all .15s ease; }
|
|
212
|
+
.modal-close:hover { color: var(--text); background: var(--bg3); }
|
|
213
|
+
.modal-body { padding: 18px 20px; flex: 1; overflow-y: auto; }
|
|
214
|
+
.modal-footer { padding: 14px 20px; border-top: 1px solid var(--border); display: flex; gap: 8px; justify-content: flex-end; flex-shrink: 0; }
|
|
215
|
+
|
|
216
|
+
.form-group { margin-bottom: 13px; }
|
|
217
|
+
.form-label { font-size: 11px; font-weight: 600; color: var(--text2); text-transform: uppercase; letter-spacing: .4px; display: block; margin-bottom: 5px; }
|
|
218
|
+
.form-input, .form-select, .form-textarea {
|
|
219
|
+
background: var(--bg3); border: 1px solid var(--border); border-radius: 6px;
|
|
220
|
+
color: var(--text); padding: 7px 12px; font-size: 13px; outline: none; width: 100%; transition: border-color .15s ease;
|
|
221
|
+
}
|
|
222
|
+
.form-input:focus, .form-select:focus, .form-textarea:focus { border-color: var(--accent); }
|
|
223
|
+
.form-textarea { resize: vertical; min-height: 80px; font-family: inherit; }
|
|
224
|
+
.form-path { font-size: 12px; font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace; color: var(--text2); background: var(--bg3); border: 1px solid var(--border); border-radius: 6px; padding: 7px 12px; word-break: break-all; }
|
|
225
|
+
|
|
226
|
+
.cmd-output { background: var(--bg3); border: 1px solid var(--border); border-radius: 6px; font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace; font-size: 12px; color: var(--text); padding: 12px; min-height: 120px; max-height: 240px; overflow-y: auto; white-space: pre-wrap; word-break: break-word; margin-top: 10px; }
|
|
227
|
+
.cmd-output.hidden { display: none; }
|
|
228
|
+
.cmd-output.success { border-color: rgba(34,197,94,.3); }
|
|
229
|
+
.cmd-output.failure { border-color: var(--red); }
|
|
230
|
+
/* ── MCP Modal — Tabbed Layout ── */
|
|
231
|
+
.mcp-header-bar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 16px; }
|
|
232
|
+
.mcp-header-path { font-size: 12px; font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace; color: var(--text2); background: var(--bg3); border: 1px solid var(--border); border-radius: 6px; padding: 6px 12px; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
233
|
+
.mcp-header-pills { display: flex; gap: 6px; flex-wrap: wrap; flex-shrink: 0; }
|
|
234
|
+
|
|
235
|
+
.mcp-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: 16px; }
|
|
236
|
+
.mcp-tab { padding: 8px 16px; font-size: 12px; font-weight: 500; color: var(--text2); cursor: pointer; border-bottom: 2px solid transparent; transition: all .15s ease; background: transparent; border-top: none; border-left: none; border-right: none; white-space: nowrap; }
|
|
237
|
+
.mcp-tab:hover { color: var(--text); }
|
|
238
|
+
.mcp-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
239
|
+
.mcp-tab-panel { display: none; }
|
|
240
|
+
.mcp-tab-panel.active { display: block; }
|
|
241
|
+
|
|
242
|
+
.mcp-section { border: 1px solid var(--border); border-radius: 6px; background: var(--bg3); padding: 14px; margin-bottom: 14px; }
|
|
243
|
+
.mcp-section:last-child { margin-bottom: 0; }
|
|
244
|
+
.mcp-section h4 { font-size: 13px; font-weight: 600; color: var(--text); margin-bottom: 8px; }
|
|
245
|
+
.mcp-section-head { display: flex; justify-content: space-between; gap: 12px; align-items: center; margin-bottom: 10px; }
|
|
246
|
+
.mcp-section-subtitle { font-size: 12px; color: var(--text2); }
|
|
247
|
+
|
|
248
|
+
/* Status overview — compact key-value pairs */
|
|
249
|
+
.mcp-status-row { display: flex; gap: 20px; flex-wrap: wrap; padding: 4px 0; }
|
|
250
|
+
.mcp-status-item { display: flex; align-items: center; gap: 6px; font-size: 12px; }
|
|
251
|
+
.mcp-status-label { color: var(--text2); }
|
|
252
|
+
.mcp-status-value { color: var(--text); font-weight: 500; }
|
|
253
|
+
.mcp-status-value.mono { font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace; font-size: 11px; }
|
|
254
|
+
.mcp-config-path { font-size: 11px; font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace; color: var(--text2); margin-top: 8px; padding: 6px 10px; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; word-break: break-all; }
|
|
255
|
+
|
|
256
|
+
.mcp-pill-row { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
257
|
+
.mcp-pill { font-size: 11px; padding: 3px 9px; border-radius: 999px; border: 1px solid var(--border); background: var(--bg2); color: var(--text2); }
|
|
258
|
+
.mcp-pill.ok { color: var(--green); border-color: rgba(34,197,94,.25); }
|
|
259
|
+
.mcp-pill.warn { color: var(--yellow); border-color: rgba(234,179,8,.3); }
|
|
260
|
+
.mcp-pill.c3 { color: var(--accent); border-color: rgba(59,130,246,.25); }
|
|
261
|
+
|
|
262
|
+
/* Controls */
|
|
263
|
+
.mcp-inline-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
264
|
+
.mcp-actions-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
|
|
265
|
+
.mcp-helper { font-size: 11px; color: var(--text2); line-height: 1.45; margin-top: 8px; }
|
|
266
|
+
|
|
267
|
+
/* Server list */
|
|
268
|
+
.mcp-server-list { display: flex; flex-direction: column; gap: 8px; }
|
|
269
|
+
.mcp-server-card { border: 1px solid var(--border); border-radius: 6px; background: var(--bg2); padding: 12px; transition: border-color .15s ease; }
|
|
270
|
+
.mcp-server-card:hover { border-color: color-mix(in srgb, var(--border) 60%, var(--accent)); }
|
|
271
|
+
.mcp-server-card.is-c3 { border-left: 3px solid var(--accent); }
|
|
272
|
+
.mcp-server-card-head { display: flex; justify-content: space-between; gap: 12px; align-items: center; margin-bottom: 6px; }
|
|
273
|
+
.mcp-server-name { font-size: 13px; font-weight: 600; color: var(--text); display: flex; align-items: center; gap: 6px; }
|
|
274
|
+
.mcp-server-name .c3-tag { font-size: 9px; font-weight: 700; padding: 1px 6px; border-radius: 9999px; background: rgba(59,130,246,.1); color: var(--accent); border: 1px solid rgba(59,130,246,.2); letter-spacing: .3px; }
|
|
275
|
+
.mcp-server-meta { display: grid; grid-template-columns: 80px 1fr; gap: 3px 10px; font-size: 12px; color: var(--text2); }
|
|
276
|
+
.mcp-server-meta dt { color: var(--text2); opacity: .7; }
|
|
277
|
+
.mcp-server-meta dd { color: var(--text); font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace; font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin: 0; }
|
|
278
|
+
.mcp-server-empty { text-align: center; padding: 24px 16px; color: var(--text2); font-size: 13px; }
|
|
279
|
+
.mcp-server-empty-icon { font-size: 28px; margin-bottom: 8px; opacity: .5; }
|
|
280
|
+
.mcp-server-empty-cta { margin-top: 10px; }
|
|
281
|
+
|
|
282
|
+
/* Add server — collapsible */
|
|
283
|
+
.mcp-add-toggle { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text2); cursor: pointer; background: transparent; border: 1px dashed var(--border); border-radius: 6px; padding: 10px 14px; width: 100%; text-align: left; transition: all .15s ease; }
|
|
284
|
+
.mcp-add-toggle:hover { border-color: var(--accent); color: var(--accent); }
|
|
285
|
+
.mcp-add-form { border: 1px solid var(--border); border-radius: 6px; background: var(--bg3); padding: 14px; margin-top: 8px; }
|
|
286
|
+
.mcp-add-form.hidden { display: none; }
|
|
287
|
+
|
|
288
|
+
/* CLI Preview */
|
|
289
|
+
.mcp-cli-list { display: flex; flex-direction: column; gap: 10px; }
|
|
290
|
+
.mcp-cli-card { border: 1px solid var(--border); border-radius: 6px; background: var(--bg2); padding: 10px; }
|
|
291
|
+
.mcp-cli-card h5 { font-size: 12px; color: var(--accent); margin-bottom: 6px; }
|
|
292
|
+
.mcp-cli-card p { font-size: 12px; color: var(--text2); margin-bottom: 8px; }
|
|
293
|
+
.mcp-cli-preview { font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace; font-size: 12px; color: var(--text); white-space: pre-wrap; word-break: break-word; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 10px; position: relative; cursor: pointer; transition: border-color .15s ease; }
|
|
294
|
+
.mcp-cli-preview:hover { border-color: var(--accent); }
|
|
295
|
+
.mcp-cli-preview::after { content: 'Click to copy'; position: absolute; top: 6px; right: 8px; font-size: 10px; color: var(--text2); opacity: 0; transition: opacity .15s ease; }
|
|
296
|
+
.mcp-cli-preview:hover::after { opacity: 1; }
|
|
297
|
+
.mcp-cli-preview.copied::after { content: 'Copied!'; color: var(--green); opacity: 1; }
|
|
298
|
+
|
|
299
|
+
/* Tool categories */
|
|
300
|
+
.mcp-tool-badges { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
301
|
+
.mcp-tool-badge { font-size: 11px; color: var(--text2); border: 1px solid var(--border); border-radius: 999px; padding: 3px 8px; background: var(--bg2); }
|
|
302
|
+
.mcp-tool-cat { margin-bottom: 12px; }
|
|
303
|
+
.mcp-tool-cat:last-child { margin-bottom: 0; }
|
|
304
|
+
|
|
305
|
+
/* Config tab — health grid + component list */
|
|
306
|
+
.config-health-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
|
|
307
|
+
.config-health-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg2); font-size: 12px; }
|
|
308
|
+
.config-health-label { color: var(--text2); }
|
|
309
|
+
.config-health-value { color: var(--text); font-weight: 500; font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace; font-size: 11px; }
|
|
310
|
+
.config-health-value.ok { color: var(--green); }
|
|
311
|
+
.config-health-value.warn { color: var(--yellow); }
|
|
312
|
+
.config-health-value.err { color: var(--red); }
|
|
313
|
+
.config-health-loading { grid-column: 1 / -1; text-align: center; color: var(--text2); font-size: 12px; padding: 12px; }
|
|
314
|
+
.config-health-issues { grid-column: 1 / -1; padding: 8px 12px; border: 1px solid rgba(239,68,68,.2); border-radius: 6px; background: rgba(239,68,68,.04); font-size: 12px; color: var(--red); }
|
|
315
|
+
.config-health-issues ul { margin: 4px 0 0 16px; padding: 0; }
|
|
316
|
+
.config-health-issues li { margin-bottom: 2px; }
|
|
317
|
+
|
|
318
|
+
.config-component-list { display: flex; flex-direction: column; gap: 6px; }
|
|
319
|
+
.config-component-row { display: flex; justify-content: space-between; align-items: center; padding: 10px 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg2); transition: border-color .15s ease; }
|
|
320
|
+
.config-component-row:hover { border-color: color-mix(in srgb, var(--border) 60%, var(--accent)); }
|
|
321
|
+
.config-component-row.running { opacity: .7; pointer-events: none; }
|
|
322
|
+
.config-component-info { display: flex; flex-direction: column; gap: 2px; }
|
|
323
|
+
.config-component-name { font-size: 13px; font-weight: 500; color: var(--text); }
|
|
324
|
+
.config-component-desc { font-size: 11px; color: var(--text2); }
|
|
325
|
+
.mcp-tool-cat-name { font-size: 12px; font-weight: 600; color: var(--text); margin-bottom: 6px; }
|
|
326
|
+
|
|
327
|
+
/* Output area */
|
|
328
|
+
.mcp-output-area { margin-top: 14px; }
|
|
329
|
+
|
|
330
|
+
.hidden-row { display: none !important; }
|
|
331
|
+
@media (max-width: 900px) {
|
|
332
|
+
.mcp-inline-grid { grid-template-columns: 1fr; }
|
|
333
|
+
}
|
|
334
|
+
@media (max-width: 640px) {
|
|
335
|
+
.mcp-inline-grid, .mcp-server-meta { grid-template-columns: 1fr; }
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* Session drawer */
|
|
339
|
+
.session-drawer {
|
|
340
|
+
position: fixed; left: 0; right: 0; bottom: 0; z-index: 120;
|
|
341
|
+
background: var(--bg2); border-top: 1px solid var(--border);
|
|
342
|
+
box-shadow: 0 -8px 24px rgba(0,0,0,.3); transform: translateY(calc(100% - 44px));
|
|
343
|
+
transition: transform .22s ease;
|
|
344
|
+
}
|
|
345
|
+
.session-drawer.open { transform: translateY(0); }
|
|
346
|
+
.session-drawer.hidden { display: none; }
|
|
347
|
+
.session-drawer-header {
|
|
348
|
+
display: flex; align-items: center; gap: 10px; min-height: 44px;
|
|
349
|
+
padding: 0 16px; border-bottom: 1px solid var(--border); background: var(--bg2);
|
|
350
|
+
cursor: pointer; user-select: none;
|
|
351
|
+
}
|
|
352
|
+
.session-drawer-header:hover { background: var(--bg3); }
|
|
353
|
+
.session-drawer-title { font-size: 12px; font-weight: 700; color: var(--text2); text-transform: uppercase; letter-spacing: .5px; }
|
|
354
|
+
.session-drawer-chevron { color: var(--text2); font-size: 14px; transition: transform .22s ease; }
|
|
355
|
+
.session-drawer.open .session-drawer-chevron { transform: rotate(180deg); }
|
|
356
|
+
.session-tabs { display: flex; gap: 6px; overflow-x: auto; padding: 10px 14px 0; }
|
|
357
|
+
.session-tab {
|
|
358
|
+
display: inline-flex; align-items: center; gap: 8px; max-width: 220px;
|
|
359
|
+
padding: 8px 10px; border: 1px solid var(--border); border-bottom: none;
|
|
360
|
+
border-radius: 8px 8px 0 0; background: var(--bg3); color: var(--text2); cursor: pointer;
|
|
361
|
+
}
|
|
362
|
+
.session-tab.active { background: var(--bg2); color: var(--text); border-color: var(--accent); }
|
|
363
|
+
.session-tab-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
364
|
+
.session-tab-status { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); flex-shrink: 0; }
|
|
365
|
+
.session-tab-status.live { background: var(--green); box-shadow: 0 0 0 4px rgba(163,190,140,.12); }
|
|
366
|
+
.session-tab-close {
|
|
367
|
+
border: none; background: transparent; color: var(--text2); cursor: pointer;
|
|
368
|
+
font-size: 14px; line-height: 1; padding: 0 2px; flex-shrink: 0;
|
|
369
|
+
}
|
|
370
|
+
.session-tab-close:hover { color: var(--red); }
|
|
371
|
+
.session-drawer-body { padding: 0 14px 14px; }
|
|
372
|
+
.session-panel {
|
|
373
|
+
display: none; background: var(--bg2); border: 1px solid var(--border);
|
|
374
|
+
border-radius: 0 8px 8px 8px; min-height: 220px; max-height: 42vh; overflow: hidden;
|
|
375
|
+
}
|
|
376
|
+
.session-panel.active { display: flex; flex-direction: column; }
|
|
377
|
+
.session-panel-header {
|
|
378
|
+
display: flex; align-items: center; gap: 10px; padding: 12px 14px;
|
|
379
|
+
border-bottom: 1px solid var(--border); flex-wrap: wrap;
|
|
380
|
+
}
|
|
381
|
+
.session-panel-title { font-size: 14px; font-weight: 600; color: var(--text); }
|
|
382
|
+
.session-panel-meta { font-size: 11px; color: var(--text2); display: flex; gap: 10px; flex-wrap: wrap; }
|
|
383
|
+
.session-live-pill {
|
|
384
|
+
font-size: 10px; font-weight: 700; padding: 3px 8px; border-radius: 20px;
|
|
385
|
+
border: 1px solid var(--border); color: var(--muted); background: var(--bg3);
|
|
386
|
+
}
|
|
387
|
+
.session-live-pill.live { color: var(--green); border-color: rgba(34,197,94,.25); background: rgba(34,197,94,.1); }
|
|
388
|
+
.session-event-list { flex: 1; overflow-y: auto; padding: 12px 14px 14px; display: flex; flex-direction: column; gap: 8px; }
|
|
389
|
+
.session-event-empty { color: var(--muted); font-size: 12px; padding: 16px 4px; }
|
|
390
|
+
.session-event {
|
|
391
|
+
border: 1px solid var(--border); border-radius: 6px; background: var(--bg3);
|
|
392
|
+
padding: 10px 12px;
|
|
393
|
+
}
|
|
394
|
+
.session-event-head { display: flex; gap: 10px; align-items: center; margin-bottom: 6px; flex-wrap: wrap; }
|
|
395
|
+
.session-event-type {
|
|
396
|
+
font-size: 10px; font-weight: 700; color: var(--accent); text-transform: uppercase;
|
|
397
|
+
letter-spacing: .45px;
|
|
398
|
+
}
|
|
399
|
+
.session-event-time { font-size: 11px; color: var(--muted); }
|
|
400
|
+
.session-event-summary { font-size: 12px; color: var(--text); line-height: 1.45; }
|
|
401
|
+
.session-event-detail {
|
|
402
|
+
margin-top: 7px; font-size: 11px; color: var(--text2); font-family: Consolas, monospace;
|
|
403
|
+
white-space: pre-wrap; word-break: break-word;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/* Notification badge on project cards */
|
|
407
|
+
.notif-badge {
|
|
408
|
+
font-size: 10px; font-weight: 700; padding: 1px 6px; border-radius: 10px;
|
|
409
|
+
background: var(--yellow); color: var(--bg); display: inline-flex; align-items: center; gap: 3px;
|
|
410
|
+
vertical-align: middle; margin-left: 4px;
|
|
411
|
+
}
|
|
412
|
+
.notif-badge.critical { background: var(--red); color: #fff; }
|
|
413
|
+
|
|
414
|
+
/* Session panel sub-tabs (Logs / Notifications) */
|
|
415
|
+
.session-subtabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: 0; }
|
|
416
|
+
.session-subtab {
|
|
417
|
+
font-size: 11px; font-weight: 600; padding: 6px 14px; cursor: pointer;
|
|
418
|
+
color: var(--text2); border-bottom: 2px solid transparent; transition: all .12s;
|
|
419
|
+
display: flex; align-items: center; gap: 5px;
|
|
420
|
+
}
|
|
421
|
+
.session-subtab:hover { color: var(--text); }
|
|
422
|
+
.session-subtab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
423
|
+
.session-subtab-badge {
|
|
424
|
+
font-size: 9px; font-weight: 700; padding: 0 5px; border-radius: 8px; min-width: 16px;
|
|
425
|
+
text-align: center; background: var(--yellow); color: var(--bg); line-height: 16px;
|
|
426
|
+
}
|
|
427
|
+
.session-notif-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 14px 4px; }
|
|
428
|
+
.session-notif-clear { font-size: 11px; cursor: pointer; color: var(--text2); background: none; border: 1px solid var(--border); border-radius: 4px; padding: 2px 8px; transition: all .12s; }
|
|
429
|
+
.session-notif-clear:hover { color: var(--red); border-color: var(--red); }
|
|
430
|
+
.session-notif-item {
|
|
431
|
+
border: 1px solid var(--border); border-radius: 6px; background: var(--bg3);
|
|
432
|
+
padding: 8px 12px; font-size: 12px;
|
|
433
|
+
}
|
|
434
|
+
.session-notif-item .notif-severity {
|
|
435
|
+
font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .4px;
|
|
436
|
+
padding: 1px 6px; border-radius: 4px; margin-right: 6px;
|
|
437
|
+
}
|
|
438
|
+
.session-notif-item .notif-severity.warning { background: rgba(234,179,8,.15); color: var(--yellow); }
|
|
439
|
+
.session-notif-item .notif-severity.critical { background: rgba(239,68,68,.12); color: var(--red); }
|
|
440
|
+
.session-notif-item .notif-severity.info { background: rgba(96,165,250,.12); color: var(--accent); }
|
|
441
|
+
.session-notif-item .notif-agent { font-size: 10px; color: var(--text2); margin-left: 4px; }
|
|
442
|
+
.session-notif-item .notif-title { color: var(--text); margin-top: 4px; line-height: 1.4; }
|
|
443
|
+
.session-notif-item .notif-time { font-size: 10px; color: var(--muted); margin-top: 3px; }
|
|
444
|
+
|
|
445
|
+
.settings-info { font-size: 12px; color: var(--text2); background: var(--bg3); border: 1px solid var(--border); border-radius: 6px; padding: 8px 12px; margin-bottom: 12px; }
|
|
446
|
+
.settings-warning { font-size: 12px; color: var(--yellow); margin-top: 8px; padding: 6px 10px; background: rgba(234,179,8,.08); border: 1px solid rgba(234,179,8,.2); border-radius: 6px; display: none; }
|
|
447
|
+
.section-divider { font-size: 11px; font-weight: 600; color: var(--text2); text-transform: uppercase; letter-spacing: .6px; border-top: 1px solid var(--border); padding-top: 14px; margin: 14px 0 10px; }
|
|
448
|
+
.svc-badge { font-size: 11px; padding: 2px 10px; border-radius: 20px; font-weight: 600; }
|
|
449
|
+
.svc-ok { background: rgba(34,197,94,.1); color: var(--green); border: 1px solid rgba(34,197,94,.25); }
|
|
450
|
+
.svc-stopped { background: var(--bg3); color: var(--text2); border: 1px solid var(--border); }
|
|
451
|
+
.svc-unknown { background: var(--bg3); color: var(--text2); border: 1px solid var(--border); }
|
|
452
|
+
.svc-error { background: rgba(239,68,68,.08); color: var(--red); border: 1px solid rgba(239,68,68,.2); }
|
|
453
|
+
.check-row { display: flex; align-items: center; gap: 8px; }
|
|
454
|
+
.check-row input[type=checkbox] { width: 15px; height: 15px; accent-color: var(--accent); cursor: pointer; }
|
|
455
|
+
.check-row label { font-size: 13px; color: var(--text2); cursor: pointer; }
|
|
456
|
+
|
|
457
|
+
/* ── Toast ── */
|
|
458
|
+
.toast-container { position: fixed; bottom: 20px; right: 20px; z-index: 200; display: flex; flex-direction: column-reverse; gap: 8px; pointer-events: none; }
|
|
459
|
+
.toast-item { background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px; font-size: 13px; max-width: 380px; min-width: 260px; box-shadow: 0 4px 24px rgba(0,0,0,.25); display: flex; align-items: center; gap: 10px; transform: translateX(120%); opacity: 0; transition: all .2s ease; pointer-events: auto; }
|
|
460
|
+
.toast-item.show { transform: translateX(0); opacity: 1; }
|
|
461
|
+
.toast-item.removing { transform: translateX(120%); opacity: 0; }
|
|
462
|
+
.toast-icon { font-size: 15px; flex-shrink: 0; line-height: 1; }
|
|
463
|
+
.toast-msg { flex: 1; line-height: 1.35; }
|
|
464
|
+
.toast-close { background: transparent; border: none; color: var(--text2); cursor: pointer; font-size: 14px; padding: 2px 4px; border-radius: 4px; flex-shrink: 0; transition: color .15s; }
|
|
465
|
+
.toast-close:hover { color: var(--text); }
|
|
466
|
+
.toast-item.ok { border-left: 3px solid var(--green); }
|
|
467
|
+
.toast-item.err { border-left: 3px solid var(--red); }
|
|
468
|
+
.toast-item.info { border-left: 3px solid var(--accent); }
|
|
469
|
+
.toast-item.warn { border-left: 3px solid var(--yellow); }
|
|
470
|
+
.toast-progress { position: absolute; bottom: 0; left: 0; height: 2px; background: var(--accent); border-radius: 0 0 0 8px; transition: width .3s ease; }
|
|
471
|
+
.toast-item { position: relative; overflow: hidden; }
|
|
472
|
+
|
|
473
|
+
/* Legacy single toast (hidden, replaced by container) */
|
|
474
|
+
#toast { display: none; }
|
|
475
|
+
|
|
476
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
477
|
+
.spin { animation: spin .7s linear infinite; display: inline-block; }
|
|
478
|
+
@media (max-width: 760px) {
|
|
479
|
+
.session-panel { max-height: 50vh; }
|
|
480
|
+
.session-tabs { padding-top: 8px; }
|
|
481
|
+
.session-tab { max-width: 160px; }
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/* ── IDE Picker ── */
|
|
485
|
+
.ide-picker { display: grid; grid-template-columns: repeat(auto-fill, minmax(108px, 1fr)); gap: 8px; margin-top: 4px; }
|
|
486
|
+
.ide-option { display: flex; flex-direction: column; align-items: center; gap: 5px; padding: 12px 8px 10px; background: var(--bg3); border: 2px solid var(--border); border-radius: 6px; cursor: pointer; text-align: center; transition: all .15s ease; user-select: none; }
|
|
487
|
+
.ide-option:hover { border-color: var(--accent); background: var(--bg2); }
|
|
488
|
+
.ide-option.selected { border-color: var(--accent); background: var(--bg2); box-shadow: 0 0 0 1px var(--accent); }
|
|
489
|
+
.ide-icon { font-size: 22px; line-height: 1; }
|
|
490
|
+
.ide-name { font-size: 12px; font-weight: 600; color: var(--text); }
|
|
491
|
+
.ide-cmd { font-size: 10px; color: var(--text2); font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace; }
|
|
492
|
+
|
|
493
|
+
/* ── Confirm Dialog ── */
|
|
494
|
+
.confirm-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.5); backdrop-filter: blur(4px); z-index: 300; display: flex; align-items: center; justify-content: center; padding: 20px; }
|
|
495
|
+
.confirm-dialog { background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; padding: 24px; max-width: 400px; width: 100%; box-shadow: 0 16px 48px rgba(0,0,0,.2); }
|
|
496
|
+
.confirm-title { font-size: 15px; font-weight: 600; color: var(--text); margin-bottom: 8px; }
|
|
497
|
+
.confirm-message { font-size: 13px; color: var(--text2); line-height: 1.5; margin-bottom: 20px; }
|
|
498
|
+
.confirm-actions { display: flex; gap: 8px; justify-content: flex-end; }
|
|
499
|
+
|
|
500
|
+
/* ── Batch Progress ── */
|
|
501
|
+
.batch-progress-bar { width: 100%; height: 4px; background: var(--bg3); border-radius: 4px; overflow: hidden; margin-bottom: 14px; }
|
|
502
|
+
.batch-progress-fill { height: 100%; background: var(--accent); transition: width .3s ease; border-radius: 4px; }
|
|
503
|
+
.batch-item { display: flex; justify-content: space-between; align-items: center; background: var(--bg3); padding: 8px 12px; border-radius: 6px; border: 1px solid var(--border); }
|
|
504
|
+
.batch-item-name { font-weight: 600; font-size: 12px; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
505
|
+
.batch-item-path { font-size: 10px; color: var(--text2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace; }
|
|
506
|
+
.batch-item-status { font-size: 10px; font-weight: 700; flex-shrink: 0; padding: 2px 8px; border-radius: 9999px; }
|
|
507
|
+
.batch-status-pending { color: var(--text2); background: var(--bg); }
|
|
508
|
+
.batch-status-running { color: var(--accent); background: rgba(59,130,246,.1); }
|
|
509
|
+
.batch-status-done { color: var(--green); background: rgba(34,197,94,.1); }
|
|
510
|
+
.batch-status-failed { color: var(--red); background: rgba(239,68,68,.1); }
|
|
511
|
+
.batch-status-cancelled { color: var(--yellow); background: rgba(234,179,8,.1); }
|
|
512
|
+
.batch-summary { font-size: 13px; color: var(--text); padding: 12px; background: var(--bg3); border-radius: 6px; border: 1px solid var(--border); margin-top: 10px; }
|
|
513
|
+
</style>
|
|
514
|
+
</head>
|
|
515
|
+
<body>
|
|
516
|
+
|
|
517
|
+
<!-- Header -->
|
|
518
|
+
<header>
|
|
519
|
+
<div class="logo">C3 <small>Project Hub</small></div>
|
|
520
|
+
<div class="header-right">
|
|
521
|
+
<span class="hub-version" id="hub-version">v…</span>
|
|
522
|
+
<span class="hub-port" id="hub-port-badge" title="Configured hub port">:…</span>
|
|
523
|
+
<div class="badge" id="session-badge"><span class="dot"></span><span id="session-count">0 active</span></div>
|
|
524
|
+
<button class="btn-theme" id="theme-toggle-btn" onclick="toggleTheme()" title="Toggle light/dark mode">Theme: Dark</button>
|
|
525
|
+
<a class="btn-settings" id="oracle-link" href="#" target="_blank" title="Oracle Memory Agent" style="display:none;text-decoration:none">Oracle</a>
|
|
526
|
+
<button class="btn-settings" onclick="openSettings()" title="Hub Settings">⚙ Settings</button>
|
|
527
|
+
<button class="btn-restart" onclick="restartHub()" title="Restart hub server">↺ Restart</button>
|
|
528
|
+
<button class="btn-refresh" onclick="refreshHub()">⟳ Refresh</button>
|
|
529
|
+
<button class="btn-refresh" onclick="location.reload()" title="Full page reload">⟳ Reload</button>
|
|
530
|
+
</div>
|
|
531
|
+
</header>
|
|
532
|
+
|
|
533
|
+
<main>
|
|
534
|
+
<aside class="sidebar" id="sidebar">
|
|
535
|
+
<div class="sidebar-header">
|
|
536
|
+
<span class="sidebar-title">Groups</span>
|
|
537
|
+
<button class="sidebar-toggle" onclick="toggleSidebar()" title="Collapse sidebar">◀</button>
|
|
538
|
+
</div>
|
|
539
|
+
<nav class="sidebar-nav" id="sidebar-nav"></nav>
|
|
540
|
+
</aside>
|
|
541
|
+
<button class="sidebar-expand" onclick="toggleSidebar()" title="Show sidebar">▶</button>
|
|
542
|
+
<div class="main-content">
|
|
543
|
+
<!-- Add Project -->
|
|
544
|
+
<div class="add-card">
|
|
545
|
+
<h2>Register Project</h2>
|
|
546
|
+
<div class="add-row">
|
|
547
|
+
<input class="input-path" id="add-path" type="text" placeholder="Absolute path (e.g. C:/projects/myapp)">
|
|
548
|
+
<input class="input-name" id="add-name" type="text" placeholder="Name (optional)">
|
|
549
|
+
<button class="btn btn-primary" id="add-btn" onclick="addProject()">+ Add</button>
|
|
550
|
+
</div>
|
|
551
|
+
</div>
|
|
552
|
+
|
|
553
|
+
<!-- Summary bar -->
|
|
554
|
+
<div class="summary-bar">
|
|
555
|
+
<h1 id="project-count">Projects</h1>
|
|
556
|
+
<button class="btn btn-xs btn-yellow" id="update-all-btn" onclick="updateAllProjects()" style="display:none; margin-right: 12px; font-weight: 700;">🚀 Update All Outdated</button>
|
|
557
|
+
<button class="btn btn-xs btn-ghost" onclick="loadProjects()" style="margin-right: 12px;" title="Manual Refresh">🔄 Refresh</button>
|
|
558
|
+
<input class="search-input" id="search-input" type="text" placeholder="🔍 Search name / path / IDE…" oninput="renderProjects()">
|
|
559
|
+
<div class="view-row">
|
|
560
|
+
<button class="btn-view active" id="view-list-btn" onclick="setProjectsView('list')">List</button>
|
|
561
|
+
<button class="btn-view" id="view-grid-btn" onclick="setProjectsView('grid')">Grid</button>
|
|
562
|
+
</div>
|
|
563
|
+
<div class="filter-row">
|
|
564
|
+
<button class="filter-btn active" onclick="setFilter('all', this)">All</button>
|
|
565
|
+
<button class="filter-btn" onclick="setFilter('active', this)">Active</button>
|
|
566
|
+
<button class="filter-btn" onclick="setFilter('idle', this)">Idle</button>
|
|
567
|
+
</div>
|
|
568
|
+
</div>
|
|
569
|
+
|
|
570
|
+
<div id="projects-grid"></div>
|
|
571
|
+
</div><!-- .main-content -->
|
|
572
|
+
</main>
|
|
573
|
+
|
|
574
|
+
<div class="session-drawer hidden" id="session-drawer">
|
|
575
|
+
<div class="session-drawer-header" onclick="toggleSessionDrawer()">
|
|
576
|
+
<div class="session-drawer-title">Session Log</div>
|
|
577
|
+
<span class="session-drawer-chevron">▲</span>
|
|
578
|
+
</div>
|
|
579
|
+
<div class="session-tabs" id="session-tabs"></div>
|
|
580
|
+
<div class="session-drawer-body" id="session-drawer-body"></div>
|
|
581
|
+
</div>
|
|
582
|
+
|
|
583
|
+
<!-- Project Setup Modal (Init + MCP combined) -->
|
|
584
|
+
<div class="modal-backdrop hidden" id="mcp-modal">
|
|
585
|
+
<div class="modal modal-lg">
|
|
586
|
+
<div class="modal-header">
|
|
587
|
+
<h3>MCP Manager</h3>
|
|
588
|
+
<button class="btn btn-xs btn-ghost" onclick="refreshMcpModal(true)" style="margin-left:auto; margin-right:8px;">Refresh</button>
|
|
589
|
+
<button class="modal-close" onclick="closeModal('mcp-modal')">×</button>
|
|
590
|
+
</div>
|
|
591
|
+
<div class="modal-body">
|
|
592
|
+
<!-- Compact header: path + status pills -->
|
|
593
|
+
<div class="mcp-header-bar">
|
|
594
|
+
<div class="mcp-header-path" id="mcp-modal-path" title=""></div>
|
|
595
|
+
<div class="mcp-header-pills" id="mcp-status-pills"></div>
|
|
596
|
+
</div>
|
|
597
|
+
|
|
598
|
+
<!-- Tab navigation -->
|
|
599
|
+
<div class="mcp-tabs" id="mcp-tabs">
|
|
600
|
+
<button class="mcp-tab active" onclick="setMcpTab('servers')">Servers</button>
|
|
601
|
+
<button class="mcp-tab" onclick="setMcpTab('setup')">C3 Setup</button>
|
|
602
|
+
<button class="mcp-tab" onclick="setMcpTab('config')">Config</button>
|
|
603
|
+
<button class="mcp-tab" onclick="setMcpTab('reference')">Reference</button>
|
|
604
|
+
</div>
|
|
605
|
+
|
|
606
|
+
<!-- Tab: Servers (overview + server list + add) -->
|
|
607
|
+
<div class="mcp-tab-panel active" id="mcp-tab-servers">
|
|
608
|
+
<!-- Status overview -->
|
|
609
|
+
<div class="mcp-section">
|
|
610
|
+
<div class="mcp-status-row" id="mcp-status-row"></div>
|
|
611
|
+
<div class="mcp-config-path" id="mcp-config-path-display" style="display:none;"></div>
|
|
612
|
+
</div>
|
|
613
|
+
|
|
614
|
+
<!-- Server list -->
|
|
615
|
+
<div id="mcp-server-list" class="mcp-server-list" style="margin-top:10px;">
|
|
616
|
+
<div class="mcp-server-empty"><div class="mcp-server-empty-icon">☍</div>Loading servers…</div>
|
|
617
|
+
</div>
|
|
618
|
+
|
|
619
|
+
<!-- Add server toggle + collapsible form -->
|
|
620
|
+
<button class="mcp-add-toggle" id="mcp-add-toggle" onclick="toggleMcpAddForm()" style="margin-top:10px;">+ Add or update a server</button>
|
|
621
|
+
<div class="mcp-add-form hidden" id="mcp-add-form">
|
|
622
|
+
<div class="mcp-inline-grid">
|
|
623
|
+
<div class="form-group">
|
|
624
|
+
<label class="form-label" for="mcp-custom-name">Server Name</label>
|
|
625
|
+
<input class="form-input" id="mcp-custom-name" placeholder="my-server">
|
|
626
|
+
</div>
|
|
627
|
+
<div class="form-group">
|
|
628
|
+
<label class="form-label" for="mcp-custom-command">Command</label>
|
|
629
|
+
<input class="form-input" id="mcp-custom-command" placeholder="python">
|
|
630
|
+
</div>
|
|
631
|
+
</div>
|
|
632
|
+
<div class="form-group">
|
|
633
|
+
<label class="form-label" for="mcp-custom-args">Args</label>
|
|
634
|
+
<textarea class="form-textarea" id="mcp-custom-args" style="min-height:56px;" placeholder='One argument per line, or JSON array like ["cli/mcp_server.py","--project","."]'></textarea>
|
|
635
|
+
</div>
|
|
636
|
+
<div class="form-group">
|
|
637
|
+
<label class="form-label" for="mcp-custom-env">Env</label>
|
|
638
|
+
<textarea class="form-textarea" id="mcp-custom-env" style="min-height:48px;" placeholder='Optional JSON object, e.g. {"FOO":"bar"}'></textarea>
|
|
639
|
+
</div>
|
|
640
|
+
<div class="form-group hidden-row" id="mcp-custom-enabled-row">
|
|
641
|
+
<label class="checkbox-row" style="display:flex; align-items:center; gap:8px; cursor:pointer;">
|
|
642
|
+
<input type="checkbox" id="mcp-custom-enabled" checked>
|
|
643
|
+
<span>Enabled</span>
|
|
644
|
+
</label>
|
|
645
|
+
</div>
|
|
646
|
+
<div class="mcp-actions-row" style="margin-top:8px;">
|
|
647
|
+
<button class="btn btn-primary" id="mcp-custom-save-btn" onclick="saveCustomMcpServer()">Save Server</button>
|
|
648
|
+
<button class="btn btn-ghost" onclick="toggleMcpAddForm()">Cancel</button>
|
|
649
|
+
</div>
|
|
650
|
+
</div>
|
|
651
|
+
</div>
|
|
652
|
+
|
|
653
|
+
<!-- Tab: C3 Setup (Init + MCP combined) -->
|
|
654
|
+
<div class="mcp-tab-panel" id="mcp-tab-setup">
|
|
655
|
+
<!-- Shared IDE / Mode selectors -->
|
|
656
|
+
<div class="mcp-section">
|
|
657
|
+
<h4>Target Configuration</h4>
|
|
658
|
+
<div class="mcp-section-subtitle" style="margin-bottom:12px;">These settings apply to both Init and MCP actions below.</div>
|
|
659
|
+
<div class="mcp-inline-grid">
|
|
660
|
+
<div class="form-group">
|
|
661
|
+
<label class="form-label" for="mcp-modal-ide">IDE</label>
|
|
662
|
+
<select class="form-select" id="mcp-modal-ide" onchange="handleMcpSelectionChange()">
|
|
663
|
+
<option value="">— keep current —</option>
|
|
664
|
+
<option value="claude-code">Claude Code CLI</option>
|
|
665
|
+
<option value="claude-app">Claude Code App</option>
|
|
666
|
+
<option value="vscode">VS Code Copilot</option>
|
|
667
|
+
<option value="cursor">Cursor</option>
|
|
668
|
+
<option value="codex">OpenAI Codex</option>
|
|
669
|
+
<option value="gemini">Gemini CLI</option>
|
|
670
|
+
<option value="antigravity">Antigravity</option>
|
|
671
|
+
</select>
|
|
672
|
+
</div>
|
|
673
|
+
<div class="form-group">
|
|
674
|
+
<label class="form-label" for="mcp-modal-mode">MCP Mode</label>
|
|
675
|
+
<select class="form-select" id="mcp-modal-mode" onchange="handleMcpSelectionChange()">
|
|
676
|
+
<option value="">— keep current —</option>
|
|
677
|
+
<option value="direct">direct</option>
|
|
678
|
+
<option value="proxy">proxy</option>
|
|
679
|
+
</select>
|
|
680
|
+
</div>
|
|
681
|
+
</div>
|
|
682
|
+
</div>
|
|
683
|
+
|
|
684
|
+
<!-- Init controls -->
|
|
685
|
+
<div class="mcp-section">
|
|
686
|
+
<h4>Initialize / Update Project</h4>
|
|
687
|
+
<div class="mcp-section-subtitle" style="margin-bottom:12px;">Rebuild index, refresh instruction files, update MCP tool definitions, ensure C3 subdirectories.</div>
|
|
688
|
+
<div class="mcp-inline-grid">
|
|
689
|
+
<div class="form-group">
|
|
690
|
+
<label class="form-label" for="setup-init-mode">Init Action</label>
|
|
691
|
+
<select class="form-select" id="setup-init-mode" onchange="syncSetupInitOptions()">
|
|
692
|
+
<option value="force">Update / Repair C3 files</option>
|
|
693
|
+
<option value="clear">Wipe C3 project files only</option>
|
|
694
|
+
</select>
|
|
695
|
+
</div>
|
|
696
|
+
<div class="form-group" id="setup-git-row">
|
|
697
|
+
<label class="form-label"> </label>
|
|
698
|
+
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; font-size:13px; color:var(--text2); padding:7px 0;">
|
|
699
|
+
<input type="checkbox" id="setup-git" style="width:15px;height:15px;accent-color:var(--accent);">
|
|
700
|
+
<span>Init Git repo if needed</span>
|
|
701
|
+
</label>
|
|
702
|
+
</div>
|
|
703
|
+
</div>
|
|
704
|
+
<div class="mcp-actions-row" style="margin-top:4px;">
|
|
705
|
+
<button class="btn btn-primary" id="setup-init-btn" onclick="runSetupInit()">Run Init</button>
|
|
706
|
+
</div>
|
|
707
|
+
</div>
|
|
708
|
+
|
|
709
|
+
<!-- MCP controls -->
|
|
710
|
+
<div class="mcp-section">
|
|
711
|
+
<h4>MCP Server (C3)</h4>
|
|
712
|
+
<div class="mcp-section-subtitle" style="margin-bottom:8px;">Install or remove the C3 MCP entry. Other servers in the config are preserved.</div>
|
|
713
|
+
<div id="mcp-install-status" style="display:none; margin-bottom:10px; font-size:12px; align-items:center; gap:8px;">
|
|
714
|
+
<span id="mcp-install-state-badge" class="mcp-pill"></span>
|
|
715
|
+
<span id="mcp-install-cfg-path" style="color:var(--muted); font-family:monospace; font-size:11px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;"></span>
|
|
716
|
+
</div>
|
|
717
|
+
<div class="mcp-actions-row" style="margin-top:0;">
|
|
718
|
+
<button class="btn btn-primary" id="mcp-install-btn" onclick="runMcpInstall()">Install C3</button>
|
|
719
|
+
<button class="btn btn-danger" id="mcp-remove-btn" onclick="runMcpRemove('c3')" style="display:none;">Remove C3</button>
|
|
720
|
+
</div>
|
|
721
|
+
</div>
|
|
722
|
+
|
|
723
|
+
<!-- Inline output for this tab -->
|
|
724
|
+
<div class="mcp-output-area">
|
|
725
|
+
<pre class="cmd-output hidden" id="mcp-output"></pre>
|
|
726
|
+
</div>
|
|
727
|
+
</div>
|
|
728
|
+
|
|
729
|
+
<!-- Tab: Config (health + component updates) -->
|
|
730
|
+
<div class="mcp-tab-panel" id="mcp-tab-config">
|
|
731
|
+
<div class="mcp-section">
|
|
732
|
+
<h4>Project Health</h4>
|
|
733
|
+
<div class="mcp-section-subtitle" style="margin-bottom:10px;">Current state of C3 configuration components.</div>
|
|
734
|
+
<div id="config-health-status" class="config-health-grid">
|
|
735
|
+
<div class="config-health-loading">Checking…</div>
|
|
736
|
+
</div>
|
|
737
|
+
</div>
|
|
738
|
+
|
|
739
|
+
<div class="mcp-section">
|
|
740
|
+
<h4>Budget & Session Settings</h4>
|
|
741
|
+
<div class="mcp-section-subtitle" style="margin-bottom:12px;">Single budget threshold and guidance toggles for this project.</div>
|
|
742
|
+
<div id="budget-settings-container" style="display:flex;flex-direction:column;gap:12px;">
|
|
743
|
+
<div style="display:grid;grid-template-columns:1fr;gap:8px;max-width:220px;">
|
|
744
|
+
<div class="form-group" style="margin-bottom:0">
|
|
745
|
+
<label class="form-label" style="font-size:11px;">Budget Threshold</label>
|
|
746
|
+
<input class="form-input" type="number" id="budget-threshold" min="1000" step="5000" placeholder="35000">
|
|
747
|
+
<span style="font-size:9px;color:var(--muted)">Nudge AI to restart when C3 tokens exceed this</span>
|
|
748
|
+
</div>
|
|
749
|
+
</div>
|
|
750
|
+
<div style="display:flex;flex-direction:column;gap:6px;">
|
|
751
|
+
<div class="check-row">
|
|
752
|
+
<input type="checkbox" id="budget-nudges-toggle">
|
|
753
|
+
<label for="budget-nudges-toggle">Budget Nudges — append warning when over threshold</label>
|
|
754
|
+
</div>
|
|
755
|
+
<div class="check-row">
|
|
756
|
+
<input type="checkbox" id="budget-alerts-toggle">
|
|
757
|
+
<label for="budget-alerts-toggle">Agent Alerts — inline critical alerts on every response</label>
|
|
758
|
+
</div>
|
|
759
|
+
</div>
|
|
760
|
+
<div style="display:flex;gap:6px;">
|
|
761
|
+
<button class="btn btn-primary btn-sm" onclick="saveBudgetSettings()">Save Budget Settings</button>
|
|
762
|
+
<span id="budget-save-msg" style="font-size:11px;color:var(--muted);align-self:center;"></span>
|
|
763
|
+
</div>
|
|
764
|
+
</div>
|
|
765
|
+
</div>
|
|
766
|
+
|
|
767
|
+
<div class="mcp-section" id="perm-section">
|
|
768
|
+
<h4>Permissions (Claude Code)</h4>
|
|
769
|
+
<div class="mcp-section-subtitle" style="margin-bottom:12px;">Claude Code permission tier for this project. C3 MCP tools are always allowed.</div>
|
|
770
|
+
<div id="perm-settings-container" style="display:flex;flex-direction:column;gap:10px;">
|
|
771
|
+
<div style="display:flex;align-items:center;gap:8px;">
|
|
772
|
+
<span style="font-size:11px;color:var(--muted)">Current tier:</span>
|
|
773
|
+
<span id="perm-current-badge" class="badge" style="font-size:11px;font-weight:600;">—</span>
|
|
774
|
+
<span id="perm-rule-counts" style="font-size:10px;color:var(--muted)"></span>
|
|
775
|
+
</div>
|
|
776
|
+
<div style="display:flex;flex-direction:column;gap:6px;" id="perm-tier-list"></div>
|
|
777
|
+
<div style="display:flex;gap:6px;align-items:center;">
|
|
778
|
+
<span id="perm-save-msg" style="font-size:11px;color:var(--muted);"></span>
|
|
779
|
+
</div>
|
|
780
|
+
</div>
|
|
781
|
+
</div>
|
|
782
|
+
|
|
783
|
+
<div class="mcp-section">
|
|
784
|
+
<h4>Update Components</h4>
|
|
785
|
+
<div class="mcp-section-subtitle" style="margin-bottom:12px;">Selectively rebuild or refresh individual components instead of running a full init.</div>
|
|
786
|
+
<div class="config-component-list">
|
|
787
|
+
<div class="config-component-row">
|
|
788
|
+
<div class="config-component-info">
|
|
789
|
+
<span class="config-component-name">Code Index</span>
|
|
790
|
+
<span class="config-component-desc">Rebuild file index and search chunks</span>
|
|
791
|
+
</div>
|
|
792
|
+
<button class="btn btn-xs btn-ghost" onclick="runComponentUpdate('index')">Rebuild</button>
|
|
793
|
+
</div>
|
|
794
|
+
<div class="config-component-row">
|
|
795
|
+
<div class="config-component-info">
|
|
796
|
+
<span class="config-component-name">Embedding Index</span>
|
|
797
|
+
<span class="config-component-desc">Rebuild semantic search embeddings (requires Ollama)</span>
|
|
798
|
+
</div>
|
|
799
|
+
<button class="btn btn-xs btn-ghost" onclick="runComponentUpdate('embeddings')">Rebuild</button>
|
|
800
|
+
</div>
|
|
801
|
+
<div class="config-component-row">
|
|
802
|
+
<div class="config-component-info">
|
|
803
|
+
<span class="config-component-name">Doc Index (RAG)</span>
|
|
804
|
+
<span class="config-component-desc">Rebuild doc/config/docstring index for session pre-loading</span>
|
|
805
|
+
</div>
|
|
806
|
+
<button class="btn btn-xs btn-ghost" onclick="runComponentUpdate('doc_index')">Rebuild</button>
|
|
807
|
+
</div>
|
|
808
|
+
<div class="config-component-row">
|
|
809
|
+
<div class="config-component-info">
|
|
810
|
+
<span class="config-component-name">Compression Dictionary</span>
|
|
811
|
+
<span class="config-component-desc">Regenerate project-specific compression terms</span>
|
|
812
|
+
</div>
|
|
813
|
+
<button class="btn btn-xs btn-ghost" onclick="runComponentUpdate('dictionary')">Rebuild</button>
|
|
814
|
+
</div>
|
|
815
|
+
<div class="config-component-row">
|
|
816
|
+
<div class="config-component-info">
|
|
817
|
+
<span class="config-component-name">Instruction Docs</span>
|
|
818
|
+
<span class="config-component-desc">Sync CLAUDE.md, AGENTS.md, GEMINI.md to current templates</span>
|
|
819
|
+
</div>
|
|
820
|
+
<button class="btn btn-xs btn-ghost" onclick="runComponentUpdate('instructions')">Sync</button>
|
|
821
|
+
</div>
|
|
822
|
+
<div class="config-component-row">
|
|
823
|
+
<div class="config-component-info">
|
|
824
|
+
<span class="config-component-name">Config & Directories</span>
|
|
825
|
+
<span class="config-component-desc">Refresh config.json and ensure .c3/ subdirectories</span>
|
|
826
|
+
</div>
|
|
827
|
+
<button class="btn btn-xs btn-ghost" onclick="runComponentUpdate('config')">Refresh</button>
|
|
828
|
+
</div>
|
|
829
|
+
<div class="config-component-row">
|
|
830
|
+
<div class="config-component-info">
|
|
831
|
+
<span class="config-component-name">MCP Server</span>
|
|
832
|
+
<span class="config-component-desc">Reinstall C3 MCP entry for the current IDE</span>
|
|
833
|
+
</div>
|
|
834
|
+
<button class="btn btn-xs btn-ghost" onclick="runComponentUpdate('mcp')">Reinstall</button>
|
|
835
|
+
</div>
|
|
836
|
+
</div>
|
|
837
|
+
</div>
|
|
838
|
+
|
|
839
|
+
<div class="mcp-section" style="margin-top:8px;">
|
|
840
|
+
<div class="mcp-actions-row">
|
|
841
|
+
<button class="btn btn-primary btn-sm" onclick="runComponentUpdate('all')">Update All Components</button>
|
|
842
|
+
</div>
|
|
843
|
+
</div>
|
|
844
|
+
|
|
845
|
+
<div class="mcp-output-area">
|
|
846
|
+
<pre class="cmd-output hidden" id="config-output"></pre>
|
|
847
|
+
</div>
|
|
848
|
+
</div>
|
|
849
|
+
|
|
850
|
+
<!-- Tab: Reference (CLI + tools) -->
|
|
851
|
+
<div class="mcp-tab-panel" id="mcp-tab-reference">
|
|
852
|
+
<div class="mcp-section">
|
|
853
|
+
<h4>CLI Commands</h4>
|
|
854
|
+
<div class="mcp-section-subtitle" style="margin-bottom:10px;">Click a command to copy it.</div>
|
|
855
|
+
<div class="mcp-cli-list" id="mcp-cli-preview-list"></div>
|
|
856
|
+
</div>
|
|
857
|
+
|
|
858
|
+
<div class="mcp-section">
|
|
859
|
+
<h4>C3 Tool Categories</h4>
|
|
860
|
+
<div class="mcp-section-subtitle" style="margin-bottom:10px;">Tool groups exposed by C3 via MCP.</div>
|
|
861
|
+
<div id="mcp-tool-categories"></div>
|
|
862
|
+
</div>
|
|
863
|
+
</div>
|
|
864
|
+
</div>
|
|
865
|
+
<div class="modal-footer">
|
|
866
|
+
<button class="btn btn-ghost" onclick="closeModal('mcp-modal')">Close</button>
|
|
867
|
+
</div>
|
|
868
|
+
</div>
|
|
869
|
+
</div>
|
|
870
|
+
|
|
871
|
+
<!-- C3 Setup Modal -->
|
|
872
|
+
<div class="modal-backdrop hidden" id="c3-setup-modal">
|
|
873
|
+
<div class="modal">
|
|
874
|
+
<div class="modal-header">
|
|
875
|
+
<h3>Set Up C3 for This Project</h3>
|
|
876
|
+
<button class="modal-close" onclick="closeModal('c3-setup-modal')">×</button>
|
|
877
|
+
</div>
|
|
878
|
+
<div class="modal-body">
|
|
879
|
+
<div class="mcp-section" style="margin-bottom:0;">
|
|
880
|
+
<div class="mcp-header-bar" style="margin-bottom:12px;">
|
|
881
|
+
<div class="mcp-header-path" id="c3-setup-path" title=""></div>
|
|
882
|
+
</div>
|
|
883
|
+
<p style="font-size:13px;color:var(--text2);margin-bottom:16px;">
|
|
884
|
+
This project does not have a C3 system yet. Initialize it now to enable
|
|
885
|
+
code intelligence, MCP tools, and session tracking.
|
|
886
|
+
</p>
|
|
887
|
+
<div class="mcp-inline-grid" style="margin-bottom:16px;">
|
|
888
|
+
<div class="form-group">
|
|
889
|
+
<label class="form-label" for="c3-setup-ide">IDE / Agent</label>
|
|
890
|
+
<select class="form-select" id="c3-setup-ide">
|
|
891
|
+
<option value="">— auto-detect —</option>
|
|
892
|
+
<option value="claude-code">Claude Code CLI</option>
|
|
893
|
+
<option value="claude-app">Claude Code App</option>
|
|
894
|
+
<option value="vscode">VS Code Copilot</option>
|
|
895
|
+
<option value="cursor">Cursor</option>
|
|
896
|
+
<option value="codex">OpenAI Codex</option>
|
|
897
|
+
<option value="gemini">Gemini CLI</option>
|
|
898
|
+
</select>
|
|
899
|
+
</div>
|
|
900
|
+
<div class="form-group">
|
|
901
|
+
<label class="form-label" for="c3-setup-mcp-mode">MCP Mode</label>
|
|
902
|
+
<select class="form-select" id="c3-setup-mcp-mode">
|
|
903
|
+
<option value="">— auto —</option>
|
|
904
|
+
<option value="direct">direct</option>
|
|
905
|
+
<option value="proxy">proxy</option>
|
|
906
|
+
</select>
|
|
907
|
+
</div>
|
|
908
|
+
</div>
|
|
909
|
+
<div style="display:flex;flex-direction:column;gap:10px;">
|
|
910
|
+
<div style="display:flex;align-items:center;gap:10px;">
|
|
911
|
+
<span id="c3-setup-step1-badge" class="mcp-pill" style="min-width:22px;text-align:center;">1</span>
|
|
912
|
+
<div style="flex:1;">
|
|
913
|
+
<div style="font-size:13px;font-weight:600;color:var(--text1);">Initialize C3</div>
|
|
914
|
+
<div style="font-size:11px;color:var(--muted);">Builds code index, config, instruction docs, and .c3 directory</div>
|
|
915
|
+
</div>
|
|
916
|
+
<button class="btn btn-primary btn-sm" id="c3-setup-init-btn" onclick="c3SetupRunInit()">Initialize</button>
|
|
917
|
+
</div>
|
|
918
|
+
<div style="display:flex;align-items:center;gap:10px;">
|
|
919
|
+
<span id="c3-setup-step2-badge" class="mcp-pill" style="min-width:22px;text-align:center;">2</span>
|
|
920
|
+
<div style="flex:1;">
|
|
921
|
+
<div style="font-size:13px;font-weight:600;color:var(--text1);">Install MCP Server</div>
|
|
922
|
+
<div style="font-size:11px;color:var(--muted);">Register the C3 MCP server in the IDE config so tools are available</div>
|
|
923
|
+
</div>
|
|
924
|
+
<button class="btn btn-xs btn-ghost" id="c3-setup-mcp-btn" onclick="c3SetupRunMcp()" disabled>Install MCP</button>
|
|
925
|
+
</div>
|
|
926
|
+
</div>
|
|
927
|
+
<div class="mcp-output-area">
|
|
928
|
+
<pre class="cmd-output hidden" id="c3-setup-output"></pre>
|
|
929
|
+
</div>
|
|
930
|
+
</div>
|
|
931
|
+
</div>
|
|
932
|
+
<div class="modal-footer">
|
|
933
|
+
<button class="btn btn-ghost" onclick="closeModal('c3-setup-modal')">Skip for now</button>
|
|
934
|
+
<button class="btn btn-primary" id="c3-setup-done-btn" onclick="closeModal('c3-setup-modal')" disabled>Done</button>
|
|
935
|
+
</div>
|
|
936
|
+
</div>
|
|
937
|
+
</div>
|
|
938
|
+
|
|
939
|
+
<!-- Edit Project Modal -->
|
|
940
|
+
<div class="modal-backdrop hidden" id="edit-modal">
|
|
941
|
+
<div class="modal">
|
|
942
|
+
<div class="modal-header">
|
|
943
|
+
<h3>✏ Edit Project</h3>
|
|
944
|
+
<button class="modal-close" onclick="closeModal('edit-modal')">×</button>
|
|
945
|
+
</div>
|
|
946
|
+
<div class="modal-body">
|
|
947
|
+
<div class="form-group">
|
|
948
|
+
<span class="form-label">Path</span>
|
|
949
|
+
<div class="form-path" id="edit-modal-path"></div>
|
|
950
|
+
</div>
|
|
951
|
+
<div class="form-group">
|
|
952
|
+
<label class="form-label" for="edit-name">Name</label>
|
|
953
|
+
<input class="form-input" type="text" id="edit-name">
|
|
954
|
+
</div>
|
|
955
|
+
<div class="form-group">
|
|
956
|
+
<label class="form-label" for="edit-tags">Tags <span style="font-weight:400;text-transform:none;letter-spacing:0">(comma-separated)</span></label>
|
|
957
|
+
<input class="form-input" type="text" id="edit-tags" placeholder="frontend, python, client">
|
|
958
|
+
</div>
|
|
959
|
+
<div class="form-group">
|
|
960
|
+
<label class="form-label" for="edit-notes">Notes</label>
|
|
961
|
+
<textarea class="form-textarea" id="edit-notes" placeholder="Any notes about this project…"></textarea>
|
|
962
|
+
</div>
|
|
963
|
+
</div>
|
|
964
|
+
<div class="modal-footer">
|
|
965
|
+
<button class="btn btn-ghost" onclick="closeModal('edit-modal')">Cancel</button>
|
|
966
|
+
<button class="btn btn-primary" id="edit-save-btn" onclick="saveEdit()">Save</button>
|
|
967
|
+
</div>
|
|
968
|
+
</div>
|
|
969
|
+
</div>
|
|
970
|
+
|
|
971
|
+
<!-- Transfer Modal -->
|
|
972
|
+
<div class="modal-backdrop hidden" id="transfer-modal">
|
|
973
|
+
<div class="modal">
|
|
974
|
+
<div class="modal-header">
|
|
975
|
+
<h3>Transfer Project</h3>
|
|
976
|
+
<button class="modal-close" onclick="closeModal('transfer-modal')">×</button>
|
|
977
|
+
</div>
|
|
978
|
+
<div class="modal-body">
|
|
979
|
+
<div class="form-group">
|
|
980
|
+
<span class="form-label">Current Path</span>
|
|
981
|
+
<div class="form-path" id="transfer-modal-old-path"></div>
|
|
982
|
+
</div>
|
|
983
|
+
<div class="form-group">
|
|
984
|
+
<label class="form-label" for="transfer-new-path">New Path</label>
|
|
985
|
+
<input class="form-input" type="text" id="transfer-new-path" placeholder="/new/location/of/project">
|
|
986
|
+
</div>
|
|
987
|
+
<p style="font-size:.8rem;color:var(--text-muted);margin-top:.5rem;">
|
|
988
|
+
Copy your project folder first, then enter the new location here. The new path must contain a .c3/ directory.
|
|
989
|
+
</p>
|
|
990
|
+
</div>
|
|
991
|
+
<div class="modal-footer">
|
|
992
|
+
<button class="btn btn-ghost" onclick="closeModal('transfer-modal')">Cancel</button>
|
|
993
|
+
<button class="btn btn-primary" id="transfer-save-btn" onclick="saveTransfer()">Transfer</button>
|
|
994
|
+
</div>
|
|
995
|
+
</div>
|
|
996
|
+
</div>
|
|
997
|
+
|
|
998
|
+
<!-- Settings Modal -->
|
|
999
|
+
<div class="modal-backdrop hidden" id="settings-modal">
|
|
1000
|
+
<div class="modal">
|
|
1001
|
+
<div class="modal-header">
|
|
1002
|
+
<h3>⚙ Settnigs</h3>
|
|
1003
|
+
<button class="modal-close" onclick="closeModal('settings-modal')">×</button>
|
|
1004
|
+
</div>
|
|
1005
|
+
<div class="modal-body">
|
|
1006
|
+
<div class="settings-info" id="settings-running-info">Hub is running on port …</div>
|
|
1007
|
+
<div class="form-group">
|
|
1008
|
+
<label class="form-label" for="settings-port">Dedicated Port</label>
|
|
1009
|
+
<input class="form-input" type="number" id="settings-port" min="1024" max="65535" placeholder="3330">
|
|
1010
|
+
</div>
|
|
1011
|
+
<div class="form-group">
|
|
1012
|
+
<div class="check-row">
|
|
1013
|
+
<input type="checkbox" id="settings-auto-browser">
|
|
1014
|
+
<label for="settings-auto-browser">Auto-open browser when hub starts</label>
|
|
1015
|
+
</div>
|
|
1016
|
+
</div>
|
|
1017
|
+
<div class="settings-warning" id="settings-port-warning"></div>
|
|
1018
|
+
|
|
1019
|
+
<!-- Session Mode section -->
|
|
1020
|
+
<div class="section-divider">Session Mode</div>
|
|
1021
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
|
|
1022
|
+
<span id="session-mode-badge" class="svc-badge svc-unknown">checking…</span>
|
|
1023
|
+
<span id="session-mode-label" style="font-size:11px;color:var(--muted)"></span>
|
|
1024
|
+
</div>
|
|
1025
|
+
<button class="btn btn-ghost" id="detach-terminal-btn" onclick="detachFromTerminal()" style="display:none;">
|
|
1026
|
+
⎋ Detach from Terminal
|
|
1027
|
+
</button>
|
|
1028
|
+
<div id="session-mode-info" style="font-size:11px;color:var(--muted);margin-top:6px;display:none;">
|
|
1029
|
+
Restarts hub as a background process with no terminal window — same as when Windows starts it automatically.
|
|
1030
|
+
</div>
|
|
1031
|
+
|
|
1032
|
+
<!-- Service section -->
|
|
1033
|
+
<div class="section-divider">Startup Service</div>
|
|
1034
|
+
<div class="settings-info" id="svc-info">
|
|
1035
|
+
Run C3 Hub automatically on login — no terminal required.
|
|
1036
|
+
</div>
|
|
1037
|
+
<div id="svc-status-row" style="display:flex;align-items:center;gap:8px;margin-bottom:10px;">
|
|
1038
|
+
<span id="svc-status-badge" class="svc-badge svc-unknown">checking…</span>
|
|
1039
|
+
<span id="svc-method" style="font-size:11px;color:var(--muted)"></span>
|
|
1040
|
+
</div>
|
|
1041
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px;">
|
|
1042
|
+
<button class="btn btn-green" id="svc-install-btn" onclick="svcAction('install')">↓ Install Service</button>
|
|
1043
|
+
<button class="btn btn-ghost" id="svc-start-btn" onclick="svcAction('start')">▶ Start Now</button>
|
|
1044
|
+
<button class="btn btn-ghost" id="svc-stop-btn" onclick="svcAction('stop')">■ Stop Hub</button>
|
|
1045
|
+
<button class="btn btn-danger" id="svc-uninstall-btn" onclick="svcAction('uninstall')">✕ Uninstall</button>
|
|
1046
|
+
</div>
|
|
1047
|
+
<div id="svc-log-row" style="font-size:11px;color:var(--muted);display:none;">
|
|
1048
|
+
Log: <span id="svc-log-path" style="font-family:monospace;color:var(--text2)"></span>
|
|
1049
|
+
</div>
|
|
1050
|
+
<pre class="cmd-output hidden" id="svc-output" style="min-height:50px;max-height:120px;margin-top:8px;"></pre>
|
|
1051
|
+
</div>
|
|
1052
|
+
<div class="modal-footer">
|
|
1053
|
+
<button class="btn btn-ghost" onclick="closeModal('settings-modal')">Close</button>
|
|
1054
|
+
<button class="btn btn-primary" id="settings-save-btn" onclick="saveSettings()">Save Settings</button>
|
|
1055
|
+
</div>
|
|
1056
|
+
</div>
|
|
1057
|
+
</div>
|
|
1058
|
+
|
|
1059
|
+
<!-- Update Project Modal -->
|
|
1060
|
+
<div class="modal-backdrop hidden" id="update-modal">
|
|
1061
|
+
<div class="modal">
|
|
1062
|
+
<div class="modal-header">
|
|
1063
|
+
<h3>🚀 Update Project C3 Version</h3>
|
|
1064
|
+
<button class="modal-close" onclick="closeModal('update-modal')">×</button>
|
|
1065
|
+
</div>
|
|
1066
|
+
<div class="modal-body">
|
|
1067
|
+
<div class="form-group">
|
|
1068
|
+
<span class="form-label">Project</span>
|
|
1069
|
+
<div class="form-path" id="update-modal-path"></div>
|
|
1070
|
+
</div>
|
|
1071
|
+
<div class="settings-info" style="margin-bottom: 15px;">
|
|
1072
|
+
<p>This will update the project's C3 configuration and instruction files to match the current hub version (<b>v<span id="update-modal-version">…</span></b>).</p>
|
|
1073
|
+
<ul style="margin-left: 20px; margin-top: 8px; font-size: 12px; color: var(--text2);">
|
|
1074
|
+
<li>Rebuilds the code index</li>
|
|
1075
|
+
<li>Refreshes instruction files (CLAUDE.md, etc.)</li>
|
|
1076
|
+
<li>Updates MCP tool definitions</li>
|
|
1077
|
+
<li>Ensures C3 subdirectories exist</li>
|
|
1078
|
+
</ul>
|
|
1079
|
+
</div>
|
|
1080
|
+
<div class="form-group">
|
|
1081
|
+
<label class="form-label" for="update-modal-ide">Target IDE</label>
|
|
1082
|
+
<select class="form-select" id="update-modal-ide">
|
|
1083
|
+
<option value="auto">Auto-detect from markers</option>
|
|
1084
|
+
<option value="claude-code">Claude Code CLI</option>
|
|
1085
|
+
<option value="claude-app">Claude Code App</option>
|
|
1086
|
+
<option value="vscode">VS Code Copilot</option>
|
|
1087
|
+
<option value="cursor">Cursor</option>
|
|
1088
|
+
<option value="codex">OpenAI Codex</option>
|
|
1089
|
+
<option value="gemini">Gemini CLI</option>
|
|
1090
|
+
<option value="antigravity">Antigravity</option>
|
|
1091
|
+
</select>
|
|
1092
|
+
</div>
|
|
1093
|
+
<pre class="cmd-output hidden" id="update-cmd-output" style="max-height: 300px;"></pre>
|
|
1094
|
+
</div>
|
|
1095
|
+
<div class="modal-footer">
|
|
1096
|
+
<button class="btn btn-ghost" onclick="closeModal('update-modal')">Cancel</button>
|
|
1097
|
+
<button class="btn btn-primary" id="update-run-btn" onclick="runUpdate()">Confirm Update</button>
|
|
1098
|
+
</div>
|
|
1099
|
+
</div>
|
|
1100
|
+
</div>
|
|
1101
|
+
|
|
1102
|
+
<!-- Batch Update Modal -->
|
|
1103
|
+
<div class="modal-backdrop hidden" id="batch-update-modal">
|
|
1104
|
+
<div class="modal">
|
|
1105
|
+
<div class="modal-header">
|
|
1106
|
+
<h3>Batch Update Projects</h3>
|
|
1107
|
+
<button class="modal-close" onclick="closeModal('batch-update-modal')">×</button>
|
|
1108
|
+
</div>
|
|
1109
|
+
<div class="modal-body">
|
|
1110
|
+
<div class="settings-info" id="batch-update-info">
|
|
1111
|
+
Updating all outdated projects to <b>v<span id="batch-update-version">…</span></b>.
|
|
1112
|
+
This runs <code>c3 init --force</code> for each project.
|
|
1113
|
+
</div>
|
|
1114
|
+
<div class="batch-progress-bar" id="batch-progress-bar" style="display:none;">
|
|
1115
|
+
<div class="batch-progress-fill" id="batch-progress-fill" style="width:0%"></div>
|
|
1116
|
+
</div>
|
|
1117
|
+
<div id="batch-update-list" style="display:flex; flex-direction:column; gap:6px; margin-top:10px;"></div>
|
|
1118
|
+
<div id="batch-summary" class="batch-summary" style="display:none;"></div>
|
|
1119
|
+
<pre class="cmd-output hidden" id="batch-update-log" style="max-height: 200px; font-size: 11px;"></pre>
|
|
1120
|
+
</div>
|
|
1121
|
+
<div class="modal-footer">
|
|
1122
|
+
<button class="btn btn-ghost" id="batch-update-cancel" onclick="cancelBatchUpdate()">Cancel</button>
|
|
1123
|
+
<button class="btn btn-danger" id="batch-cancel-running" onclick="cancelBatchUpdate()" style="display:none;">Cancel Update</button>
|
|
1124
|
+
<button class="btn btn-primary" id="batch-update-run-btn" onclick="runBatchUpdate()">Start Batch Update</button>
|
|
1125
|
+
<button class="btn btn-yellow" id="batch-retry-btn" onclick="retryFailedBatch()" style="display:none;">Retry Failed</button>
|
|
1126
|
+
</div>
|
|
1127
|
+
</div>
|
|
1128
|
+
</div>
|
|
1129
|
+
|
|
1130
|
+
<!-- IDE Launcher Modal -->
|
|
1131
|
+
<div class="modal-backdrop hidden" id="ide-modal">
|
|
1132
|
+
<div class="modal">
|
|
1133
|
+
<div class="modal-header">
|
|
1134
|
+
<h3>Open in IDE / CLI</h3>
|
|
1135
|
+
<button class="modal-close" onclick="closeModal('ide-modal')">×</button>
|
|
1136
|
+
</div>
|
|
1137
|
+
<div class="modal-body">
|
|
1138
|
+
<div class="form-group">
|
|
1139
|
+
<span class="form-label">Project</span>
|
|
1140
|
+
<div class="form-path" id="ide-modal-path"></div>
|
|
1141
|
+
</div>
|
|
1142
|
+
<div class="form-group">
|
|
1143
|
+
<span class="form-label">Choose IDE / CLI</span>
|
|
1144
|
+
<div class="ide-picker" id="ide-picker"></div>
|
|
1145
|
+
</div>
|
|
1146
|
+
<div class="form-group" id="ide-custom-row" style="display:none">
|
|
1147
|
+
<label class="form-label" for="ide-custom-cmd">Custom command</label>
|
|
1148
|
+
<input class="form-input" type="text" id="ide-custom-cmd" placeholder="e.g. nvim" style="font-family:monospace">
|
|
1149
|
+
</div>
|
|
1150
|
+
<pre class="cmd-output hidden" id="ide-cmd-output" style="min-height:auto; padding:8px 10px;"></pre>
|
|
1151
|
+
</div>
|
|
1152
|
+
<div class="modal-footer">
|
|
1153
|
+
<button class="btn btn-ghost" onclick="closeModal('ide-modal')">Cancel</button>
|
|
1154
|
+
<button class="btn btn-primary" id="ide-launch-btn" onclick="launchIde()">▶ Launch</button>
|
|
1155
|
+
</div>
|
|
1156
|
+
</div>
|
|
1157
|
+
</div>
|
|
1158
|
+
|
|
1159
|
+
<div id="toast"></div>
|
|
1160
|
+
<div class="toast-container" id="toast-container"></div>
|
|
1161
|
+
<div id="confirm-root"></div>
|
|
1162
|
+
|
|
1163
|
+
<script>
|
|
1164
|
+
// ── State ──────────────────────────────────────────────────────────────────
|
|
1165
|
+
let allProjects = [];
|
|
1166
|
+
let hubRestarting = false;
|
|
1167
|
+
let currentFilter = 'all';
|
|
1168
|
+
let currentGroup = 'all';
|
|
1169
|
+
let sidebarCollapsed = false;
|
|
1170
|
+
let collapsedFolders = new Set();
|
|
1171
|
+
let currentTheme = 'dark';
|
|
1172
|
+
let currentProjectsView = 'list';
|
|
1173
|
+
const BUILD_TIME = "2026-03-10 07:22";
|
|
1174
|
+
let hubVersion = '?';
|
|
1175
|
+
let hubDedicatedPort = null;
|
|
1176
|
+
let hubRunningPort = location.port || 3330;
|
|
1177
|
+
let pollTimer = null;
|
|
1178
|
+
let detailsCache = {};
|
|
1179
|
+
const launchingPaths = new Set();
|
|
1180
|
+
let sessionDrawerOpen = true;
|
|
1181
|
+
let sessionDrawerTabs = [];
|
|
1182
|
+
let activeSessionTab = null;
|
|
1183
|
+
let sessionPollTimer = null;
|
|
1184
|
+
|
|
1185
|
+
// Modal state
|
|
1186
|
+
let modalPath = null;
|
|
1187
|
+
let editPath = null;
|
|
1188
|
+
let transferPath = null;
|
|
1189
|
+
let idePath = null;
|
|
1190
|
+
let ideSelected = null;
|
|
1191
|
+
let mcpModalPath = null;
|
|
1192
|
+
let c3SetupPath = null;
|
|
1193
|
+
let mcpCapabilities = null;
|
|
1194
|
+
|
|
1195
|
+
const IDE_LABELS = {
|
|
1196
|
+
'claude-code': 'Claude Code CLI',
|
|
1197
|
+
'claude-app': 'Claude Code App',
|
|
1198
|
+
'vscode': 'VS Code',
|
|
1199
|
+
'cursor': 'Cursor',
|
|
1200
|
+
'codex': 'Codex CLI',
|
|
1201
|
+
'gemini': 'Gemini CLI',
|
|
1202
|
+
'antigravity': 'Antigravity',
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
const IDE_OPTIONS = [
|
|
1206
|
+
{ id: 'claude-code', name: 'Claude Code CLI', icon: '\u{1F916}', cmd: 'claude' },
|
|
1207
|
+
{ id: 'claude-app', name: 'Claude Code App', icon: '\u{1F4D0}', cmd: 'claude-app' },
|
|
1208
|
+
{ id: 'codex', name: 'Codex CLI', icon: '\u{1F9E0}', cmd: 'codex' },
|
|
1209
|
+
{ id: 'gemini', name: 'Gemini CLI', icon: '\u{1F48E}', cmd: 'gemini' },
|
|
1210
|
+
{ id: 'antigravity', name: 'Antigravity', icon: '\u{1F680}', cmd: 'antigravity' },
|
|
1211
|
+
{ id: 'vscode', name: 'VS Code', icon: '\u{1F4BB}', cmd: 'code' },
|
|
1212
|
+
{ id: 'cursor', name: 'Cursor', icon: '\u26A1', cmd: 'cursor' },
|
|
1213
|
+
{ id: 'custom', name: 'Custom', icon: '\u2328', cmd: '...' },
|
|
1214
|
+
];
|
|
1215
|
+
|
|
1216
|
+
// ── Boot ───────────────────────────────────────────────────────────────────
|
|
1217
|
+
document.addEventListener('DOMContentLoaded', async () => {
|
|
1218
|
+
try {
|
|
1219
|
+
const r = await fetch('/api/version');
|
|
1220
|
+
const d = await r.json();
|
|
1221
|
+
hubVersion = d.c3_version || '?';
|
|
1222
|
+
document.getElementById('hub-version').textContent = 'C3 v' + hubVersion + ' · ' + BUILD_TIME;
|
|
1223
|
+
} catch(e) {}
|
|
1224
|
+
|
|
1225
|
+
try {
|
|
1226
|
+
const r = await fetch('/api/hub/config');
|
|
1227
|
+
const d = await r.json();
|
|
1228
|
+
hubDedicatedPort = d.port || 3330;
|
|
1229
|
+
currentTheme = d.theme || 'dark';
|
|
1230
|
+
currentProjectsView = d.projects_view || 'list';
|
|
1231
|
+
currentGroup = d.sidebar_group || 'all';
|
|
1232
|
+
sidebarCollapsed = !!d.sidebar_collapsed;
|
|
1233
|
+
if (sidebarCollapsed) document.querySelector('main').classList.add('sidebar-collapsed');
|
|
1234
|
+
document.getElementById('hub-port-badge').textContent = ':' + hubDedicatedPort;
|
|
1235
|
+
applyTheme(currentTheme);
|
|
1236
|
+
syncProjectsViewButtons();
|
|
1237
|
+
// Oracle link detection
|
|
1238
|
+
const oracleUrl = d.oracle_url || 'http://localhost:3331';
|
|
1239
|
+
if (oracleUrl) {
|
|
1240
|
+
fetch(oracleUrl + '/api/health', {signal: AbortSignal.timeout(2000)})
|
|
1241
|
+
.then(r => r.json())
|
|
1242
|
+
.then(h => { if (h.service === 'c3-oracle') { const el = document.getElementById('oracle-link'); el.href = oracleUrl; el.style.display = ''; }})
|
|
1243
|
+
.catch(() => {});
|
|
1244
|
+
}
|
|
1245
|
+
} catch(e) {}
|
|
1246
|
+
|
|
1247
|
+
applyTheme(currentTheme);
|
|
1248
|
+
syncProjectsViewButtons();
|
|
1249
|
+
|
|
1250
|
+
loadProjects();
|
|
1251
|
+
schedulePoll();
|
|
1252
|
+
renderSessionDrawer();
|
|
1253
|
+
|
|
1254
|
+
try {
|
|
1255
|
+
const r = await fetch('/api/projects/mcp-capabilities');
|
|
1256
|
+
mcpCapabilities = await r.json();
|
|
1257
|
+
} catch(e) {}
|
|
1258
|
+
|
|
1259
|
+
['add-path','add-name'].forEach(id =>
|
|
1260
|
+
document.getElementById(id).addEventListener('keydown', e => { if (e.key === 'Enter') addProject(); })
|
|
1261
|
+
);
|
|
1262
|
+
document.addEventListener('keydown', e => {
|
|
1263
|
+
if (e.key === 'Escape') {
|
|
1264
|
+
['mcp-modal','edit-modal','transfer-modal','settings-modal','update-modal','batch-update-modal','ide-modal','c3-setup-modal'].forEach(id => {
|
|
1265
|
+
if (!document.getElementById(id).classList.contains('hidden')) closeModal(id);
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
function schedulePoll() {
|
|
1272
|
+
clearTimeout(pollTimer);
|
|
1273
|
+
pollTimer = setTimeout(() => { loadProjects(); schedulePoll(); }, 5000);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function scheduleSessionPoll() {
|
|
1277
|
+
clearTimeout(sessionPollTimer);
|
|
1278
|
+
if (!sessionDrawerTabs.length) return;
|
|
1279
|
+
sessionPollTimer = setTimeout(async () => {
|
|
1280
|
+
await refreshSessionDrawerTabs();
|
|
1281
|
+
scheduleSessionPoll();
|
|
1282
|
+
}, 4000);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// ── Data ───────────────────────────────────────────────────────────────────
|
|
1286
|
+
async function loadProjects() {
|
|
1287
|
+
try {
|
|
1288
|
+
const r = await fetch('/api/projects?ts=' + Date.now(), {cache: 'no-store'});
|
|
1289
|
+
allProjects = await r.json();
|
|
1290
|
+
detailsCache = {};
|
|
1291
|
+
renderSidebar();
|
|
1292
|
+
renderProjects();
|
|
1293
|
+
} catch(e) {
|
|
1294
|
+
if (!hubRestarting) toast('Failed to load projects', 'err');
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
async function refreshHub() {
|
|
1299
|
+
await loadProjects();
|
|
1300
|
+
await refreshSessionDrawerTabs();
|
|
1301
|
+
toast('Projects refreshed', 'ok');
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
async function restartHub() {
|
|
1305
|
+
const btn = document.querySelector('.btn-restart');
|
|
1306
|
+
btn.disabled = true;
|
|
1307
|
+
hubRestarting = true;
|
|
1308
|
+
toast('Restarting hub…', 'info');
|
|
1309
|
+
try {
|
|
1310
|
+
await fetch('/api/hub/restart', {method: 'POST'});
|
|
1311
|
+
} catch(e) {}
|
|
1312
|
+
// Poll until the hub is confirmed back up (checks service identity to avoid
|
|
1313
|
+
// a false positive against a different server on this port)
|
|
1314
|
+
let attempts = 0;
|
|
1315
|
+
const poll = setInterval(async () => {
|
|
1316
|
+
attempts++;
|
|
1317
|
+
try {
|
|
1318
|
+
const r = await fetch('/api/health', {cache: 'no-store'});
|
|
1319
|
+
if (r.ok) {
|
|
1320
|
+
const d = await r.json().catch(() => ({}));
|
|
1321
|
+
if (d.service === 'c3-hub') {
|
|
1322
|
+
clearInterval(poll);
|
|
1323
|
+
btn.disabled = false;
|
|
1324
|
+
hubRestarting = false;
|
|
1325
|
+
toast('Hub restarted', 'ok');
|
|
1326
|
+
loadProjects();
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
} catch(e) {}
|
|
1331
|
+
if (attempts > 40) {
|
|
1332
|
+
clearInterval(poll);
|
|
1333
|
+
btn.disabled = false;
|
|
1334
|
+
hubRestarting = false;
|
|
1335
|
+
toast('Hub restart timed out — reload the page manually', 'err');
|
|
1336
|
+
}
|
|
1337
|
+
}, 500);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
async function detachFromTerminal() {
|
|
1341
|
+
const btn = document.getElementById('detach-terminal-btn');
|
|
1342
|
+
const ok = await confirmDialog({ title: 'Detach from Terminal', message: 'Restart hub as a background process? The terminal window will be freed.', confirmText: 'Detach' });
|
|
1343
|
+
if (!ok) return;
|
|
1344
|
+
btn.disabled = true;
|
|
1345
|
+
hubRestarting = true;
|
|
1346
|
+
closeModal('settings-modal');
|
|
1347
|
+
toast('Detaching from terminal…', 'info');
|
|
1348
|
+
try {
|
|
1349
|
+
await fetch('/api/hub/restart', {method: 'POST'});
|
|
1350
|
+
} catch(e) {}
|
|
1351
|
+
let attempts = 0;
|
|
1352
|
+
const poll = setInterval(async () => {
|
|
1353
|
+
attempts++;
|
|
1354
|
+
try {
|
|
1355
|
+
const r = await fetch('/api/health', {cache: 'no-store'});
|
|
1356
|
+
if (r.ok) {
|
|
1357
|
+
const d = await r.json().catch(() => ({}));
|
|
1358
|
+
if (d.service === 'c3-hub') {
|
|
1359
|
+
clearInterval(poll);
|
|
1360
|
+
hubRestarting = false;
|
|
1361
|
+
toast('Hub running in background (no terminal)', 'ok');
|
|
1362
|
+
loadProjects();
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
} catch(e) {}
|
|
1367
|
+
if (attempts > 40) {
|
|
1368
|
+
clearInterval(poll);
|
|
1369
|
+
hubRestarting = false;
|
|
1370
|
+
toast('Hub restart timed out — reload the page manually', 'err');
|
|
1371
|
+
}
|
|
1372
|
+
}, 500);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// ── Render ─────────────────────────────────────────────────────────────────
|
|
1376
|
+
function setFilter(f, el) {
|
|
1377
|
+
currentFilter = f;
|
|
1378
|
+
if (currentGroup === '_active' || currentGroup === '_idle') {
|
|
1379
|
+
currentGroup = 'all';
|
|
1380
|
+
renderSidebar();
|
|
1381
|
+
}
|
|
1382
|
+
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
|
1383
|
+
el.classList.add('active');
|
|
1384
|
+
renderProjects();
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function applyTheme(theme) {
|
|
1388
|
+
currentTheme = theme === 'light' ? 'light' : 'dark';
|
|
1389
|
+
document.body.setAttribute('data-theme', currentTheme);
|
|
1390
|
+
const btn = document.getElementById('theme-toggle-btn');
|
|
1391
|
+
if (btn) btn.textContent = 'Theme: ' + (currentTheme === 'light' ? 'Light' : 'Dark');
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
async function toggleTheme() {
|
|
1395
|
+
applyTheme(currentTheme === 'dark' ? 'light' : 'dark');
|
|
1396
|
+
await saveHubPrefs({theme: currentTheme});
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function syncProjectsViewButtons() {
|
|
1400
|
+
document.getElementById('view-list-btn').classList.toggle('active', currentProjectsView === 'list');
|
|
1401
|
+
document.getElementById('view-grid-btn').classList.toggle('active', currentProjectsView === 'grid');
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
async function setProjectsView(mode) {
|
|
1405
|
+
currentProjectsView = mode === 'grid' ? 'grid' : 'list';
|
|
1406
|
+
syncProjectsViewButtons();
|
|
1407
|
+
renderProjects();
|
|
1408
|
+
await saveHubPrefs({projects_view: currentProjectsView});
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
async function saveHubPrefs(patch) {
|
|
1412
|
+
try {
|
|
1413
|
+
await fetch('/api/hub/config', {
|
|
1414
|
+
method: 'POST',
|
|
1415
|
+
headers: {'Content-Type':'application/json'},
|
|
1416
|
+
body: JSON.stringify(patch),
|
|
1417
|
+
});
|
|
1418
|
+
} catch (e) {}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// ── Sidebar ────────────────────────────────────────────────────────────────
|
|
1422
|
+
|
|
1423
|
+
function buildTagTree(projects) {
|
|
1424
|
+
// Build a tree from tags like "clients/acme" → { clients: { acme: {} } }
|
|
1425
|
+
// Each node: { display, total, active, children: {} }
|
|
1426
|
+
const root = { display: '', total: 0, active: 0, children: {} };
|
|
1427
|
+
let untaggedTotal = 0, untaggedActive = 0;
|
|
1428
|
+
|
|
1429
|
+
projects.forEach(p => {
|
|
1430
|
+
const tags = Array.isArray(p.tags) ? p.tags : [];
|
|
1431
|
+
if (tags.length === 0) {
|
|
1432
|
+
untaggedTotal++;
|
|
1433
|
+
if (p.active) untaggedActive++;
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
tags.forEach(tag => {
|
|
1437
|
+
const parts = tag.split('/').filter(Boolean);
|
|
1438
|
+
let node = root;
|
|
1439
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1440
|
+
const seg = parts[i];
|
|
1441
|
+
const key = seg.toLowerCase();
|
|
1442
|
+
if (!node.children[key]) {
|
|
1443
|
+
node.children[key] = { display: seg, total: 0, active: 0, children: {} };
|
|
1444
|
+
}
|
|
1445
|
+
node.children[key].total++;
|
|
1446
|
+
if (p.active) node.children[key].active++;
|
|
1447
|
+
node = node.children[key];
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
return { root, untaggedTotal, untaggedActive };
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function renderTreeItems(children, depth, pathPrefix) {
|
|
1456
|
+
let h = '';
|
|
1457
|
+
const keys = Object.keys(children).sort((a, b) => a.localeCompare(b));
|
|
1458
|
+
keys.forEach(key => {
|
|
1459
|
+
const node = children[key];
|
|
1460
|
+
const fullPath = pathPrefix ? pathPrefix + '/' + node.display : node.display;
|
|
1461
|
+
const hasKids = Object.keys(node.children).length > 0;
|
|
1462
|
+
const collapsed = collapsedFolders.has(fullPath.toLowerCase());
|
|
1463
|
+
const isActive = currentGroup.toLowerCase() === fullPath.toLowerCase();
|
|
1464
|
+
const cnt = node.active > 0 ? node.active + '/' + node.total : '' + node.total;
|
|
1465
|
+
|
|
1466
|
+
let toggleHtml = '';
|
|
1467
|
+
if (hasKids) {
|
|
1468
|
+
toggleHtml = '<span class="sidebar-folder-toggle" onclick="event.stopPropagation();toggleFolder(\'' + esc(fullPath).replace(/'/g,"\\'") + '\')">'
|
|
1469
|
+
+ (collapsed ? '▸' : '▾') + '</span>';
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
h += '<div class="sidebar-item' + (isActive ? ' active' : '') + '" data-depth="' + depth + '" '
|
|
1473
|
+
+ 'onclick="selectGroup(\'' + esc(fullPath).replace(/'/g,"\\'") + '\')" '
|
|
1474
|
+
+ 'title="' + esc(fullPath) + ' (' + node.total + ' projects)">'
|
|
1475
|
+
+ toggleHtml
|
|
1476
|
+
+ '<span class="sidebar-item-label">' + esc(node.display) + '</span>'
|
|
1477
|
+
+ '<span class="sidebar-item-count">' + cnt + '</span></div>';
|
|
1478
|
+
|
|
1479
|
+
if (hasKids && !collapsed) {
|
|
1480
|
+
h += renderTreeItems(node.children, depth + 1, fullPath);
|
|
1481
|
+
}
|
|
1482
|
+
});
|
|
1483
|
+
return h;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
function renderSidebar() {
|
|
1487
|
+
const nav = document.getElementById('sidebar-nav');
|
|
1488
|
+
if (!nav) return;
|
|
1489
|
+
|
|
1490
|
+
const { root, untaggedTotal, untaggedActive } = buildTagTree(allProjects);
|
|
1491
|
+
const totalActive = allProjects.filter(p => p.active).length;
|
|
1492
|
+
let h = '';
|
|
1493
|
+
|
|
1494
|
+
// Fixed items
|
|
1495
|
+
h += sidebarFixedItem('all', 'All Projects', allProjects.length, totalActive);
|
|
1496
|
+
h += '<div class="sidebar-divider"></div>';
|
|
1497
|
+
h += sidebarFixedItem('_active', 'Active', totalActive, totalActive);
|
|
1498
|
+
h += sidebarFixedItem('_idle', 'Idle', allProjects.length - totalActive, 0);
|
|
1499
|
+
|
|
1500
|
+
// Tag tree
|
|
1501
|
+
if (Object.keys(root.children).length > 0 || untaggedTotal > 0) {
|
|
1502
|
+
h += '<div class="sidebar-divider"></div>';
|
|
1503
|
+
h += renderTreeItems(root.children, 0, '');
|
|
1504
|
+
if (untaggedTotal > 0) {
|
|
1505
|
+
h += sidebarFixedItem('untagged', 'Untagged', untaggedTotal, untaggedActive);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
nav.innerHTML = h;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function sidebarFixedItem(key, label, total, active) {
|
|
1513
|
+
const isActive = currentGroup.toLowerCase() === key.toLowerCase();
|
|
1514
|
+
const cnt = active > 0 ? active + '/' + total : '' + total;
|
|
1515
|
+
return '<div class="sidebar-item' + (isActive ? ' active' : '') + '" onclick="selectGroup(\'' + esc(key).replace(/'/g,"\\'") + '\')" title="' + esc(label) + ' (' + total + ' projects)">'
|
|
1516
|
+
+ '<span class="sidebar-item-label">' + esc(label) + '</span>'
|
|
1517
|
+
+ '<span class="sidebar-item-count">' + cnt + '</span></div>';
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
function selectGroup(group) {
|
|
1521
|
+
currentGroup = group;
|
|
1522
|
+
if (group === '_active') {
|
|
1523
|
+
currentFilter = 'active';
|
|
1524
|
+
syncFilterButtons();
|
|
1525
|
+
} else if (group === '_idle') {
|
|
1526
|
+
currentFilter = 'idle';
|
|
1527
|
+
syncFilterButtons();
|
|
1528
|
+
} else {
|
|
1529
|
+
currentFilter = 'all';
|
|
1530
|
+
syncFilterButtons();
|
|
1531
|
+
}
|
|
1532
|
+
renderSidebar();
|
|
1533
|
+
renderProjects();
|
|
1534
|
+
saveHubPrefs({ sidebar_group: currentGroup });
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
function toggleFolder(path) {
|
|
1538
|
+
const key = path.toLowerCase();
|
|
1539
|
+
if (collapsedFolders.has(key)) collapsedFolders.delete(key);
|
|
1540
|
+
else collapsedFolders.add(key);
|
|
1541
|
+
renderSidebar();
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
function syncFilterButtons() {
|
|
1545
|
+
document.querySelectorAll('.filter-btn').forEach(b => {
|
|
1546
|
+
const f = b.textContent.trim().toLowerCase();
|
|
1547
|
+
b.classList.toggle('active', f === currentFilter);
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
function toggleSidebar() {
|
|
1552
|
+
sidebarCollapsed = !sidebarCollapsed;
|
|
1553
|
+
document.querySelector('main').classList.toggle('sidebar-collapsed', sidebarCollapsed);
|
|
1554
|
+
saveHubPrefs({ sidebar_collapsed: sidebarCollapsed });
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// ── Projects ───────────────────────────────────────────────────────────────
|
|
1558
|
+
|
|
1559
|
+
function renderProjects() {
|
|
1560
|
+
const query = (document.getElementById('search-input').value || '').toLowerCase().trim();
|
|
1561
|
+
let list = allProjects.filter(p => {
|
|
1562
|
+
// Group filter
|
|
1563
|
+
if (currentGroup === 'untagged') {
|
|
1564
|
+
if (Array.isArray(p.tags) && p.tags.length > 0) return false;
|
|
1565
|
+
} else if (currentGroup === '_active') {
|
|
1566
|
+
if (!p.active) return false;
|
|
1567
|
+
} else if (currentGroup === '_idle') {
|
|
1568
|
+
if (p.active) return false;
|
|
1569
|
+
} else if (currentGroup !== 'all') {
|
|
1570
|
+
const tags = (Array.isArray(p.tags) ? p.tags : []).map(t => t.toLowerCase());
|
|
1571
|
+
const g = currentGroup.toLowerCase();
|
|
1572
|
+
// Prefix match: selecting "clients" shows "clients", "clients/acme", etc.
|
|
1573
|
+
const match = tags.some(t => t === g || t.startsWith(g + '/'));
|
|
1574
|
+
if (!match) return false;
|
|
1575
|
+
}
|
|
1576
|
+
// Status filter (skip if group already handles status)
|
|
1577
|
+
if (currentGroup !== '_active' && currentGroup !== '_idle') {
|
|
1578
|
+
if (currentFilter === 'active' && !p.active) return false;
|
|
1579
|
+
if (currentFilter === 'idle' && p.active) return false;
|
|
1580
|
+
}
|
|
1581
|
+
if (query) {
|
|
1582
|
+
const hay = ((p.name||'') + ' ' + (p.path||'') + ' ' + ((p.tags||[]).join(' ')) + ' ' + (p.ide||'')).toLowerCase();
|
|
1583
|
+
if (!hay.includes(query)) return false;
|
|
1584
|
+
}
|
|
1585
|
+
return true;
|
|
1586
|
+
});
|
|
1587
|
+
const active = allProjects.filter(p => p.active).length;
|
|
1588
|
+
|
|
1589
|
+
// Show/hide batch update button
|
|
1590
|
+
const outdatedCount = allProjects.filter(p => p.c3_version && p.c3_version !== hubVersion).length;
|
|
1591
|
+
const updateAllBtn = document.getElementById('update-all-btn');
|
|
1592
|
+
if (updateAllBtn) {
|
|
1593
|
+
updateAllBtn.style.display = outdatedCount > 1 ? 'inline-block' : 'none';
|
|
1594
|
+
updateAllBtn.textContent = `🚀 Update All (${outdatedCount})`;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
document.getElementById('session-count').textContent = active + ' active';
|
|
1598
|
+
document.getElementById('session-badge').classList.toggle('has-active', active > 0);
|
|
1599
|
+
document.getElementById('project-count').textContent =
|
|
1600
|
+
`${allProjects.length} Project${allProjects.length !== 1 ? 's' : ''}` +
|
|
1601
|
+
(list.length !== allProjects.length ? ` · ${list.length} shown` : '');
|
|
1602
|
+
|
|
1603
|
+
const grid = document.getElementById('projects-grid');
|
|
1604
|
+
grid.classList.toggle('grid-view', currentProjectsView === 'grid');
|
|
1605
|
+
if (list.length === 0) {
|
|
1606
|
+
grid.innerHTML = `<div class="empty-state"><div class="icon">${currentFilter === 'all' && !query ? '📂' : '🔍'}</div><p>${currentFilter === 'all' && !query ? 'No projects yet. Add one above.' : 'No matching projects.'}</p></div>`;
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
grid.innerHTML = list.map(projectCard).join('');
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
function cardId(path) {
|
|
1613
|
+
return 'c' + btoa(encodeURIComponent(path)).replace(/[^a-zA-Z0-9]/g,'').slice(0,20);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
function projectCard(p) {
|
|
1617
|
+
const active = !!p.active || !!p.session_active;
|
|
1618
|
+
const cid = cardId(p.path);
|
|
1619
|
+
const accessible = p.accessible !== false;
|
|
1620
|
+
|
|
1621
|
+
// Accessibility badge
|
|
1622
|
+
let accessBadge = '';
|
|
1623
|
+
if (!accessible) {
|
|
1624
|
+
accessBadge = `<span class="ver-badge outdated" title="Project path not accessible — drive may not be mounted yet">⚠ offline</span>`;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// Version badge
|
|
1628
|
+
let verBadge = '';
|
|
1629
|
+
if (!p.initialized && p.c3_version == null) {
|
|
1630
|
+
verBadge = `<span class="ver-badge uninit" title="Project not initialized with C3">not initialized</span>`;
|
|
1631
|
+
} else if (p.c3_version) {
|
|
1632
|
+
const outdated = hubVersion !== '?' && p.c3_version !== hubVersion;
|
|
1633
|
+
let upBtn = outdated ? `<button class="btn-sm btn-update-project" style="margin-left: 5px; color: var(--yellow); border-color: var(--yellow); background: transparent;" onclick="updateProject('${jsq(p.path)}')">update</button>` : '';
|
|
1634
|
+
verBadge = `<span class="ver-badge ${outdated ? 'outdated' : ''}" title="${outdated ? 'Hub is v' + hubVersion + ', project is v' + p.c3_version : 'v' + p.c3_version}">v${esc(p.c3_version)}${outdated ? ' ⚠' : ''}</span>${upBtn}`;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// MCP badge
|
|
1638
|
+
let mcpBadge = '';
|
|
1639
|
+
if (p.mcp_installed === true) {
|
|
1640
|
+
const mode = p.mcp_mode ? ` · ${p.mcp_mode}` : '';
|
|
1641
|
+
mcpBadge = `<span class="mcp-badge ok">MCP${esc(mode)}</span>`;
|
|
1642
|
+
} else if (p.mcp_installed === false) {
|
|
1643
|
+
mcpBadge = `<span class="mcp-badge none">no MCP</span>`;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// Budget badge (active only)
|
|
1647
|
+
let budgetBadge = '';
|
|
1648
|
+
if (active && p.budget) {
|
|
1649
|
+
const b = p.budget;
|
|
1650
|
+
const tokens = b.response_tokens || 0;
|
|
1651
|
+
const threshold = b.threshold || 35000;
|
|
1652
|
+
|
|
1653
|
+
const pct = Math.min(100, Math.round((tokens / threshold) * 100));
|
|
1654
|
+
const cls = tokens >= threshold ? 'critical' : '';
|
|
1655
|
+
const label = tokens > 1000 ? Math.round(tokens/1000) + 'k' : tokens;
|
|
1656
|
+
|
|
1657
|
+
budgetBadge = `
|
|
1658
|
+
<div class="budget-badge" title="Context Budget: ${tokens.toLocaleString()} / ${threshold.toLocaleString()} tokens">
|
|
1659
|
+
<span class="budget-bar"><div class="budget-fill ${cls}" style="width: ${pct}%"></div></span>
|
|
1660
|
+
<span>${label}</span>
|
|
1661
|
+
</div>`;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// Notification badge
|
|
1665
|
+
const nc = p.notification_count || 0;
|
|
1666
|
+
const notifBadge = nc > 0
|
|
1667
|
+
? `<span class="notif-badge${nc >= 10 ? ' critical' : ''}" title="${nc} notification${nc !== 1 ? 's' : ''}">🔔 ${nc}</span>`
|
|
1668
|
+
: '';
|
|
1669
|
+
const clearNotifBtn = nc > 0
|
|
1670
|
+
? `<button class="btn btn-xs btn-ghost" onclick="event.stopPropagation();clearSessionNotifications('${jsq(p.path)}')" title="Clear ${nc} notification${nc !== 1 ? 's' : ''}">🔕 Clear</button>`
|
|
1671
|
+
: '';
|
|
1672
|
+
|
|
1673
|
+
const statusPill = active
|
|
1674
|
+
? `<span class="status-pill active">● ACTIVE</span>`
|
|
1675
|
+
: `<span class="status-pill stopped">○ idle</span>`;
|
|
1676
|
+
|
|
1677
|
+
const portVal = active && p.port != null
|
|
1678
|
+
? `<span class="port-link" onclick="openSession(${p.port})" title="Open C3 UI">:${p.port}</span>`
|
|
1679
|
+
: active && p.session_active ? `<span title="Active MCP session (no UI server)">MCP</span>`
|
|
1680
|
+
: `—`;
|
|
1681
|
+
|
|
1682
|
+
// Tags + notes
|
|
1683
|
+
const tags = Array.isArray(p.tags) ? p.tags : [];
|
|
1684
|
+
const tagChips = tags.map(t => `<span class="tag-chip">${esc(t)}</span>`).join('');
|
|
1685
|
+
const noteIcon = p.notes ? `<span class="note-icon" title="${esc(p.notes)}">📝</span>` : '';
|
|
1686
|
+
const hasTags = tagChips || noteIcon;
|
|
1687
|
+
|
|
1688
|
+
const last = formatProjectLastSession(p.last_session);
|
|
1689
|
+
const added = p.added_at ? p.added_at.slice(0,10) : '?';
|
|
1690
|
+
const pathDisplay = p.path.length > 70 ? '…' + p.path.slice(-67) : p.path;
|
|
1691
|
+
const ide = IDE_LABELS[p.ide] || p.ide || '?';
|
|
1692
|
+
|
|
1693
|
+
const mcpSummary = p.mcp_installed
|
|
1694
|
+
? `${p.mcp_mode || 'direct'} · ${Array.isArray(p.mcp_servers) ? p.mcp_servers.length : '...'} server(s)`
|
|
1695
|
+
: 'not installed';
|
|
1696
|
+
|
|
1697
|
+
// Action buttons
|
|
1698
|
+
const startBtn = !active && accessible ? `<button class="btn btn-xs btn-green" onclick="startSession('${jsq(p.path)}')">▶ Start</button>` : '';
|
|
1699
|
+
// When active via MCP session but no UI server running, offer to launch the UI
|
|
1700
|
+
const startUiBtn = active && p.port == null && accessible ? `<button class="btn btn-xs btn-primary" onclick="startSession('${jsq(p.path)}')">↗ Open</button>` : '';
|
|
1701
|
+
const openBtn = active && p.port != null ? `<button class="btn btn-xs btn-primary" onclick="openSession(${p.port})">↗ Open</button>` : '';
|
|
1702
|
+
const restartBtn = active && p.port != null ? `<button class="btn btn-xs btn-ghost" onclick="restartSession('${jsq(p.path)}',${p.port})" title="Restart UI server">↺ Restart</button>` : '';
|
|
1703
|
+
const stopBtn = active && p.port != null ? `<button class="btn btn-xs btn-ghost" onclick="stopSession(${p.port})">■ Stop</button>`
|
|
1704
|
+
: active && p.session_active ? `<button class="btn btn-xs btn-ghost" onclick="endMcpSession('${jsq(p.path)}')">■ Stop</button>`
|
|
1705
|
+
: '';
|
|
1706
|
+
const autostartOn = !!p.autostart_ui;
|
|
1707
|
+
const autostartBtn = accessible ? `<button class="btn btn-xs ${autostartOn ? 'btn-green' : 'btn-ghost'}" onclick="toggleAutostart('${jsq(p.path)}',${autostartOn})" title="${autostartOn ? 'Autostart on (click to disable)' : 'Autostart off (click to enable)'}">⚡ Auto</button>` : '';
|
|
1708
|
+
const folderBtn = `<button class="btn btn-xs btn-ghost" onclick="openFolder('${jsq(p.path)}')">📂 Folder</button>`;
|
|
1709
|
+
const logBtn = `<button class="btn btn-xs btn-ghost" onclick="addSessionViewer('${jsq(p.path)}','${jsq(p.name)}')">≡ Session Log</button>`;
|
|
1710
|
+
const ledgerBtn = active && p.port != null ? `<button class="btn btn-xs btn-ghost" onclick="window.open('http://localhost:${p.port}/edits','_blank')">📋 Ledger</button>` : '';
|
|
1711
|
+
const setupBtn = `<button class="btn btn-xs btn-ghost" onclick="openMcpModal('${jsq(p.path)}','${jsq(p.ide)}','${jsq(p.mcp_mode||'')}')">⚙ Setup</button>`;
|
|
1712
|
+
const editBtn = `<button class="btn btn-xs btn-ghost" onclick="openEditModal('${jsq(p.path)}','${jsq(p.name)}','${jsq((p.tags||[]).join(','))}')">✏ Edit</button>`;
|
|
1713
|
+
const ideBtn = `<button class="btn btn-xs btn-primary" onclick="openIdeModal('${jsq(p.path)}','${jsq(p.ide)}')">▶ IDE</button>`;
|
|
1714
|
+
const transferBtn = !active ? `<button class="btn btn-xs btn-ghost" onclick="openTransferModal('${jsq(p.path)}','${jsq(p.name)}')">Transfer</button>` : '';
|
|
1715
|
+
const removeBtn = !active ? `<button class="btn btn-xs btn-danger" onclick="removeProject('${jsq(p.path)}','${jsq(p.name)}')">✕ Remove</button>` : '';
|
|
1716
|
+
|
|
1717
|
+
const isLaunching = !active && launchingPaths.has(p.path);
|
|
1718
|
+
return `
|
|
1719
|
+
<div class="project-card ${active ? 'is-active' : ''} ${isLaunching ? 'is-launching' : ''}" id="${cid}-wrap">
|
|
1720
|
+
<div class="card-zone-identity">
|
|
1721
|
+
<div class="project-name-row">
|
|
1722
|
+
<span class="project-name">${esc(p.name)}</span>
|
|
1723
|
+
${statusPill}
|
|
1724
|
+
${accessBadge}
|
|
1725
|
+
${verBadge}
|
|
1726
|
+
${mcpBadge}
|
|
1727
|
+
${notifBadge}
|
|
1728
|
+
</div>
|
|
1729
|
+
<div class="project-path" title="${esc(p.path)}">${esc(pathDisplay)}</div>
|
|
1730
|
+
${hasTags || budgetBadge ? `<div class="tags-row">${tagChips}${budgetBadge}${noteIcon}</div>` : ''}
|
|
1731
|
+
</div>
|
|
1732
|
+
<div class="card-zone-meta">
|
|
1733
|
+
<div class="info-row">
|
|
1734
|
+
<span class="info-item"><span class="info-label">IDE</span> ${esc(ide)}</span>
|
|
1735
|
+
<span class="info-item"><span class="info-label">Port</span> ${portVal}</span>
|
|
1736
|
+
<span class="info-item"><span class="info-label">Last</span> ${last}</span>
|
|
1737
|
+
</div>
|
|
1738
|
+
</div>
|
|
1739
|
+
<div class="card-zone-actions">
|
|
1740
|
+
<div class="card-actions-primary">
|
|
1741
|
+
${startBtn}${startUiBtn}${openBtn}${restartBtn}${stopBtn}${autostartBtn}${folderBtn}${logBtn}${ledgerBtn}${setupBtn}${ideBtn}
|
|
1742
|
+
</div>
|
|
1743
|
+
<div class="card-actions-secondary">
|
|
1744
|
+
${clearNotifBtn}${editBtn}${transferBtn}${removeBtn}
|
|
1745
|
+
</div>
|
|
1746
|
+
</div>
|
|
1747
|
+
</div>`;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
function toggleSessionDrawer(forceOpen = null) {
|
|
1751
|
+
if (!sessionDrawerTabs.length) return;
|
|
1752
|
+
sessionDrawerOpen = forceOpen == null ? !sessionDrawerOpen : !!forceOpen;
|
|
1753
|
+
renderSessionDrawer();
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
function addSessionViewer(path, name) {
|
|
1757
|
+
const existing = sessionDrawerTabs.find(tab => tab.path === path);
|
|
1758
|
+
if (existing) {
|
|
1759
|
+
activeSessionTab = path;
|
|
1760
|
+
sessionDrawerOpen = true;
|
|
1761
|
+
renderSessionDrawer();
|
|
1762
|
+
refreshSessionViewer(path);
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
sessionDrawerTabs.push({
|
|
1767
|
+
path,
|
|
1768
|
+
name,
|
|
1769
|
+
events: [],
|
|
1770
|
+
latestTimestamp: null,
|
|
1771
|
+
active: false,
|
|
1772
|
+
port: null,
|
|
1773
|
+
loading: true,
|
|
1774
|
+
notifications: [],
|
|
1775
|
+
notifLoading: false,
|
|
1776
|
+
subTab: 'logs',
|
|
1777
|
+
});
|
|
1778
|
+
activeSessionTab = path;
|
|
1779
|
+
sessionDrawerOpen = true;
|
|
1780
|
+
renderSessionDrawer();
|
|
1781
|
+
refreshSessionViewer(path);
|
|
1782
|
+
refreshSessionNotifications(path);
|
|
1783
|
+
scheduleSessionPoll();
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
function closeSessionViewer(path) {
|
|
1787
|
+
sessionDrawerTabs = sessionDrawerTabs.filter(tab => tab.path !== path);
|
|
1788
|
+
if (activeSessionTab === path) {
|
|
1789
|
+
activeSessionTab = sessionDrawerTabs.length ? sessionDrawerTabs[sessionDrawerTabs.length - 1].path : null;
|
|
1790
|
+
}
|
|
1791
|
+
if (!sessionDrawerTabs.length) {
|
|
1792
|
+
sessionDrawerOpen = true;
|
|
1793
|
+
}
|
|
1794
|
+
renderSessionDrawer();
|
|
1795
|
+
scheduleSessionPoll();
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
function setActiveSessionViewer(path) {
|
|
1799
|
+
activeSessionTab = path;
|
|
1800
|
+
sessionDrawerOpen = true;
|
|
1801
|
+
renderSessionDrawer();
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
function getSessionTab(path) {
|
|
1805
|
+
return sessionDrawerTabs.find(tab => tab.path === path);
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
async function refreshSessionDrawerTabs() {
|
|
1809
|
+
await Promise.all(sessionDrawerTabs.map(tab => refreshSessionViewer(tab.path, true)));
|
|
1810
|
+
// Refresh notifications for the active tab
|
|
1811
|
+
if (activeSessionTab) {
|
|
1812
|
+
const tab = getSessionTab(activeSessionTab);
|
|
1813
|
+
if (tab && tab.subTab === 'notifications') refreshSessionNotifications(activeSessionTab);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
async function refreshSessionViewer(path, incremental = false) {
|
|
1818
|
+
const tab = getSessionTab(path);
|
|
1819
|
+
if (!tab) return;
|
|
1820
|
+
|
|
1821
|
+
const payload = {path, limit: incremental ? 60 : 120};
|
|
1822
|
+
if (incremental && tab.latestTimestamp) payload.since = tab.latestTimestamp;
|
|
1823
|
+
|
|
1824
|
+
try {
|
|
1825
|
+
const r = await fetch('/api/projects/activity', {
|
|
1826
|
+
method: 'POST',
|
|
1827
|
+
headers: {'Content-Type':'application/json'},
|
|
1828
|
+
body: JSON.stringify(payload),
|
|
1829
|
+
});
|
|
1830
|
+
const d = await r.json();
|
|
1831
|
+
if (d.error) throw new Error(d.error);
|
|
1832
|
+
|
|
1833
|
+
const incoming = Array.isArray(d.events) ? d.events.slice().reverse() : [];
|
|
1834
|
+
tab.name = d.project?.name || tab.name;
|
|
1835
|
+
tab.active = !!d.project?.active;
|
|
1836
|
+
tab.port = d.project?.port || null;
|
|
1837
|
+
tab.latestTimestamp = d.latest_timestamp || tab.latestTimestamp;
|
|
1838
|
+
tab.loading = false;
|
|
1839
|
+
tab.error = null;
|
|
1840
|
+
|
|
1841
|
+
if (!incremental || !tab.events.length) {
|
|
1842
|
+
tab.events = incoming;
|
|
1843
|
+
} else if (incoming.length) {
|
|
1844
|
+
const seen = new Set(tab.events.map(ev => `${ev.timestamp}|${ev.type}|${JSON.stringify(ev)}`));
|
|
1845
|
+
incoming.forEach(ev => {
|
|
1846
|
+
const key = `${ev.timestamp}|${ev.type}|${JSON.stringify(ev)}`;
|
|
1847
|
+
if (!seen.has(key)) tab.events.push(ev);
|
|
1848
|
+
});
|
|
1849
|
+
if (tab.events.length > 200) tab.events = tab.events.slice(-200);
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
renderSessionDrawer();
|
|
1853
|
+
} catch (e) {
|
|
1854
|
+
tab.loading = false;
|
|
1855
|
+
tab.error = e.message;
|
|
1856
|
+
renderSessionDrawer();
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
async function refreshSessionNotifications(path) {
|
|
1861
|
+
const tab = getSessionTab(path);
|
|
1862
|
+
if (!tab) return;
|
|
1863
|
+
tab.notifLoading = true;
|
|
1864
|
+
try {
|
|
1865
|
+
const r = await fetch('/api/projects/notifications', {
|
|
1866
|
+
method: 'POST',
|
|
1867
|
+
headers: {'Content-Type':'application/json'},
|
|
1868
|
+
body: JSON.stringify({path, limit: 50}),
|
|
1869
|
+
});
|
|
1870
|
+
const d = await r.json();
|
|
1871
|
+
tab.notifications = Array.isArray(d.notifications) ? d.notifications : [];
|
|
1872
|
+
tab.notifLoading = false;
|
|
1873
|
+
renderSessionDrawer();
|
|
1874
|
+
} catch(e) {
|
|
1875
|
+
tab.notifLoading = false;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
async function clearSessionNotifications(path) {
|
|
1880
|
+
try {
|
|
1881
|
+
await fetch('/api/projects/notifications/clear', {
|
|
1882
|
+
method: 'POST',
|
|
1883
|
+
headers: {'Content-Type':'application/json'},
|
|
1884
|
+
body: JSON.stringify({path}),
|
|
1885
|
+
});
|
|
1886
|
+
const tab = getSessionTab(path);
|
|
1887
|
+
if (tab) tab.notifications = [];
|
|
1888
|
+
renderSessionDrawer();
|
|
1889
|
+
loadProjects(); // refresh badge counts on cards
|
|
1890
|
+
toast('Notifications cleared', 'ok');
|
|
1891
|
+
} catch(e) {
|
|
1892
|
+
toast('Failed to clear notifications', 'err');
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
function setSessionSubTab(path, subTab) {
|
|
1897
|
+
const tab = getSessionTab(path);
|
|
1898
|
+
if (!tab) return;
|
|
1899
|
+
tab.subTab = subTab;
|
|
1900
|
+
if (subTab === 'notifications' && !tab.notifications.length && !tab.notifLoading) {
|
|
1901
|
+
refreshSessionNotifications(path);
|
|
1902
|
+
}
|
|
1903
|
+
renderSessionDrawer();
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
function renderSessionDrawer() {
|
|
1907
|
+
const drawer = document.getElementById('session-drawer');
|
|
1908
|
+
const tabsEl = document.getElementById('session-tabs');
|
|
1909
|
+
const bodyEl = document.getElementById('session-drawer-body');
|
|
1910
|
+
|
|
1911
|
+
if (!sessionDrawerTabs.length) {
|
|
1912
|
+
drawer.classList.add('hidden');
|
|
1913
|
+
document.body.classList.remove('with-session-drawer');
|
|
1914
|
+
tabsEl.innerHTML = '';
|
|
1915
|
+
bodyEl.innerHTML = '';
|
|
1916
|
+
clearTimeout(sessionPollTimer);
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
drawer.classList.remove('hidden');
|
|
1921
|
+
document.body.classList.add('with-session-drawer');
|
|
1922
|
+
drawer.classList.toggle('open', sessionDrawerOpen);
|
|
1923
|
+
|
|
1924
|
+
if (!activeSessionTab || !sessionDrawerTabs.some(tab => tab.path === activeSessionTab)) {
|
|
1925
|
+
activeSessionTab = sessionDrawerTabs[0].path;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
tabsEl.innerHTML = sessionDrawerTabs.map(tab => `
|
|
1929
|
+
<div class="session-tab ${tab.path === activeSessionTab ? 'active' : ''}" onclick="setActiveSessionViewer('${jsq(tab.path)}')">
|
|
1930
|
+
<span class="session-tab-status ${tab.active ? 'live' : ''}"></span>
|
|
1931
|
+
<span class="session-tab-name" title="${esc(tab.path)}">${esc(tab.name || tab.path)}</span>
|
|
1932
|
+
<button class="session-tab-close" title="Close session viewer" onclick="event.stopPropagation(); closeSessionViewer('${jsq(tab.path)}')">×</button>
|
|
1933
|
+
</div>
|
|
1934
|
+
`).join('');
|
|
1935
|
+
|
|
1936
|
+
bodyEl.innerHTML = sessionDrawerTabs.map(tab => {
|
|
1937
|
+
const meta = [];
|
|
1938
|
+
meta.push(`<span>${tab.active ? 'Active session' : 'Idle project'}</span>`);
|
|
1939
|
+
if (tab.port) meta.push(`<span>Port <a class="port-link" href="http://localhost:${tab.port}" target="_blank" rel="noopener">:${tab.port}</a></span>`);
|
|
1940
|
+
meta.push(`<span title="${esc(tab.path)}">${esc(tab.path.length > 90 ? '…' + tab.path.slice(-87) : tab.path)}</span>`);
|
|
1941
|
+
|
|
1942
|
+
const subTab = tab.subTab || 'logs';
|
|
1943
|
+
const nc = (tab.notifications || []).length;
|
|
1944
|
+
const notifBadge = nc > 0 ? `<span class="session-subtab-badge">${nc}</span>` : '';
|
|
1945
|
+
|
|
1946
|
+
// Logs content
|
|
1947
|
+
let logsContent = '<div class="session-event-empty">Loading session activity…</div>';
|
|
1948
|
+
if (tab.error) {
|
|
1949
|
+
logsContent = `<div class="session-event-empty">Failed to load activity: ${esc(tab.error)}</div>`;
|
|
1950
|
+
} else if (!tab.loading && !tab.events.length) {
|
|
1951
|
+
logsContent = '<div class="session-event-empty">No activity captured yet for this project.</div>';
|
|
1952
|
+
} else if (tab.events.length) {
|
|
1953
|
+
logsContent = tab.events.slice().reverse().map(renderSessionEvent).join('');
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
// Notifications content
|
|
1957
|
+
let notifContent = '';
|
|
1958
|
+
if (tab.notifLoading) {
|
|
1959
|
+
notifContent = '<div class="session-event-empty">Loading notifications…</div>';
|
|
1960
|
+
} else if (!nc) {
|
|
1961
|
+
notifContent = '<div class="session-event-empty">No pending notifications.</div>';
|
|
1962
|
+
} else {
|
|
1963
|
+
notifContent = tab.notifications.map(n => `
|
|
1964
|
+
<div class="session-notif-item">
|
|
1965
|
+
<div>
|
|
1966
|
+
<span class="notif-severity ${esc(n.severity || 'info')}">${esc(n.severity || 'info')}</span>
|
|
1967
|
+
<span class="notif-agent">${esc(n.agent || '')}</span>
|
|
1968
|
+
</div>
|
|
1969
|
+
<div class="notif-title">${esc(n.title || n.message || '')}</div>
|
|
1970
|
+
<div class="notif-time">${esc(formatTimestamp(n.timestamp))}</div>
|
|
1971
|
+
</div>
|
|
1972
|
+
`).join('');
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
const notifHeader = subTab === 'notifications' && nc > 0
|
|
1976
|
+
? `<div class="session-notif-header"><span>${nc} notification${nc !== 1 ? 's' : ''}</span><button class="session-notif-clear" onclick="clearSessionNotifications('${jsq(tab.path)}')">Clear All</button></div>`
|
|
1977
|
+
: '';
|
|
1978
|
+
|
|
1979
|
+
return `
|
|
1980
|
+
<div class="session-panel ${tab.path === activeSessionTab ? 'active' : ''}">
|
|
1981
|
+
<div class="session-panel-header">
|
|
1982
|
+
<div class="session-panel-title">${esc(tab.name || tab.path)}</div>
|
|
1983
|
+
<div class="session-live-pill ${tab.active ? 'live' : ''}">${tab.active ? 'LIVE' : 'IDLE'}</div>
|
|
1984
|
+
<div class="session-panel-meta">${meta.join('')}</div>
|
|
1985
|
+
</div>
|
|
1986
|
+
<div class="session-subtabs">
|
|
1987
|
+
<div class="session-subtab ${subTab === 'logs' ? 'active' : ''}" onclick="setSessionSubTab('${jsq(tab.path)}','logs')">Logs</div>
|
|
1988
|
+
<div class="session-subtab ${subTab === 'notifications' ? 'active' : ''}" onclick="setSessionSubTab('${jsq(tab.path)}','notifications')">Notifications ${notifBadge}</div>
|
|
1989
|
+
</div>
|
|
1990
|
+
${notifHeader}
|
|
1991
|
+
<div class="session-event-list" style="${subTab !== 'logs' ? 'display:none' : ''}">${logsContent}</div>
|
|
1992
|
+
<div class="session-event-list" style="${subTab !== 'notifications' ? 'display:none' : ''}">${notifContent}</div>
|
|
1993
|
+
</div>
|
|
1994
|
+
`;
|
|
1995
|
+
}).join('');
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
function renderSessionEvent(event) {
|
|
1999
|
+
const detail = Object.entries(event || {})
|
|
2000
|
+
.filter(([key]) => key !== 'timestamp' && key !== 'type')
|
|
2001
|
+
.slice(0, 8)
|
|
2002
|
+
.map(([key, value]) => `${key}: ${formatEventValue(value)}`)
|
|
2003
|
+
.join('\n');
|
|
2004
|
+
|
|
2005
|
+
return `
|
|
2006
|
+
<div class="session-event">
|
|
2007
|
+
<div class="session-event-head">
|
|
2008
|
+
<span class="session-event-type">${esc(event.type || 'event')}</span>
|
|
2009
|
+
<span class="session-event-time">${esc(formatTimestamp(event.timestamp))}</span>
|
|
2010
|
+
</div>
|
|
2011
|
+
<div class="session-event-summary">${esc(summarizeEvent(event))}</div>
|
|
2012
|
+
${detail ? `<div class="session-event-detail">${esc(detail)}</div>` : ''}
|
|
2013
|
+
</div>
|
|
2014
|
+
`;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
function summarizeEvent(event) {
|
|
2018
|
+
if (!event) return 'Unknown event';
|
|
2019
|
+
if (event.type === 'tool_call' && event.tool_name) return `Tool call: ${event.tool_name}`;
|
|
2020
|
+
if (event.type === 'decision' && event.reasoning) return event.reasoning;
|
|
2021
|
+
if (event.type === 'file_change' && event.path) return `Changed ${event.path}`;
|
|
2022
|
+
if (event.type === 'fact_stored' && event.fact) return `Stored fact: ${event.fact}`;
|
|
2023
|
+
if (event.type === 'session_start') return 'Session started';
|
|
2024
|
+
if (event.type === 'session_save') return 'Session saved';
|
|
2025
|
+
return event.summary || event.message || event.note || `Recorded ${event.type || 'event'}`;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
function formatEventValue(value) {
|
|
2029
|
+
if (value == null) return 'null';
|
|
2030
|
+
if (typeof value === 'string') return value;
|
|
2031
|
+
try { return JSON.stringify(value); } catch { return String(value); }
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
function formatTimestamp(value) {
|
|
2035
|
+
if (!value) return 'unknown time';
|
|
2036
|
+
const dt = new Date(value);
|
|
2037
|
+
if (Number.isNaN(dt.getTime())) return value;
|
|
2038
|
+
return dt.toLocaleString([], {month:'short', day:'numeric', hour:'2-digit', minute:'2-digit', second:'2-digit'});
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
function formatProjectLastSession(value) {
|
|
2042
|
+
if (!value) return 'never';
|
|
2043
|
+
const dt = new Date(value);
|
|
2044
|
+
if (Number.isNaN(dt.getTime())) return String(value).slice(0, 10);
|
|
2045
|
+
const dayText = Math.max(0, Math.floor((Date.now() - dt.getTime()) / 86400000));
|
|
2046
|
+
return `${dt.toISOString().slice(0, 10)} (${dayText} day${dayText === 1 ? '' : 's'})`;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
async function fetchProjectDetails(path, force = false) {
|
|
2050
|
+
if (!force && detailsCache[path]) return detailsCache[path];
|
|
2051
|
+
const ide = mcpModalPath === path ? (document.getElementById('mcp-modal-ide')?.value?.trim() || null) : null;
|
|
2052
|
+
const r = await fetch('/api/projects/details', {
|
|
2053
|
+
method: 'POST',
|
|
2054
|
+
headers: {'Content-Type':'application/json'},
|
|
2055
|
+
body: JSON.stringify({path, ide}),
|
|
2056
|
+
});
|
|
2057
|
+
const d = await r.json();
|
|
2058
|
+
if (d.error) throw new Error(d.error);
|
|
2059
|
+
detailsCache[path] = d;
|
|
2060
|
+
return d;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
async function updateProject(path) {
|
|
2064
|
+
const project = allProjects.find(p => p.path === path) || {path, ide: 'auto'};
|
|
2065
|
+
document.getElementById('update-modal-path').textContent = path;
|
|
2066
|
+
document.getElementById('update-modal-version').textContent = hubVersion;
|
|
2067
|
+
document.getElementById('update-modal-ide').value = project.ide || 'auto';
|
|
2068
|
+
|
|
2069
|
+
const out = document.getElementById('update-cmd-output');
|
|
2070
|
+
out.textContent = '';
|
|
2071
|
+
out.className = 'cmd-output hidden';
|
|
2072
|
+
|
|
2073
|
+
const runBtn = document.getElementById('update-run-btn');
|
|
2074
|
+
runBtn.disabled = false;
|
|
2075
|
+
runBtn.textContent = 'Confirm Update';
|
|
2076
|
+
|
|
2077
|
+
modalPath = path;
|
|
2078
|
+
openModal('update-modal');
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
async function runUpdate() {
|
|
2082
|
+
const path = modalPath;
|
|
2083
|
+
const ide = document.getElementById('update-modal-ide').value;
|
|
2084
|
+
const runBtn = document.getElementById('update-run-btn');
|
|
2085
|
+
const out = document.getElementById('update-cmd-output');
|
|
2086
|
+
|
|
2087
|
+
runBtn.disabled = true;
|
|
2088
|
+
runBtn.innerHTML = '<span class="spin">⟳</span> Updating…';
|
|
2089
|
+
out.textContent = 'Starting update...\n';
|
|
2090
|
+
out.className = 'cmd-output';
|
|
2091
|
+
|
|
2092
|
+
try {
|
|
2093
|
+
const r = await fetch('/api/projects/run-init', {
|
|
2094
|
+
method: 'POST',
|
|
2095
|
+
headers: {'Content-Type':'application/json'},
|
|
2096
|
+
body: JSON.stringify({path, init_mode: 'force', ide: ide === 'auto' ? null : ide}),
|
|
2097
|
+
});
|
|
2098
|
+
const d = await r.json();
|
|
2099
|
+
if (d.error) throw new Error(d.error);
|
|
2100
|
+
|
|
2101
|
+
out.textContent = d.output || '(no output)';
|
|
2102
|
+
out.className = 'cmd-output ' + (d.success ? 'success' : 'failure');
|
|
2103
|
+
toast(d.success ? 'Project updated' : 'Update failed', d.success ? 'ok' : 'err');
|
|
2104
|
+
if (d.success) loadProjects();
|
|
2105
|
+
} catch(e) {
|
|
2106
|
+
out.textContent += '\nError: ' + e.message;
|
|
2107
|
+
out.className = 'cmd-output failure';
|
|
2108
|
+
toast('Error: ' + e.message, 'err');
|
|
2109
|
+
} finally {
|
|
2110
|
+
runBtn.disabled = false;
|
|
2111
|
+
runBtn.textContent = 'Confirm Update';
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
function normalizeCliIde(ide, fallbackIde = 'claude-code') {
|
|
2116
|
+
const value = (ide || '').trim();
|
|
2117
|
+
return value || fallbackIde || 'claude-code';
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
function getSelectedMcpIde() {
|
|
2121
|
+
const details = mcpModalPath ? detailsCache[mcpModalPath] : null;
|
|
2122
|
+
const selected = document.getElementById('mcp-modal-ide')?.value?.trim() || '';
|
|
2123
|
+
return normalizeCliIde(selected, details?.ide || 'claude-code');
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
function getSelectedMcpMode() {
|
|
2127
|
+
const details = mcpModalPath ? detailsCache[mcpModalPath] : null;
|
|
2128
|
+
const selected = document.getElementById('mcp-modal-mode')?.value?.trim() || '';
|
|
2129
|
+
return selected || details?.mcp_mode || 'direct';
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
function resetMcpOutput() {
|
|
2133
|
+
const out = document.getElementById('mcp-output');
|
|
2134
|
+
out.textContent = '';
|
|
2135
|
+
out.className = 'cmd-output hidden';
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
function setMcpOutput(text, kind = '') {
|
|
2139
|
+
const out = document.getElementById('mcp-output');
|
|
2140
|
+
out.textContent = text || '(no output)';
|
|
2141
|
+
out.className = 'cmd-output' + (kind ? ` ${kind}` : '');
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
function renderMcpToolCategories() {
|
|
2145
|
+
const host = document.getElementById('mcp-tool-categories');
|
|
2146
|
+
if (!mcpCapabilities?.tool_categories?.length) {
|
|
2147
|
+
host.innerHTML = '<div style="font-size:12px;color:var(--text2);">Capability metadata unavailable.</div>';
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
host.innerHTML = mcpCapabilities.tool_categories.map(category => `
|
|
2151
|
+
<div class="mcp-tool-cat">
|
|
2152
|
+
<div class="mcp-tool-cat-name">${esc(category.name)}</div>
|
|
2153
|
+
<div class="mcp-tool-badges">${(category.tools || []).map(tool => `<span class="mcp-tool-badge">${esc(tool)}</span>`).join('')}</div>
|
|
2154
|
+
</div>
|
|
2155
|
+
`).join('');
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
let mcpActiveTab = 'servers';
|
|
2159
|
+
function setMcpTab(tab) {
|
|
2160
|
+
mcpActiveTab = tab;
|
|
2161
|
+
const tabKeywords = {servers:'server', setup:'setup', config:'config', reference:'ref'};
|
|
2162
|
+
document.querySelectorAll('#mcp-tabs .mcp-tab').forEach(el => {
|
|
2163
|
+
const txt = el.textContent.toLowerCase();
|
|
2164
|
+
el.classList.toggle('active', txt.includes(tabKeywords[tab] || tab));
|
|
2165
|
+
});
|
|
2166
|
+
['servers', 'setup', 'config', 'reference'].forEach(t => {
|
|
2167
|
+
const panel = document.getElementById('mcp-tab-' + t);
|
|
2168
|
+
if (panel) panel.classList.toggle('active', t === tab);
|
|
2169
|
+
});
|
|
2170
|
+
if (tab === 'reference') syncMcpCommandPreview();
|
|
2171
|
+
if (tab === 'config') { loadConfigHealth(); loadBudgetSettings(); loadPermissions(); }
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
function toggleMcpAddForm() {
|
|
2175
|
+
const form = document.getElementById('mcp-add-form');
|
|
2176
|
+
const toggle = document.getElementById('mcp-add-toggle');
|
|
2177
|
+
const showing = form.classList.contains('hidden');
|
|
2178
|
+
form.classList.toggle('hidden');
|
|
2179
|
+
toggle.style.display = showing ? 'none' : '';
|
|
2180
|
+
if (showing) {
|
|
2181
|
+
document.getElementById('mcp-custom-name').focus();
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
function copyCliCommand(el) {
|
|
2186
|
+
const text = el.textContent.trim();
|
|
2187
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
2188
|
+
el.classList.add('copied');
|
|
2189
|
+
setTimeout(() => el.classList.remove('copied'), 1500);
|
|
2190
|
+
}).catch(() => {});
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
function renderMcpStatus(details) {
|
|
2194
|
+
// Header pills — compact status indicators
|
|
2195
|
+
const pills = document.getElementById('mcp-status-pills');
|
|
2196
|
+
const statusPills = [
|
|
2197
|
+
`<span class="mcp-pill ${details.mcp_installed ? 'ok' : 'warn'}">${details.mcp_installed ? 'configured' : 'not configured'}</span>`,
|
|
2198
|
+
`<span class="mcp-pill">${esc(details.mcp_mode || 'direct')}</span>`,
|
|
2199
|
+
];
|
|
2200
|
+
const serverCount = (details.mcp_servers || []).length;
|
|
2201
|
+
if (serverCount > 0) statusPills.push(`<span class="mcp-pill">${serverCount} server${serverCount !== 1 ? 's' : ''}</span>`);
|
|
2202
|
+
pills.innerHTML = statusPills.join('');
|
|
2203
|
+
|
|
2204
|
+
// Compact status row in Servers tab
|
|
2205
|
+
const row = document.getElementById('mcp-status-row');
|
|
2206
|
+
const activeIdeLabel = IDE_LABELS[details.ide] || details.ide || 'unknown';
|
|
2207
|
+
row.innerHTML = `
|
|
2208
|
+
<div class="mcp-status-item"><span class="mcp-status-label">IDE</span> <span class="mcp-status-value">${esc(activeIdeLabel)}</span></div>
|
|
2209
|
+
<div class="mcp-status-item"><span class="mcp-status-label">Mode</span> <span class="mcp-status-value">${esc(details.mcp_mode || 'direct')}</span></div>
|
|
2210
|
+
<div class="mcp-status-item"><span class="mcp-status-label">Servers</span> <span class="mcp-status-value">${serverCount}</span></div>
|
|
2211
|
+
<div class="mcp-status-item"><span class="mcp-status-label">C3</span> <span class="mcp-status-value">v${esc(details.hub_c3_version || hubVersion || '?')}</span></div>
|
|
2212
|
+
`;
|
|
2213
|
+
|
|
2214
|
+
// Config path display
|
|
2215
|
+
const cfgEl = document.getElementById('mcp-config-path-display');
|
|
2216
|
+
if (details.mcp_config_path) {
|
|
2217
|
+
cfgEl.textContent = details.mcp_config_path;
|
|
2218
|
+
cfgEl.style.display = '';
|
|
2219
|
+
} else {
|
|
2220
|
+
cfgEl.style.display = 'none';
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
function renderMcpServers(details) {
|
|
2225
|
+
const host = document.getElementById('mcp-server-list');
|
|
2226
|
+
if (!details.mcp_installed) {
|
|
2227
|
+
host.innerHTML = `<div class="mcp-server-empty">
|
|
2228
|
+
<div class="mcp-server-empty-icon">🔌</div>
|
|
2229
|
+
MCP is not installed for this project yet.
|
|
2230
|
+
<div class="mcp-server-empty-cta"><button class="btn btn-primary btn-sm" onclick="setMcpTab('setup')">Set up C3 MCP</button></div>
|
|
2231
|
+
</div>`;
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
if (!details.mcp_servers || !details.mcp_servers.length) {
|
|
2235
|
+
host.innerHTML = `<div class="mcp-server-empty">
|
|
2236
|
+
<div class="mcp-server-empty-icon">☍</div>
|
|
2237
|
+
No servers defined in the active config.
|
|
2238
|
+
<div class="mcp-server-empty-cta"><button class="btn btn-ghost btn-sm" onclick="toggleMcpAddForm()">+ Add a server</button></div>
|
|
2239
|
+
</div>`;
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
// Sort: c3 first, then alphabetical
|
|
2243
|
+
const sorted = [...details.mcp_servers].sort((a, b) => {
|
|
2244
|
+
if (a.name === 'c3') return -1;
|
|
2245
|
+
if (b.name === 'c3') return 1;
|
|
2246
|
+
return a.name.localeCompare(b.name);
|
|
2247
|
+
});
|
|
2248
|
+
host.innerHTML = sorted.map(server => {
|
|
2249
|
+
const isC3 = server.name === 'c3';
|
|
2250
|
+
const args = (server.args || []).join(' ');
|
|
2251
|
+
const envKeys = (server.env_keys || []).join(', ');
|
|
2252
|
+
return `
|
|
2253
|
+
<div class="mcp-server-card ${isC3 ? 'is-c3' : ''}">
|
|
2254
|
+
<div class="mcp-server-card-head">
|
|
2255
|
+
<div class="mcp-server-name">${esc(server.name)}${isC3 ? ' <span class="c3-tag">C3</span>' : ''}</div>
|
|
2256
|
+
<div style="display:flex; gap:4px;">
|
|
2257
|
+
<button class="btn btn-xs btn-ghost" onclick="prefillMcpServer('${jsq(server.name)}','${jsq(server.command || '')}','${jsq(JSON.stringify(server.args || []))}','${jsq(JSON.stringify(server.env_keys || []))}')" title="Edit this server">Edit</button>
|
|
2258
|
+
<button class="btn btn-xs btn-danger" onclick="runMcpRemove('${jsq(server.name)}')" title="Remove this server">Remove</button>
|
|
2259
|
+
</div>
|
|
2260
|
+
</div>
|
|
2261
|
+
<dl class="mcp-server-meta">
|
|
2262
|
+
<dt>Command</dt><dd>${esc(server.command || '—')}</dd>
|
|
2263
|
+
<dt>Args</dt><dd title="${esc(args)}">${esc(args || '(none)')}</dd>
|
|
2264
|
+
${envKeys ? `<dt>Env</dt><dd>${esc(envKeys)}</dd>` : ''}
|
|
2265
|
+
</dl>
|
|
2266
|
+
</div>
|
|
2267
|
+
`;
|
|
2268
|
+
}).join('');
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
function buildMcpCliPreview(path, ide, mode) {
|
|
2272
|
+
const qPath = `"${path}"`;
|
|
2273
|
+
const qIde = ide ? ` --ide ${ide}` : '';
|
|
2274
|
+
const qMode = mode ? ` --mcp-mode ${mode}` : '';
|
|
2275
|
+
return [
|
|
2276
|
+
{ title: 'Install / Update C3', description: 'Set up or refresh the C3 MCP server for this project.', command: `c3 install-mcp ${qPath}${qIde}${qMode}`.trim() },
|
|
2277
|
+
{ title: 'Remove C3', description: 'Removes only the C3 MCP entry from IDE config.', command: `c3 mcp-remove c3 ${qPath}${qIde}`.trim() },
|
|
2278
|
+
];
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
function syncMcpCommandPreview() {
|
|
2282
|
+
const host = document.getElementById('mcp-cli-preview-list');
|
|
2283
|
+
const enabledRow = document.getElementById('mcp-custom-enabled-row');
|
|
2284
|
+
if (enabledRow) enabledRow.classList.toggle('hidden-row', getSelectedMcpIde() !== 'codex');
|
|
2285
|
+
if (!mcpModalPath) {
|
|
2286
|
+
host.innerHTML = '<div style="font-size:12px;color:var(--text2);">Open a project to see CLI previews.</div>';
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
const previews = buildMcpCliPreview(mcpModalPath, getSelectedMcpIde(), getSelectedMcpMode());
|
|
2290
|
+
host.innerHTML = previews.map(item => `
|
|
2291
|
+
<div class="mcp-cli-card">
|
|
2292
|
+
<h5>${esc(item.title)}</h5>
|
|
2293
|
+
<p>${esc(item.description)}</p>
|
|
2294
|
+
<div class="mcp-cli-preview" onclick="copyCliCommand(this)">${esc(item.command)}</div>
|
|
2295
|
+
</div>
|
|
2296
|
+
`).join('');
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
function handleMcpSelectionChange() {
|
|
2300
|
+
syncMcpCommandPreview();
|
|
2301
|
+
if (mcpModalPath) refreshMcpModal(true);
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
function prefillMcpServer(name, command, argsJson, envKeysJson) {
|
|
2305
|
+
document.getElementById('mcp-custom-name').value = name || '';
|
|
2306
|
+
document.getElementById('mcp-custom-command').value = command || '';
|
|
2307
|
+
try {
|
|
2308
|
+
const args = JSON.parse(argsJson || '[]');
|
|
2309
|
+
document.getElementById('mcp-custom-args').value = Array.isArray(args) ? args.join('\n') : '';
|
|
2310
|
+
} catch(e) {
|
|
2311
|
+
document.getElementById('mcp-custom-args').value = '';
|
|
2312
|
+
}
|
|
2313
|
+
try {
|
|
2314
|
+
const envKeys = JSON.parse(envKeysJson || '[]');
|
|
2315
|
+
const env = {};
|
|
2316
|
+
(Array.isArray(envKeys) ? envKeys : []).forEach(key => { env[key] = ''; });
|
|
2317
|
+
document.getElementById('mcp-custom-env').value = Object.keys(env).length ? JSON.stringify(env, null, 2) : '';
|
|
2318
|
+
} catch(e) {
|
|
2319
|
+
document.getElementById('mcp-custom-env').value = '';
|
|
2320
|
+
}
|
|
2321
|
+
// Ensure add form is visible
|
|
2322
|
+
const form = document.getElementById('mcp-add-form');
|
|
2323
|
+
if (form.classList.contains('hidden')) {
|
|
2324
|
+
form.classList.remove('hidden');
|
|
2325
|
+
document.getElementById('mcp-add-toggle').style.display = 'none';
|
|
2326
|
+
}
|
|
2327
|
+
// Scroll form into view
|
|
2328
|
+
form.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
async function openMcpModal(path, ide, mcpMode, initialTab) {
|
|
2332
|
+
mcpModalPath = path;
|
|
2333
|
+
const pathEl = document.getElementById('mcp-modal-path');
|
|
2334
|
+
pathEl.textContent = path;
|
|
2335
|
+
pathEl.title = path;
|
|
2336
|
+
document.getElementById('mcp-modal-ide').value = ide || '';
|
|
2337
|
+
document.getElementById('mcp-modal-mode').value = mcpMode || '';
|
|
2338
|
+
document.getElementById('mcp-custom-name').value = '';
|
|
2339
|
+
document.getElementById('mcp-custom-command').value = '';
|
|
2340
|
+
document.getElementById('mcp-custom-args').value = '';
|
|
2341
|
+
document.getElementById('mcp-custom-env').value = '';
|
|
2342
|
+
document.getElementById('mcp-custom-enabled').checked = true;
|
|
2343
|
+
document.getElementById('mcp-custom-enabled-row').classList.toggle('hidden-row', getSelectedMcpIde() !== 'codex');
|
|
2344
|
+
// Reset init controls
|
|
2345
|
+
document.getElementById('setup-init-mode').value = 'force';
|
|
2346
|
+
document.getElementById('setup-git').checked = false;
|
|
2347
|
+
syncSetupInitOptions();
|
|
2348
|
+
// Reset add form to collapsed
|
|
2349
|
+
document.getElementById('mcp-add-form').classList.add('hidden');
|
|
2350
|
+
document.getElementById('mcp-add-toggle').style.display = '';
|
|
2351
|
+
// Set initial tab
|
|
2352
|
+
setMcpTab(initialTab || 'servers');
|
|
2353
|
+
resetMcpOutput();
|
|
2354
|
+
renderMcpToolCategories();
|
|
2355
|
+
syncMcpCommandPreview();
|
|
2356
|
+
openModal('mcp-modal');
|
|
2357
|
+
await refreshMcpModal(true);
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
async function refreshMcpModal(force = false) {
|
|
2361
|
+
if (!mcpModalPath) return;
|
|
2362
|
+
try {
|
|
2363
|
+
const details = await fetchProjectDetails(mcpModalPath, force);
|
|
2364
|
+
renderMcpStatus(details);
|
|
2365
|
+
renderMcpServers(details);
|
|
2366
|
+
updateMcpInstallSection(details);
|
|
2367
|
+
document.getElementById('mcp-custom-enabled-row').classList.toggle('hidden-row', getSelectedMcpIde() !== 'codex');
|
|
2368
|
+
if (mcpActiveTab === 'reference') syncMcpCommandPreview();
|
|
2369
|
+
} catch (e) {
|
|
2370
|
+
setMcpOutput('Error: ' + e.message, 'failure');
|
|
2371
|
+
// Switch to setup tab to show the error
|
|
2372
|
+
setMcpTab('setup');
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
function updateMcpInstallSection(details) {
|
|
2377
|
+
const c3Server = (details.mcp_servers || []).find(s => s.name === 'c3');
|
|
2378
|
+
const isInstalled = !!c3Server;
|
|
2379
|
+
|
|
2380
|
+
// Status badge
|
|
2381
|
+
const badge = document.getElementById('mcp-install-state-badge');
|
|
2382
|
+
const statusRow = document.getElementById('mcp-install-status');
|
|
2383
|
+
const cfgPath = document.getElementById('mcp-install-cfg-path');
|
|
2384
|
+
badge.textContent = isInstalled ? 'installed' : 'not installed';
|
|
2385
|
+
badge.className = 'mcp-pill ' + (isInstalled ? 'ok' : 'warn');
|
|
2386
|
+
cfgPath.textContent = details.mcp_config_path || '';
|
|
2387
|
+
cfgPath.title = details.mcp_config_path || '';
|
|
2388
|
+
statusRow.style.display = 'flex';
|
|
2389
|
+
|
|
2390
|
+
// Install button label
|
|
2391
|
+
document.getElementById('mcp-install-btn').textContent = isInstalled ? 'Update C3' : 'Install C3';
|
|
2392
|
+
|
|
2393
|
+
// Remove button only shown when installed
|
|
2394
|
+
document.getElementById('mcp-remove-btn').style.display = isInstalled ? '' : 'none';
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
async function removeMcpServer(path, name, ide) {
|
|
2398
|
+
const ok = await confirmDialog({ title: 'Remove MCP Server', message: `Remove "${name}" from ${ide || 'target IDE'} config?`, confirmText: 'Remove', danger: true });
|
|
2399
|
+
if (!ok) return;
|
|
2400
|
+
toast('Removing MCP server…', 'info');
|
|
2401
|
+
try {
|
|
2402
|
+
const r = await fetch('/api/projects/run-mcp-remove', {
|
|
2403
|
+
method: 'POST',
|
|
2404
|
+
headers: {'Content-Type':'application/json'},
|
|
2405
|
+
body: JSON.stringify({path, name, ide}),
|
|
2406
|
+
});
|
|
2407
|
+
const d = await r.json();
|
|
2408
|
+
if (d.error) throw new Error(d.error);
|
|
2409
|
+
toast(d.success ? 'MCP server removed' : 'Removal failed', d.success ? 'ok' : 'err');
|
|
2410
|
+
if (d.success) {
|
|
2411
|
+
delete detailsCache[path];
|
|
2412
|
+
if (mcpModalPath === path) await refreshMcpModal(true);
|
|
2413
|
+
await loadProjects();
|
|
2414
|
+
}
|
|
2415
|
+
} catch(e) {
|
|
2416
|
+
toast('Error: ' + e.message, 'err');
|
|
2417
|
+
throw e;
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
async function runMcpInstall() {
|
|
2422
|
+
if (!mcpModalPath) return;
|
|
2423
|
+
const btn = document.getElementById('mcp-install-btn');
|
|
2424
|
+
const payload = {
|
|
2425
|
+
path: mcpModalPath,
|
|
2426
|
+
ide: document.getElementById('mcp-modal-ide').value.trim() || null,
|
|
2427
|
+
mcp_mode: document.getElementById('mcp-modal-mode').value.trim() || null,
|
|
2428
|
+
};
|
|
2429
|
+
btn.disabled = true;
|
|
2430
|
+
setMcpOutput('Running install-mcp…');
|
|
2431
|
+
try {
|
|
2432
|
+
const r = await fetch('/api/projects/run-mcp', {
|
|
2433
|
+
method: 'POST',
|
|
2434
|
+
headers: {'Content-Type':'application/json'},
|
|
2435
|
+
body: JSON.stringify(payload),
|
|
2436
|
+
});
|
|
2437
|
+
const d = await r.json();
|
|
2438
|
+
if (d.error) throw new Error(d.error);
|
|
2439
|
+
setMcpOutput(d.output || '(no output)', d.success ? 'success' : 'failure');
|
|
2440
|
+
toast(d.success ? 'MCP updated' : 'MCP update failed', d.success ? 'ok' : 'err');
|
|
2441
|
+
if (d.success) {
|
|
2442
|
+
delete detailsCache[mcpModalPath];
|
|
2443
|
+
await loadProjects();
|
|
2444
|
+
await refreshMcpModal(true);
|
|
2445
|
+
}
|
|
2446
|
+
} catch (e) {
|
|
2447
|
+
setMcpOutput('Error: ' + e.message, 'failure');
|
|
2448
|
+
toast('Error: ' + e.message, 'err');
|
|
2449
|
+
} finally {
|
|
2450
|
+
btn.disabled = false;
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
async function runMcpRemove(name) {
|
|
2455
|
+
if (!mcpModalPath) return;
|
|
2456
|
+
try {
|
|
2457
|
+
await removeMcpServer(mcpModalPath, name, getSelectedMcpIde());
|
|
2458
|
+
setMcpOutput(`Removed MCP server "${name}".`, 'success');
|
|
2459
|
+
} catch (e) {}
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
function parseArgsInput(raw) {
|
|
2463
|
+
const text = (raw || '').trim();
|
|
2464
|
+
if (!text) return [];
|
|
2465
|
+
if (text.startsWith('[')) {
|
|
2466
|
+
const parsed = JSON.parse(text);
|
|
2467
|
+
if (!Array.isArray(parsed)) throw new Error('Args JSON must be an array.');
|
|
2468
|
+
return parsed.map(item => String(item));
|
|
2469
|
+
}
|
|
2470
|
+
return text.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
function parseEnvInput(raw) {
|
|
2474
|
+
const text = (raw || '').trim();
|
|
2475
|
+
if (!text) return {};
|
|
2476
|
+
const parsed = JSON.parse(text);
|
|
2477
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
2478
|
+
throw new Error('Env must be a JSON object.');
|
|
2479
|
+
}
|
|
2480
|
+
return parsed;
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
async function saveCustomMcpServer() {
|
|
2484
|
+
if (!mcpModalPath) return;
|
|
2485
|
+
const btn = document.getElementById('mcp-custom-save-btn');
|
|
2486
|
+
try {
|
|
2487
|
+
const payload = {
|
|
2488
|
+
path: mcpModalPath,
|
|
2489
|
+
ide: document.getElementById('mcp-modal-ide').value.trim() || null,
|
|
2490
|
+
name: document.getElementById('mcp-custom-name').value.trim(),
|
|
2491
|
+
command: document.getElementById('mcp-custom-command').value.trim(),
|
|
2492
|
+
args: parseArgsInput(document.getElementById('mcp-custom-args').value),
|
|
2493
|
+
env: parseEnvInput(document.getElementById('mcp-custom-env').value),
|
|
2494
|
+
enabled: !!document.getElementById('mcp-custom-enabled').checked,
|
|
2495
|
+
};
|
|
2496
|
+
if (!payload.name || !payload.command) throw new Error('Server name and command are required.');
|
|
2497
|
+
btn.disabled = true;
|
|
2498
|
+
setMcpOutput(`Saving MCP server "${payload.name}"…`);
|
|
2499
|
+
const r = await fetch('/api/projects/mcp-server-add', {
|
|
2500
|
+
method: 'POST',
|
|
2501
|
+
headers: {'Content-Type':'application/json'},
|
|
2502
|
+
body: JSON.stringify(payload),
|
|
2503
|
+
});
|
|
2504
|
+
const d = await r.json();
|
|
2505
|
+
if (d.error) throw new Error(d.error);
|
|
2506
|
+
setMcpOutput(`Saved MCP server "${payload.name}" to ${d.config_path || 'config'}.`, 'success');
|
|
2507
|
+
toast('MCP server saved', 'ok');
|
|
2508
|
+
delete detailsCache[mcpModalPath];
|
|
2509
|
+
// Collapse add form and clear fields
|
|
2510
|
+
document.getElementById('mcp-add-form').classList.add('hidden');
|
|
2511
|
+
document.getElementById('mcp-add-toggle').style.display = '';
|
|
2512
|
+
document.getElementById('mcp-custom-name').value = '';
|
|
2513
|
+
document.getElementById('mcp-custom-command').value = '';
|
|
2514
|
+
document.getElementById('mcp-custom-args').value = '';
|
|
2515
|
+
document.getElementById('mcp-custom-env').value = '';
|
|
2516
|
+
await loadProjects();
|
|
2517
|
+
await refreshMcpModal(true);
|
|
2518
|
+
} catch (e) {
|
|
2519
|
+
setMcpOutput('Error: ' + e.message, 'failure');
|
|
2520
|
+
toast('Error: ' + e.message, 'err');
|
|
2521
|
+
} finally {
|
|
2522
|
+
btn.disabled = false;
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
function renderMcpDetails(body, d) {
|
|
2527
|
+
if (!d.mcp_installed) {
|
|
2528
|
+
body.innerHTML = '<div class="no-mcp-msg">MCP not installed for this project.</div>';
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2531
|
+
const cfgPath = d.mcp_config_path ? `<div class="expand-cfg-path">${esc(d.mcp_config_path)}</div>` : '';
|
|
2532
|
+
if (!d.mcp_servers || d.mcp_servers.length === 0) {
|
|
2533
|
+
body.innerHTML = cfgPath + '<div class="no-mcp-msg">No servers defined in config.</div>';
|
|
2534
|
+
return;
|
|
2535
|
+
}
|
|
2536
|
+
const rows = d.mcp_servers.map(s => {
|
|
2537
|
+
const args = (s.args || []).join(' ');
|
|
2538
|
+
const envKeys = (s.env_keys || []).join(', ');
|
|
2539
|
+
const isManaged = s.name === 'c3';
|
|
2540
|
+
return `<tr>
|
|
2541
|
+
<td>${esc(s.name)}</td>
|
|
2542
|
+
<td>${esc(s.command)}</td>
|
|
2543
|
+
<td>${esc(args)}</td>
|
|
2544
|
+
<td>${esc(envKeys)}</td>
|
|
2545
|
+
<td style="text-align: right; white-space: nowrap;">
|
|
2546
|
+
<button class="btn btn-xs btn-ghost" onclick="openMcpModal('${jsq(d.path)}','${jsq(d.ide)}','${jsq(d.mcp_mode||'direct')}','setup')">Update</button>
|
|
2547
|
+
<button class="btn btn-xs btn-danger" onclick="removeMcpServer('${jsq(d.path)}', '${jsq(s.name)}', '${jsq(d.ide)}')">Remove</button>
|
|
2548
|
+
</td>
|
|
2549
|
+
</tr>`;
|
|
2550
|
+
}).join('');
|
|
2551
|
+
body.innerHTML = cfgPath + `<table class="mcp-table">
|
|
2552
|
+
<thead><tr><th>Name</th><th>Command</th><th>Args</th><th>Env Keys</th><th style="text-align:right">Actions</th></tr></thead>
|
|
2553
|
+
<tbody>${rows}</tbody>
|
|
2554
|
+
</table>`;
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
// ── Setup Init (inside MCP modal's C3 Setup tab) ───────────────────────────
|
|
2558
|
+
function syncSetupInitOptions() {
|
|
2559
|
+
const initMode = document.getElementById('setup-init-mode').value || 'force';
|
|
2560
|
+
document.getElementById('setup-git-row').style.display = initMode === 'force' ? '' : 'none';
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
async function runSetupInit() {
|
|
2564
|
+
if (!mcpModalPath) return;
|
|
2565
|
+
const btn = document.getElementById('setup-init-btn');
|
|
2566
|
+
const ide = document.getElementById('mcp-modal-ide').value.trim() || null;
|
|
2567
|
+
const initMode = document.getElementById('setup-init-mode').value.trim() || 'force';
|
|
2568
|
+
const mcpMode = document.getElementById('mcp-modal-mode').value.trim() || null;
|
|
2569
|
+
const git = !!document.getElementById('setup-git').checked;
|
|
2570
|
+
|
|
2571
|
+
btn.disabled = true;
|
|
2572
|
+
btn.innerHTML = '<span class="spin">⟳</span> Running…';
|
|
2573
|
+
setMcpOutput('Running init…');
|
|
2574
|
+
|
|
2575
|
+
try {
|
|
2576
|
+
const r = await fetch('/api/projects/run-init', {
|
|
2577
|
+
method: 'POST',
|
|
2578
|
+
headers: {'Content-Type':'application/json'},
|
|
2579
|
+
body: JSON.stringify({path: mcpModalPath, ide, mcp_mode: mcpMode, init_mode: initMode, git}),
|
|
2580
|
+
});
|
|
2581
|
+
const d = await r.json();
|
|
2582
|
+
if (d.error) throw new Error(d.error);
|
|
2583
|
+
|
|
2584
|
+
setMcpOutput(d.output || '(no output)', d.success ? 'success' : 'failure');
|
|
2585
|
+
toast(d.success ? 'Project initialized' : 'Init finished with errors', d.success ? 'ok' : 'err');
|
|
2586
|
+
if (d.success) {
|
|
2587
|
+
delete detailsCache[mcpModalPath];
|
|
2588
|
+
await loadProjects();
|
|
2589
|
+
await refreshMcpModal(true);
|
|
2590
|
+
}
|
|
2591
|
+
} catch(e) {
|
|
2592
|
+
setMcpOutput('Error: ' + e.message, 'failure');
|
|
2593
|
+
toast('Error: ' + e.message, 'err');
|
|
2594
|
+
} finally {
|
|
2595
|
+
btn.disabled = false;
|
|
2596
|
+
btn.textContent = 'Run Init';
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
// ── Config Tab: Health + Component Updates ─────────────────────────────────
|
|
2601
|
+
async function loadConfigHealth() {
|
|
2602
|
+
if (!mcpModalPath) return;
|
|
2603
|
+
const host = document.getElementById('config-health-status');
|
|
2604
|
+
host.innerHTML = '<div class="config-health-loading">Checking…</div>';
|
|
2605
|
+
try {
|
|
2606
|
+
const r = await fetch('/api/projects/health', {
|
|
2607
|
+
method: 'POST',
|
|
2608
|
+
headers: {'Content-Type':'application/json'},
|
|
2609
|
+
body: JSON.stringify({path: mcpModalPath}),
|
|
2610
|
+
});
|
|
2611
|
+
const h = await r.json();
|
|
2612
|
+
if (h.error) { host.innerHTML = `<div class="config-health-loading" style="color:var(--red)">${esc(h.error)}</div>`; return; }
|
|
2613
|
+
|
|
2614
|
+
let html = '';
|
|
2615
|
+
html += `<div class="config-health-item"><span class="config-health-label">Version</span><span class="config-health-value">${esc(h.config_version || '?')}</span></div>`;
|
|
2616
|
+
html += `<div class="config-health-item"><span class="config-health-label">Index</span><span class="config-health-value">${h.index_files ?? 0} files, ${h.index_chunks ?? 0} chunks</span></div>`;
|
|
2617
|
+
const embedCount = h.embedded_files ?? 0;
|
|
2618
|
+
html += `<div class="config-health-item"><span class="config-health-label">Embeddings</span><span class="config-health-value">${embedCount > 0 ? embedCount + ' files (semantic ready)' : 'not built'}</span></div>`;
|
|
2619
|
+
const docChunks = h.doc_chunks ?? 0;
|
|
2620
|
+
html += `<div class="config-health-item"><span class="config-health-label">Doc Index (RAG)</span><span class="config-health-value">${docChunks > 0 ? docChunks + ' chunks' : 'not built'}</span></div>`;
|
|
2621
|
+
|
|
2622
|
+
const staleCls = (h.stale_files || 0) > 5 ? 'warn' : '';
|
|
2623
|
+
html += `<div class="config-health-item"><span class="config-health-label">Stale Files</span><span class="config-health-value ${staleCls}">${h.stale_files ?? 0}</span></div>`;
|
|
2624
|
+
html += `<div class="config-health-item"><span class="config-health-label">Instructions</span><span class="config-health-value">${esc(h.instructions_file || '?')}</span></div>`;
|
|
2625
|
+
html += `<div class="config-health-item"><span class="config-health-label">Sessions</span><span class="config-health-value">${h.sessions ?? 0}</span></div>`;
|
|
2626
|
+
html += `<div class="config-health-item"><span class="config-health-label">Facts</span><span class="config-health-value">${h.facts ?? 0}</span></div>`;
|
|
2627
|
+
|
|
2628
|
+
const healthCls = h.healthy ? 'ok' : 'err';
|
|
2629
|
+
html += `<div class="config-health-item"><span class="config-health-label">Status</span><span class="config-health-value ${healthCls}">${h.healthy ? 'Healthy' : h.issues?.length + ' issue(s)'}</span></div>`;
|
|
2630
|
+
|
|
2631
|
+
if (h.path_changed) {
|
|
2632
|
+
html += `<div class="config-health-item"><span class="config-health-label">Path Change</span><span class="config-health-value warn">Moved from ${esc(h.old_path || '?')}</span></div>`;
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
if (h.issues?.length) {
|
|
2636
|
+
html += `<div class="config-health-issues"><strong>Issues:</strong><ul>${h.issues.map(i => `<li>${esc(i)}</li>`).join('')}</ul></div>`;
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
host.innerHTML = html;
|
|
2640
|
+
} catch(e) {
|
|
2641
|
+
host.innerHTML = `<div class="config-health-loading" style="color:var(--red)">Failed to check health: ${esc(e.message)}</div>`;
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
async function loadBudgetSettings() {
|
|
2646
|
+
if (!mcpModalPath) return;
|
|
2647
|
+
try {
|
|
2648
|
+
const r = await fetch('/api/projects/budget', {
|
|
2649
|
+
method: 'POST',
|
|
2650
|
+
headers: {'Content-Type':'application/json'},
|
|
2651
|
+
body: JSON.stringify({path: mcpModalPath}),
|
|
2652
|
+
});
|
|
2653
|
+
const cfg = await r.json();
|
|
2654
|
+
document.getElementById('budget-threshold').value = cfg.threshold || 35000;
|
|
2655
|
+
document.getElementById('budget-nudges-toggle').checked = cfg.show_context_nudges !== false;
|
|
2656
|
+
document.getElementById('budget-alerts-toggle').checked = cfg.prepend_notifications !== false;
|
|
2657
|
+
} catch(e) {
|
|
2658
|
+
document.getElementById('budget-save-msg').textContent = 'Failed to load: ' + e.message;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
async function saveBudgetSettings() {
|
|
2663
|
+
if (!mcpModalPath) return;
|
|
2664
|
+
const msgEl = document.getElementById('budget-save-msg');
|
|
2665
|
+
msgEl.textContent = 'Saving…';
|
|
2666
|
+
try {
|
|
2667
|
+
const payload = {
|
|
2668
|
+
path: mcpModalPath,
|
|
2669
|
+
threshold: parseInt(document.getElementById('budget-threshold').value) || 35000,
|
|
2670
|
+
show_context_nudges: document.getElementById('budget-nudges-toggle').checked,
|
|
2671
|
+
prepend_notifications: document.getElementById('budget-alerts-toggle').checked,
|
|
2672
|
+
};
|
|
2673
|
+
const r = await fetch('/api/projects/budget', {
|
|
2674
|
+
method: 'PUT',
|
|
2675
|
+
headers: {'Content-Type':'application/json'},
|
|
2676
|
+
body: JSON.stringify(payload),
|
|
2677
|
+
});
|
|
2678
|
+
const d = await r.json();
|
|
2679
|
+
if (d.error) { msgEl.textContent = 'Error: ' + d.error; return; }
|
|
2680
|
+
msgEl.textContent = '✓ Saved';
|
|
2681
|
+
msgEl.style.color = 'var(--green)';
|
|
2682
|
+
setTimeout(() => { msgEl.textContent = ''; msgEl.style.color = ''; }, 3000);
|
|
2683
|
+
} catch(e) {
|
|
2684
|
+
msgEl.textContent = 'Error: ' + e.message;
|
|
2685
|
+
msgEl.style.color = 'var(--red)';
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
async function loadPermissions() {
|
|
2690
|
+
if (!mcpModalPath) return;
|
|
2691
|
+
try {
|
|
2692
|
+
const r = await fetch('/api/projects/permissions', {
|
|
2693
|
+
method: 'POST',
|
|
2694
|
+
headers: {'Content-Type':'application/json'},
|
|
2695
|
+
body: JSON.stringify({path: mcpModalPath}),
|
|
2696
|
+
});
|
|
2697
|
+
const cfg = await r.json();
|
|
2698
|
+
const section = document.getElementById('perm-section');
|
|
2699
|
+
if (cfg.supported === false) { if (section) section.style.display = 'none'; return; }
|
|
2700
|
+
if (section) section.style.display = '';
|
|
2701
|
+
const badge = document.getElementById('perm-current-badge');
|
|
2702
|
+
const counts = document.getElementById('perm-rule-counts');
|
|
2703
|
+
const list = document.getElementById('perm-tier-list');
|
|
2704
|
+
const current = cfg.current_tier || 'not set';
|
|
2705
|
+
badge.textContent = current;
|
|
2706
|
+
badge.style.background = current === 'read-only' ? 'var(--blue)' : current === 'standard' ? 'var(--green)' : current === 'permissive' ? 'var(--yellow)' : 'var(--muted)';
|
|
2707
|
+
badge.style.color = current === 'permissive' ? '#000' : '#fff';
|
|
2708
|
+
counts.textContent = cfg.allow_count ? `${cfg.allow_count} allow, ${cfg.deny_count} deny rules` : '';
|
|
2709
|
+
list.innerHTML = '';
|
|
2710
|
+
if (cfg.tiers) {
|
|
2711
|
+
for (const [name, info] of Object.entries(cfg.tiers)) {
|
|
2712
|
+
const isActive = current === name;
|
|
2713
|
+
const color = name === 'read-only' ? 'var(--blue)' : name === 'standard' ? 'var(--green)' : 'var(--yellow)';
|
|
2714
|
+
const row = document.createElement('div');
|
|
2715
|
+
row.style.cssText = `display:flex;align-items:center;gap:10px;padding:7px 10px;border-radius:6px;border:1px solid ${isActive ? color : 'var(--border)'};background:${isActive ? color + '18' : 'transparent'};cursor:pointer;transition:all 0.15s;`;
|
|
2716
|
+
row.innerHTML = `
|
|
2717
|
+
<div style="width:13px;height:13px;border-radius:50%;border:2px solid ${isActive ? color : 'var(--muted)'};background:${isActive ? color : 'transparent'};display:flex;align-items:center;justify-content:center;flex-shrink:0;">
|
|
2718
|
+
${isActive ? '<div style="width:5px;height:5px;border-radius:50%;background:var(--bg)"></div>' : ''}
|
|
2719
|
+
</div>
|
|
2720
|
+
<div style="flex:1;">
|
|
2721
|
+
<div style="font-size:12px;font-weight:${isActive ? 600 : 400};color:${isActive ? color : 'var(--text)'}">${name}</div>
|
|
2722
|
+
<div style="font-size:10px;color:var(--muted)">${info.description}</div>
|
|
2723
|
+
</div>
|
|
2724
|
+
<div style="font-size:10px;color:var(--muted);font-family:monospace;white-space:nowrap;">${info.allow_count} allow</div>
|
|
2725
|
+
`;
|
|
2726
|
+
if (!isActive) {
|
|
2727
|
+
row.onclick = () => applyPermTier(name);
|
|
2728
|
+
}
|
|
2729
|
+
list.appendChild(row);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
} catch(e) {
|
|
2733
|
+
document.getElementById('perm-save-msg').textContent = 'Failed to load: ' + e.message;
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
async function applyPermTier(tier) {
|
|
2738
|
+
if (!mcpModalPath) return;
|
|
2739
|
+
const msgEl = document.getElementById('perm-save-msg');
|
|
2740
|
+
msgEl.textContent = 'Applying…';
|
|
2741
|
+
msgEl.style.color = 'var(--muted)';
|
|
2742
|
+
try {
|
|
2743
|
+
const r = await fetch('/api/projects/permissions/apply', {
|
|
2744
|
+
method: 'POST',
|
|
2745
|
+
headers: {'Content-Type':'application/json'},
|
|
2746
|
+
body: JSON.stringify({path: mcpModalPath, tier}),
|
|
2747
|
+
});
|
|
2748
|
+
const d = await r.json();
|
|
2749
|
+
if (d.error) { msgEl.textContent = 'Error: ' + d.error; msgEl.style.color = 'var(--red)'; return; }
|
|
2750
|
+
msgEl.textContent = `✓ Applied '${tier}' — restart Claude Code to activate`;
|
|
2751
|
+
msgEl.style.color = 'var(--green)';
|
|
2752
|
+
setTimeout(() => { msgEl.textContent = ''; msgEl.style.color = ''; }, 4000);
|
|
2753
|
+
loadPermissions();
|
|
2754
|
+
} catch(e) {
|
|
2755
|
+
msgEl.textContent = 'Error: ' + e.message;
|
|
2756
|
+
msgEl.style.color = 'var(--red)';
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
function setConfigOutput(text, kind = '') {
|
|
2761
|
+
const out = document.getElementById('config-output');
|
|
2762
|
+
out.textContent = text || '(no output)';
|
|
2763
|
+
out.className = 'cmd-output' + (kind ? ` ${kind}` : '');
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
async function runComponentUpdate(component) {
|
|
2767
|
+
if (!mcpModalPath) return;
|
|
2768
|
+
|
|
2769
|
+
// "Update All" runs each component in sequence
|
|
2770
|
+
if (component === 'all') {
|
|
2771
|
+
const components = ['config', 'index', 'embeddings', 'doc_index', 'dictionary', 'instructions', 'mcp'];
|
|
2772
|
+
setConfigOutput('Running all component updates…');
|
|
2773
|
+
let allOutput = '';
|
|
2774
|
+
let allOk = true;
|
|
2775
|
+
for (const c of components) {
|
|
2776
|
+
const row = document.querySelector(`.config-component-row[data-component="${c}"]`) ||
|
|
2777
|
+
[...document.querySelectorAll('.config-component-row')].find(el => el.querySelector('button')?.onclick?.toString().includes(`'${c}'`));
|
|
2778
|
+
try {
|
|
2779
|
+
const r = await fetch('/api/projects/run-component', {
|
|
2780
|
+
method: 'POST',
|
|
2781
|
+
headers: {'Content-Type':'application/json'},
|
|
2782
|
+
body: JSON.stringify({
|
|
2783
|
+
path: mcpModalPath, component: c,
|
|
2784
|
+
ide: document.getElementById('mcp-modal-ide')?.value?.trim() || null,
|
|
2785
|
+
mcp_mode: document.getElementById('mcp-modal-mode')?.value?.trim() || null,
|
|
2786
|
+
}),
|
|
2787
|
+
});
|
|
2788
|
+
const d = await r.json();
|
|
2789
|
+
allOutput += `── ${c} ──\n${d.output || d.error || '(no output)'}\n\n`;
|
|
2790
|
+
if (!d.success) allOk = false;
|
|
2791
|
+
} catch(e) {
|
|
2792
|
+
allOutput += `── ${c} ──\nError: ${e.message}\n\n`;
|
|
2793
|
+
allOk = false;
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
setConfigOutput(allOutput.trim(), allOk ? 'success' : 'failure');
|
|
2797
|
+
toast(allOk ? 'All components updated' : 'Some components failed', allOk ? 'ok' : 'err');
|
|
2798
|
+
delete detailsCache[mcpModalPath];
|
|
2799
|
+
await loadProjects();
|
|
2800
|
+
await loadConfigHealth();
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
// Single component update
|
|
2805
|
+
const btn = event?.target;
|
|
2806
|
+
const origText = btn?.textContent;
|
|
2807
|
+
if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spin">⟳</span>'; }
|
|
2808
|
+
setConfigOutput(`Updating ${component}…`);
|
|
2809
|
+
|
|
2810
|
+
try {
|
|
2811
|
+
const r = await fetch('/api/projects/run-component', {
|
|
2812
|
+
method: 'POST',
|
|
2813
|
+
headers: {'Content-Type':'application/json'},
|
|
2814
|
+
body: JSON.stringify({
|
|
2815
|
+
path: mcpModalPath, component,
|
|
2816
|
+
ide: document.getElementById('mcp-modal-ide')?.value?.trim() || null,
|
|
2817
|
+
mcp_mode: document.getElementById('mcp-modal-mode')?.value?.trim() || null,
|
|
2818
|
+
}),
|
|
2819
|
+
});
|
|
2820
|
+
const d = await r.json();
|
|
2821
|
+
if (d.error && !d.success) throw new Error(d.error);
|
|
2822
|
+
setConfigOutput(d.output || '(no output)', d.success ? 'success' : 'failure');
|
|
2823
|
+
toast(d.success ? `${component} updated` : `${component} failed`, d.success ? 'ok' : 'err');
|
|
2824
|
+
if (d.success) {
|
|
2825
|
+
delete detailsCache[mcpModalPath];
|
|
2826
|
+
await loadProjects();
|
|
2827
|
+
await loadConfigHealth();
|
|
2828
|
+
}
|
|
2829
|
+
} catch(e) {
|
|
2830
|
+
setConfigOutput('Error: ' + e.message, 'failure');
|
|
2831
|
+
toast('Error: ' + e.message, 'err');
|
|
2832
|
+
} finally {
|
|
2833
|
+
if (btn) { btn.disabled = false; btn.textContent = origText; }
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
// ── IDE Launcher Modal ─────────────────────────────────────────────────────
|
|
2838
|
+
function openIdeModal(path, currentIde) {
|
|
2839
|
+
idePath = path;
|
|
2840
|
+
ideSelected = currentIde || 'claude-code';
|
|
2841
|
+
document.getElementById('ide-modal-path').textContent = path;
|
|
2842
|
+
const out = document.getElementById('ide-cmd-output');
|
|
2843
|
+
out.textContent = '';
|
|
2844
|
+
out.classList.add('hidden');
|
|
2845
|
+
const btn = document.getElementById('ide-launch-btn');
|
|
2846
|
+
btn.textContent = '\u25B6 Launch';
|
|
2847
|
+
btn.disabled = false;
|
|
2848
|
+
btn.onclick = launchIde;
|
|
2849
|
+
renderIdePicker();
|
|
2850
|
+
openModal('ide-modal');
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
function renderIdePicker() {
|
|
2854
|
+
const picker = document.getElementById('ide-picker');
|
|
2855
|
+
picker.innerHTML = IDE_OPTIONS.map(opt => `
|
|
2856
|
+
<div class="ide-option${ideSelected === opt.id ? ' selected' : ''}"
|
|
2857
|
+
onclick="selectIdeOption('${opt.id}')">
|
|
2858
|
+
<span class="ide-icon">${opt.icon}</span>
|
|
2859
|
+
<span class="ide-name">${esc(opt.name)}</span>
|
|
2860
|
+
<span class="ide-cmd">${esc(opt.cmd)}</span>
|
|
2861
|
+
</div>`).join('');
|
|
2862
|
+
document.getElementById('ide-custom-row').style.display =
|
|
2863
|
+
ideSelected === 'custom' ? '' : 'none';
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
function selectIdeOption(id) {
|
|
2867
|
+
ideSelected = id;
|
|
2868
|
+
renderIdePicker();
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
async function launchIde() {
|
|
2872
|
+
if (!idePath || !ideSelected) return;
|
|
2873
|
+
const customCmd = ideSelected === 'custom'
|
|
2874
|
+
? document.getElementById('ide-custom-cmd').value.trim() : '';
|
|
2875
|
+
if (ideSelected === 'custom' && !customCmd) {
|
|
2876
|
+
toast('Enter a custom command first.', 'err'); return;
|
|
2877
|
+
}
|
|
2878
|
+
const btn = document.getElementById('ide-launch-btn');
|
|
2879
|
+
const out = document.getElementById('ide-cmd-output');
|
|
2880
|
+
btn.disabled = true;
|
|
2881
|
+
btn.textContent = 'Launching\u2026';
|
|
2882
|
+
out.textContent = '';
|
|
2883
|
+
out.classList.add('hidden');
|
|
2884
|
+
try {
|
|
2885
|
+
const resp = await fetch('/api/projects/launch-ide', {
|
|
2886
|
+
method: 'POST',
|
|
2887
|
+
headers: {'Content-Type': 'application/json'},
|
|
2888
|
+
body: JSON.stringify({ path: idePath, ide: ideSelected, custom_cmd: customCmd })
|
|
2889
|
+
});
|
|
2890
|
+
const data = await resp.json();
|
|
2891
|
+
if (data.error) {
|
|
2892
|
+
out.textContent = '\u2715 ' + data.error;
|
|
2893
|
+
out.classList.remove('hidden');
|
|
2894
|
+
btn.disabled = false;
|
|
2895
|
+
btn.textContent = '\u25B6 Launch';
|
|
2896
|
+
} else {
|
|
2897
|
+
out.textContent = '\u2713 Launched.';
|
|
2898
|
+
out.classList.remove('hidden');
|
|
2899
|
+
btn.disabled = false;
|
|
2900
|
+
btn.textContent = '\u25B6 Launch';
|
|
2901
|
+
}
|
|
2902
|
+
} catch (e) {
|
|
2903
|
+
out.textContent = '\u2715 ' + e.message;
|
|
2904
|
+
out.classList.remove('hidden');
|
|
2905
|
+
btn.disabled = false;
|
|
2906
|
+
btn.textContent = '\u25B6 Launch';
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
// ── Edit Modal ─────────────────────────────────────────────────────────────
|
|
2911
|
+
function openEditModal(path, name, tagsStr) {
|
|
2912
|
+
editPath = path;
|
|
2913
|
+
// Fetch latest data from allProjects for notes
|
|
2914
|
+
const proj = allProjects.find(p => p.path === path) || {};
|
|
2915
|
+
document.getElementById('edit-modal-path').textContent = path;
|
|
2916
|
+
document.getElementById('edit-name').value = proj.name || name || '';
|
|
2917
|
+
document.getElementById('edit-tags').value = (proj.tags || []).join(', ') || tagsStr || '';
|
|
2918
|
+
document.getElementById('edit-notes').value = proj.notes || '';
|
|
2919
|
+
document.getElementById('edit-save-btn').disabled = false;
|
|
2920
|
+
openModal('edit-modal');
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
async function saveEdit() {
|
|
2924
|
+
if (!editPath) return;
|
|
2925
|
+
const name = document.getElementById('edit-name').value.trim();
|
|
2926
|
+
const tags = document.getElementById('edit-tags').value;
|
|
2927
|
+
const notes = document.getElementById('edit-notes').value;
|
|
2928
|
+
const btn = document.getElementById('edit-save-btn');
|
|
2929
|
+
btn.disabled = true;
|
|
2930
|
+
try {
|
|
2931
|
+
const r = await fetch('/api/projects/update', {
|
|
2932
|
+
method: 'POST',
|
|
2933
|
+
headers: {'Content-Type':'application/json'},
|
|
2934
|
+
body: JSON.stringify({path: editPath, name, tags, notes}),
|
|
2935
|
+
});
|
|
2936
|
+
const d = await r.json();
|
|
2937
|
+
if (d.error) throw new Error(d.error);
|
|
2938
|
+
toast(d.updated ? 'Project updated' : 'Project not found', d.updated ? 'ok' : 'err');
|
|
2939
|
+
if (d.updated) { closeModal('edit-modal'); loadProjects(); }
|
|
2940
|
+
} catch(e) {
|
|
2941
|
+
toast('Error: ' + e.message, 'err');
|
|
2942
|
+
} finally {
|
|
2943
|
+
btn.disabled = false;
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
// ── Transfer Modal ────────────────────────────────────────────────────────
|
|
2948
|
+
|
|
2949
|
+
function openTransferModal(path, name) {
|
|
2950
|
+
transferPath = path;
|
|
2951
|
+
document.getElementById('transfer-modal-old-path').textContent = path;
|
|
2952
|
+
document.getElementById('transfer-new-path').value = '';
|
|
2953
|
+
document.getElementById('transfer-save-btn').disabled = false;
|
|
2954
|
+
openModal('transfer-modal');
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
async function saveTransfer() {
|
|
2958
|
+
if (!transferPath) return;
|
|
2959
|
+
const newPath = document.getElementById('transfer-new-path').value.trim();
|
|
2960
|
+
if (!newPath) { toast('New path is required', 'err'); return; }
|
|
2961
|
+
const btn = document.getElementById('transfer-save-btn');
|
|
2962
|
+
btn.disabled = true;
|
|
2963
|
+
try {
|
|
2964
|
+
const r = await fetch('/api/projects/transfer', {
|
|
2965
|
+
method: 'POST',
|
|
2966
|
+
headers: {'Content-Type':'application/json'},
|
|
2967
|
+
body: JSON.stringify({old_path: transferPath, new_path: newPath}),
|
|
2968
|
+
});
|
|
2969
|
+
const d = await r.json();
|
|
2970
|
+
if (d.error) throw new Error(d.error);
|
|
2971
|
+
toast(d.transferred ? 'Project transferred' : 'Transfer failed', d.transferred ? 'ok' : 'err');
|
|
2972
|
+
if (d.transferred) { closeModal('transfer-modal'); delete detailsCache[transferPath]; loadProjects(); }
|
|
2973
|
+
} catch(e) {
|
|
2974
|
+
toast('Error: ' + e.message, 'err');
|
|
2975
|
+
} finally {
|
|
2976
|
+
btn.disabled = false;
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
// ── Settings Modal ─────────────────────────────────────────────────────────
|
|
2981
|
+
async function openSettings() {
|
|
2982
|
+
try {
|
|
2983
|
+
const r = await fetch('/api/hub/config');
|
|
2984
|
+
const d = await r.json();
|
|
2985
|
+
document.getElementById('settings-port').value = d.port || 3330;
|
|
2986
|
+
document.getElementById('settings-auto-browser').checked = !!d.auto_open_browser;
|
|
2987
|
+
document.getElementById('settings-running-info').textContent =
|
|
2988
|
+
`Hub is running on port ${hubRunningPort}`;
|
|
2989
|
+
document.getElementById('settings-port-warning').style.display = 'none';
|
|
2990
|
+
// Session mode
|
|
2991
|
+
const hasTerminal = !!d.has_terminal;
|
|
2992
|
+
const badge = document.getElementById('session-mode-badge');
|
|
2993
|
+
const label = document.getElementById('session-mode-label');
|
|
2994
|
+
const detachBtn = document.getElementById('detach-terminal-btn');
|
|
2995
|
+
const modeInfo = document.getElementById('session-mode-info');
|
|
2996
|
+
if (hasTerminal) {
|
|
2997
|
+
badge.textContent = '⬛ terminal';
|
|
2998
|
+
badge.className = 'svc-badge svc-stopped';
|
|
2999
|
+
label.textContent = 'Running with terminal window attached';
|
|
3000
|
+
detachBtn.style.display = '';
|
|
3001
|
+
modeInfo.style.display = '';
|
|
3002
|
+
} else {
|
|
3003
|
+
badge.textContent = '● background';
|
|
3004
|
+
badge.className = 'svc-badge svc-ok';
|
|
3005
|
+
label.textContent = 'Running as background process (no terminal)';
|
|
3006
|
+
detachBtn.style.display = 'none';
|
|
3007
|
+
modeInfo.style.display = 'none';
|
|
3008
|
+
}
|
|
3009
|
+
} catch(e) {
|
|
3010
|
+
toast('Could not load hub config', 'err');
|
|
3011
|
+
return;
|
|
3012
|
+
}
|
|
3013
|
+
openModal('settings-modal');
|
|
3014
|
+
// Reset service output
|
|
3015
|
+
const out = document.getElementById('svc-output');
|
|
3016
|
+
out.textContent = ''; out.className = 'cmd-output hidden';
|
|
3017
|
+
// Load service status async
|
|
3018
|
+
loadServiceStatus();
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
async function loadServiceStatus() {
|
|
3022
|
+
setSvcBadge('checking…', 'svc-unknown');
|
|
3023
|
+
try {
|
|
3024
|
+
const r = await fetch('/api/hub/service');
|
|
3025
|
+
const d = await r.json();
|
|
3026
|
+
renderServiceStatus(d);
|
|
3027
|
+
} catch(e) {
|
|
3028
|
+
setSvcBadge('unavailable', 'svc-error');
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
function renderServiceStatus(d) {
|
|
3033
|
+
const installed = d.installed;
|
|
3034
|
+
const running = d.running; // null = unknown (windows), true/false otherwise
|
|
3035
|
+
|
|
3036
|
+
let label, cls;
|
|
3037
|
+
if (!installed) {
|
|
3038
|
+
label = 'not installed'; cls = 'svc-stopped';
|
|
3039
|
+
} else if (running === true) {
|
|
3040
|
+
label = '● running'; cls = 'svc-ok';
|
|
3041
|
+
} else if (running === false) {
|
|
3042
|
+
label = '○ stopped'; cls = 'svc-stopped';
|
|
3043
|
+
} else {
|
|
3044
|
+
label = '● installed'; cls = 'svc-ok';
|
|
3045
|
+
}
|
|
3046
|
+
setSvcBadge(label, cls);
|
|
3047
|
+
|
|
3048
|
+
const method = d.method || '';
|
|
3049
|
+
const portStr = d.port ? ` (port ${d.port})` : '';
|
|
3050
|
+
document.getElementById('svc-method').textContent = method + portStr;
|
|
3051
|
+
|
|
3052
|
+
const logRow = document.getElementById('svc-log-row');
|
|
3053
|
+
if (d.log_path) {
|
|
3054
|
+
document.getElementById('svc-log-path').textContent = d.log_path;
|
|
3055
|
+
logRow.style.display = 'block';
|
|
3056
|
+
} else {
|
|
3057
|
+
logRow.style.display = 'none';
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
// Button states
|
|
3061
|
+
document.getElementById('svc-install-btn').textContent = installed ? '↓ Reinstall' : '↓ Install Service';
|
|
3062
|
+
document.getElementById('svc-uninstall-btn').style.display = installed ? '' : 'none';
|
|
3063
|
+
document.getElementById('svc-start-btn').style.display = installed ? '' : 'none';
|
|
3064
|
+
document.getElementById('svc-stop-btn').textContent = '■ Stop Hub';
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
function setSvcBadge(text, cls) {
|
|
3068
|
+
const el = document.getElementById('svc-status-badge');
|
|
3069
|
+
el.textContent = text;
|
|
3070
|
+
el.className = 'svc-badge ' + cls;
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
async function svcAction(action) {
|
|
3074
|
+
const out = document.getElementById('svc-output');
|
|
3075
|
+
|
|
3076
|
+
if (action === 'stop') {
|
|
3077
|
+
const ok = await confirmDialog({ title: 'Stop Hub', message: 'The page will become unreachable until the hub is restarted.', confirmText: 'Stop', danger: true });
|
|
3078
|
+
if (!ok) return;
|
|
3079
|
+
}
|
|
3080
|
+
if (action === 'uninstall') {
|
|
3081
|
+
const ok = await confirmDialog({ title: 'Uninstall Service', message: 'The hub will no longer auto-start on login.', confirmText: 'Uninstall', danger: true });
|
|
3082
|
+
if (!ok) return;
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
out.textContent = action + '…';
|
|
3086
|
+
out.className = 'cmd-output';
|
|
3087
|
+
setSvcBadge('working…', 'svc-unknown');
|
|
3088
|
+
|
|
3089
|
+
try {
|
|
3090
|
+
const r = await fetch('/api/hub/service/' + action, {method: 'POST'});
|
|
3091
|
+
|
|
3092
|
+
if (action === 'stop') {
|
|
3093
|
+
// Server is shutting down — show stopped message
|
|
3094
|
+
out.textContent = 'Hub stopped. Restart with: c3 hub';
|
|
3095
|
+
out.className = 'cmd-output success';
|
|
3096
|
+
setSvcBadge('stopped', 'svc-stopped');
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
const d = await r.json();
|
|
3101
|
+
out.textContent = d.output || (d.success ? 'Done.' : 'Failed.');
|
|
3102
|
+
out.className = 'cmd-output ' + (d.success ? 'success' : 'failure');
|
|
3103
|
+
toast(d.success ? action + ' succeeded' : action + ' failed', d.success ? 'ok' : 'err');
|
|
3104
|
+
setTimeout(loadServiceStatus, 800);
|
|
3105
|
+
} catch(e) {
|
|
3106
|
+
if (action === 'stop') {
|
|
3107
|
+
out.textContent = 'Hub stopped. Restart with: c3 hub';
|
|
3108
|
+
out.className = 'cmd-output success';
|
|
3109
|
+
setSvcBadge('stopped', 'svc-stopped');
|
|
3110
|
+
} else {
|
|
3111
|
+
out.textContent = 'Error: ' + e.message;
|
|
3112
|
+
out.className = 'cmd-output failure';
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
async function saveSettings() {
|
|
3118
|
+
const portVal = parseInt(document.getElementById('settings-port').value, 10);
|
|
3119
|
+
const autoBrowser = document.getElementById('settings-auto-browser').checked;
|
|
3120
|
+
const btn = document.getElementById('settings-save-btn');
|
|
3121
|
+
if (isNaN(portVal) || portVal < 1024 || portVal > 65535) {
|
|
3122
|
+
toast('Port must be 1024–65535', 'err');
|
|
3123
|
+
return;
|
|
3124
|
+
}
|
|
3125
|
+
btn.disabled = true;
|
|
3126
|
+
try {
|
|
3127
|
+
const r = await fetch('/api/hub/config', {
|
|
3128
|
+
method: 'POST',
|
|
3129
|
+
headers: {'Content-Type':'application/json'},
|
|
3130
|
+
body: JSON.stringify({port: portVal, auto_open_browser: autoBrowser}),
|
|
3131
|
+
});
|
|
3132
|
+
const d = await r.json();
|
|
3133
|
+
if (d.error) throw new Error(d.error);
|
|
3134
|
+
hubDedicatedPort = portVal;
|
|
3135
|
+
document.getElementById('hub-port-badge').textContent = ':' + portVal;
|
|
3136
|
+
toast('Settings saved', 'ok');
|
|
3137
|
+
// Show restart warning if port differs from running port
|
|
3138
|
+
if (portVal != hubRunningPort) {
|
|
3139
|
+
const warn = document.getElementById('settings-port-warning');
|
|
3140
|
+
warn.textContent = `⚠ Restart hub to apply new port (currently running on :${hubRunningPort})`;
|
|
3141
|
+
warn.style.display = 'block';
|
|
3142
|
+
}
|
|
3143
|
+
} catch(e) {
|
|
3144
|
+
toast('Error: ' + e.message, 'err');
|
|
3145
|
+
} finally {
|
|
3146
|
+
btn.disabled = false;
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
// ── Modal generic open/close with animation ───────────────────────────────
|
|
3151
|
+
function openModal(id) {
|
|
3152
|
+
const el = document.getElementById(id);
|
|
3153
|
+
el.classList.remove('hidden');
|
|
3154
|
+
// Force reflow before adding visible class for CSS transition
|
|
3155
|
+
void el.offsetWidth;
|
|
3156
|
+
el.classList.add('modal-visible');
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
function closeModal(id) {
|
|
3160
|
+
const el = document.getElementById(id);
|
|
3161
|
+
el.classList.remove('modal-visible');
|
|
3162
|
+
setTimeout(() => {
|
|
3163
|
+
el.classList.add('hidden');
|
|
3164
|
+
}, 150); // match CSS transition duration
|
|
3165
|
+
if (id === 'mcp-modal') { mcpModalPath = null; }
|
|
3166
|
+
if (id === 'update-modal') { modalPath = null; }
|
|
3167
|
+
if (id === 'edit-modal') { editPath = null; }
|
|
3168
|
+
if (id === 'transfer-modal') { transferPath = null; }
|
|
3169
|
+
if (id === 'c3-setup-modal') { c3SetupPath = null; }
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
// Close modals on backdrop click
|
|
3173
|
+
['mcp-modal','edit-modal','transfer-modal','settings-modal','update-modal','batch-update-modal','ide-modal','c3-setup-modal'].forEach(id => {
|
|
3174
|
+
document.getElementById(id).addEventListener('click', e => {
|
|
3175
|
+
if (e.target === e.currentTarget) closeModal(id);
|
|
3176
|
+
});
|
|
3177
|
+
});
|
|
3178
|
+
|
|
3179
|
+
// ── Project actions ────────────────────────────────────────────────────────
|
|
3180
|
+
async function addProject() {
|
|
3181
|
+
const pathEl = document.getElementById('add-path');
|
|
3182
|
+
const nameEl = document.getElementById('add-name');
|
|
3183
|
+
const path = pathEl.value.trim(), name = nameEl.value.trim() || null;
|
|
3184
|
+
if (!path) { toast('Enter a project path', 'info'); pathEl.focus(); return; }
|
|
3185
|
+
|
|
3186
|
+
const btn = document.getElementById('add-btn');
|
|
3187
|
+
btn.disabled = true;
|
|
3188
|
+
try {
|
|
3189
|
+
const r = await fetch('/api/projects', {
|
|
3190
|
+
method: 'POST',
|
|
3191
|
+
headers: {'Content-Type':'application/json'},
|
|
3192
|
+
body: JSON.stringify({path, name}),
|
|
3193
|
+
});
|
|
3194
|
+
const d = await r.json();
|
|
3195
|
+
if (!r.ok) throw new Error(d.error || 'Unknown error');
|
|
3196
|
+
toast('Registered: ' + d.name, 'ok');
|
|
3197
|
+
pathEl.value = ''; nameEl.value = '';
|
|
3198
|
+
await loadProjects();
|
|
3199
|
+
if (!d.c3_initialized) openC3SetupModal(d.path || path);
|
|
3200
|
+
} catch(e) {
|
|
3201
|
+
toast('Error: ' + e.message, 'err');
|
|
3202
|
+
} finally {
|
|
3203
|
+
btn.disabled = false;
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
function openC3SetupModal(path) {
|
|
3208
|
+
c3SetupPath = path;
|
|
3209
|
+
const pathEl = document.getElementById('c3-setup-path');
|
|
3210
|
+
pathEl.textContent = path;
|
|
3211
|
+
pathEl.title = path;
|
|
3212
|
+
// Reset state
|
|
3213
|
+
document.getElementById('c3-setup-ide').value = '';
|
|
3214
|
+
document.getElementById('c3-setup-mcp-mode').value = '';
|
|
3215
|
+
document.getElementById('c3-setup-init-btn').disabled = false;
|
|
3216
|
+
document.getElementById('c3-setup-init-btn').textContent = 'Initialize';
|
|
3217
|
+
document.getElementById('c3-setup-mcp-btn').disabled = true;
|
|
3218
|
+
document.getElementById('c3-setup-done-btn').disabled = true;
|
|
3219
|
+
const out = document.getElementById('c3-setup-output');
|
|
3220
|
+
out.textContent = '';
|
|
3221
|
+
out.className = 'cmd-output hidden';
|
|
3222
|
+
document.getElementById('c3-setup-step1-badge').className = 'mcp-pill';
|
|
3223
|
+
document.getElementById('c3-setup-step2-badge').className = 'mcp-pill';
|
|
3224
|
+
openModal('c3-setup-modal');
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
function setC3SetupOutput(text, kind = '') {
|
|
3228
|
+
const out = document.getElementById('c3-setup-output');
|
|
3229
|
+
out.textContent = text || '(no output)';
|
|
3230
|
+
out.className = 'cmd-output' + (kind ? ` ${kind}` : '');
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
async function c3SetupRunInit() {
|
|
3234
|
+
if (!c3SetupPath) return;
|
|
3235
|
+
const btn = document.getElementById('c3-setup-init-btn');
|
|
3236
|
+
const ide = document.getElementById('c3-setup-ide').value.trim() || null;
|
|
3237
|
+
const mcpMode = document.getElementById('c3-setup-mcp-mode').value.trim() || null;
|
|
3238
|
+
btn.disabled = true;
|
|
3239
|
+
btn.innerHTML = '<span class="spin">⟳</span> Running…';
|
|
3240
|
+
setC3SetupOutput('Initializing C3…');
|
|
3241
|
+
try {
|
|
3242
|
+
const r = await fetch('/api/projects/run-init', {
|
|
3243
|
+
method: 'POST',
|
|
3244
|
+
headers: {'Content-Type':'application/json'},
|
|
3245
|
+
body: JSON.stringify({path: c3SetupPath, ide, mcp_mode: mcpMode, init_mode: 'force'}),
|
|
3246
|
+
});
|
|
3247
|
+
const d = await r.json();
|
|
3248
|
+
if (d.error) throw new Error(d.error);
|
|
3249
|
+
setC3SetupOutput(d.output || '(no output)', d.success ? 'success' : 'failure');
|
|
3250
|
+
if (d.success) {
|
|
3251
|
+
document.getElementById('c3-setup-step1-badge').className = 'mcp-pill ok';
|
|
3252
|
+
document.getElementById('c3-setup-mcp-btn').disabled = false;
|
|
3253
|
+
toast('C3 initialized', 'ok');
|
|
3254
|
+
delete detailsCache[c3SetupPath];
|
|
3255
|
+
await loadProjects();
|
|
3256
|
+
} else {
|
|
3257
|
+
toast('Init finished with errors', 'err');
|
|
3258
|
+
}
|
|
3259
|
+
} catch(e) {
|
|
3260
|
+
setC3SetupOutput('Error: ' + e.message, 'failure');
|
|
3261
|
+
toast('Error: ' + e.message, 'err');
|
|
3262
|
+
} finally {
|
|
3263
|
+
btn.disabled = false;
|
|
3264
|
+
btn.textContent = 'Re-run Init';
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
async function c3SetupRunMcp() {
|
|
3269
|
+
if (!c3SetupPath) return;
|
|
3270
|
+
const btn = document.getElementById('c3-setup-mcp-btn');
|
|
3271
|
+
const ide = document.getElementById('c3-setup-ide').value.trim() || null;
|
|
3272
|
+
const mcpMode = document.getElementById('c3-setup-mcp-mode').value.trim() || null;
|
|
3273
|
+
btn.disabled = true;
|
|
3274
|
+
btn.innerHTML = '<span class="spin">⟳</span> Installing…';
|
|
3275
|
+
setC3SetupOutput('Installing MCP server…');
|
|
3276
|
+
try {
|
|
3277
|
+
const r = await fetch('/api/projects/run-mcp', {
|
|
3278
|
+
method: 'POST',
|
|
3279
|
+
headers: {'Content-Type':'application/json'},
|
|
3280
|
+
body: JSON.stringify({path: c3SetupPath, ide, mcp_mode: mcpMode}),
|
|
3281
|
+
});
|
|
3282
|
+
const d = await r.json();
|
|
3283
|
+
if (d.error) throw new Error(d.error);
|
|
3284
|
+
setC3SetupOutput(d.output || '(no output)', d.success ? 'success' : 'failure');
|
|
3285
|
+
if (d.success) {
|
|
3286
|
+
document.getElementById('c3-setup-step2-badge').className = 'mcp-pill ok';
|
|
3287
|
+
document.getElementById('c3-setup-done-btn').disabled = false;
|
|
3288
|
+
toast('MCP server installed', 'ok');
|
|
3289
|
+
delete detailsCache[c3SetupPath];
|
|
3290
|
+
await loadProjects();
|
|
3291
|
+
} else {
|
|
3292
|
+
toast('MCP install finished with errors', 'err');
|
|
3293
|
+
}
|
|
3294
|
+
} catch(e) {
|
|
3295
|
+
setC3SetupOutput('Error: ' + e.message, 'failure');
|
|
3296
|
+
toast('Error: ' + e.message, 'err');
|
|
3297
|
+
} finally {
|
|
3298
|
+
btn.disabled = false;
|
|
3299
|
+
btn.textContent = 'Re-run Install';
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
|
|
3303
|
+
async function removeProject(path, name) {
|
|
3304
|
+
const ok = await confirmDialog({ title: 'Remove Project', message: `Remove "${name}" from the hub? Project files are NOT deleted.`, confirmText: 'Remove', danger: true });
|
|
3305
|
+
if (!ok) return;
|
|
3306
|
+
const r = await fetch('/api/projects/remove', {
|
|
3307
|
+
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
3308
|
+
body: JSON.stringify({path}),
|
|
3309
|
+
});
|
|
3310
|
+
const d = await r.json();
|
|
3311
|
+
toast(d.removed ? 'Removed: ' + name : 'Not found', d.removed ? 'ok' : 'info');
|
|
3312
|
+
delete detailsCache[path];
|
|
3313
|
+
loadProjects();
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
async function startSession(path) {
|
|
3317
|
+
toast('Launching session…', 'info');
|
|
3318
|
+
const r = await fetch('/api/sessions/start', {
|
|
3319
|
+
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
3320
|
+
body: JSON.stringify({path}),
|
|
3321
|
+
});
|
|
3322
|
+
const d = await r.json();
|
|
3323
|
+
if (d.error) { toast('Error: ' + d.error, 'err'); return; }
|
|
3324
|
+
toast(d.launched ? 'Session launched — watching…' : 'Launch failed', d.launched ? 'ok' : 'err');
|
|
3325
|
+
if (d.launched) {
|
|
3326
|
+
launchingPaths.add(path);
|
|
3327
|
+
renderProjects(); // immediate yellow-border feedback before first fetch
|
|
3328
|
+
|
|
3329
|
+
const project = allProjects.find(x => x.path === path);
|
|
3330
|
+
addSessionViewer(path, project?.name || path);
|
|
3331
|
+
|
|
3332
|
+
// Adaptive poll: 0ms, 0.5s, 1s, 1.5s, 2s, 2s, 3s, 3s, 3s, 5s, 5s, 5s (~28s, 12 checks)
|
|
3333
|
+
const DELAYS = [0, 500, 1000, 1500, 2000, 2000, 3000, 3000, 3000, 5000, 5000, 5000];
|
|
3334
|
+
let idx = 0;
|
|
3335
|
+
const poll = async () => {
|
|
3336
|
+
await loadProjects();
|
|
3337
|
+
await refreshSessionViewer(path, true);
|
|
3338
|
+
const p = allProjects.find(x => x.path === path);
|
|
3339
|
+
if (p?.active) { launchingPaths.delete(path); renderProjects(); return; }
|
|
3340
|
+
if (++idx >= DELAYS.length) {
|
|
3341
|
+
launchingPaths.delete(path);
|
|
3342
|
+
renderProjects();
|
|
3343
|
+
const pFinal = allProjects.find(x => x.path === path);
|
|
3344
|
+
if (!pFinal?.active) toast('Session did not become active — check the project log at .c3/ui.log', 'warn');
|
|
3345
|
+
return;
|
|
3346
|
+
}
|
|
3347
|
+
setTimeout(poll, DELAYS[idx]);
|
|
3348
|
+
};
|
|
3349
|
+
setTimeout(poll, DELAYS[idx]);
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
// (syncCmdModalOptions removed — init controls now live in the MCP modal's Setup tab)
|
|
3354
|
+
|
|
3355
|
+
async function restartSession(path, port) {
|
|
3356
|
+
toast('Restarting UI server…', 'info');
|
|
3357
|
+
const r = await fetch('/api/sessions/restart', {
|
|
3358
|
+
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
3359
|
+
body: JSON.stringify({path, port}),
|
|
3360
|
+
});
|
|
3361
|
+
const d = await r.json();
|
|
3362
|
+
toast(d.launched ? 'UI server restarted' : ('Restart failed: ' + (d.error||'')), d.launched ? 'ok' : 'err');
|
|
3363
|
+
setTimeout(loadProjects, 1500);
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
async function toggleAutostart(path, currentlyOn) {
|
|
3367
|
+
const enabled = !currentlyOn;
|
|
3368
|
+
const r = await fetch('/api/sessions/autostart', {
|
|
3369
|
+
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
3370
|
+
body: JSON.stringify({path, enabled}),
|
|
3371
|
+
});
|
|
3372
|
+
const d = await r.json();
|
|
3373
|
+
if (d.error) { toast('Error: ' + d.error, 'err'); return; }
|
|
3374
|
+
toast(enabled ? '⚡ Autostart enabled — UI will launch when Hub starts' : 'Autostart disabled', 'ok');
|
|
3375
|
+
loadProjects();
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
async function stopSession(port) {
|
|
3379
|
+
const r = await fetch('/api/sessions/stop', {
|
|
3380
|
+
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
3381
|
+
body: JSON.stringify({port}),
|
|
3382
|
+
});
|
|
3383
|
+
const d = await r.json();
|
|
3384
|
+
toast(d.stopped ? `Session :${port} stopped` : 'Stop failed', d.stopped ? 'ok' : 'err');
|
|
3385
|
+
setTimeout(loadProjects, 1000);
|
|
3386
|
+
sessionDrawerTabs.forEach(tab => { if (tab.port === port) tab.active = false; });
|
|
3387
|
+
renderSessionDrawer();
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3390
|
+
async function endMcpSession(path) {
|
|
3391
|
+
const r = await fetch('/api/sessions/end', {
|
|
3392
|
+
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
3393
|
+
body: JSON.stringify({path}),
|
|
3394
|
+
});
|
|
3395
|
+
const d = await r.json();
|
|
3396
|
+
toast(d.stopped ? 'Session ended' : 'No active session found', d.stopped ? 'ok' : 'info');
|
|
3397
|
+
loadProjects();
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3400
|
+
function openSession(port) { window.open('http://localhost:' + port, '_blank'); }
|
|
3401
|
+
|
|
3402
|
+
async function openFolder(path) {
|
|
3403
|
+
toast('Opening folder…', 'info');
|
|
3404
|
+
try {
|
|
3405
|
+
const r = await fetch('/api/projects/open', {
|
|
3406
|
+
method: 'POST',
|
|
3407
|
+
headers: {'Content-Type':'application/json'},
|
|
3408
|
+
body: JSON.stringify({path}),
|
|
3409
|
+
});
|
|
3410
|
+
const d = await r.json().catch(() => null);
|
|
3411
|
+
if (!r.ok) {
|
|
3412
|
+
if (r.status === 404) {
|
|
3413
|
+
throw new Error('Folder open endpoint is unavailable on this server. Restart the hub or update to the latest C3 instance.');
|
|
3414
|
+
}
|
|
3415
|
+
throw new Error(d?.error || `Server error (HTTP ${r.status})`);
|
|
3416
|
+
}
|
|
3417
|
+
if (d?.error) throw new Error(d.error);
|
|
3418
|
+
toast('Folder opened', 'ok');
|
|
3419
|
+
} catch(e) {
|
|
3420
|
+
toast('Error: ' + e.message, 'err');
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
async function updateAllProjects() {
|
|
3425
|
+
const outdated = allProjects.filter(p => p.c3_version && p.c3_version !== hubVersion);
|
|
3426
|
+
if (!outdated.length) return;
|
|
3427
|
+
_batchOutdated = outdated;
|
|
3428
|
+
|
|
3429
|
+
document.getElementById('batch-update-version').textContent = hubVersion;
|
|
3430
|
+
const list = document.getElementById('batch-update-list');
|
|
3431
|
+
list.innerHTML = outdated.map(p => `
|
|
3432
|
+
<div class="batch-item">
|
|
3433
|
+
<div style="overflow:hidden; flex:1; min-width:0;">
|
|
3434
|
+
<div class="batch-item-name">${esc(p.name)}</div>
|
|
3435
|
+
<div class="batch-item-path">${esc(p.path)}</div>
|
|
3436
|
+
</div>
|
|
3437
|
+
<span class="batch-item-status batch-status-pending" id="batch-item-${cardId(p.path)}">pending</span>
|
|
3438
|
+
</div>
|
|
3439
|
+
`).join('');
|
|
3440
|
+
|
|
3441
|
+
const log = document.getElementById('batch-update-log');
|
|
3442
|
+
log.textContent = '';
|
|
3443
|
+
log.className = 'cmd-output hidden';
|
|
3444
|
+
document.getElementById('batch-progress-bar').style.display = 'none';
|
|
3445
|
+
document.getElementById('batch-progress-fill').style.width = '0%';
|
|
3446
|
+
document.getElementById('batch-summary').style.display = 'none';
|
|
3447
|
+
document.getElementById('batch-retry-btn').style.display = 'none';
|
|
3448
|
+
|
|
3449
|
+
const runBtn = document.getElementById('batch-update-run-btn');
|
|
3450
|
+
runBtn.disabled = false;
|
|
3451
|
+
runBtn.textContent = 'Start Batch Update';
|
|
3452
|
+
runBtn.style.display = '';
|
|
3453
|
+
runBtn.onclick = runBatchUpdate;
|
|
3454
|
+
document.getElementById('batch-update-cancel').style.display = '';
|
|
3455
|
+
document.getElementById('batch-update-cancel').onclick = () => closeModal('batch-update-modal');
|
|
3456
|
+
document.getElementById('batch-cancel-running').style.display = 'none';
|
|
3457
|
+
|
|
3458
|
+
openModal('batch-update-modal');
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
// (old runBatchUpdate removed — replaced by polling-based version below)
|
|
3462
|
+
|
|
3463
|
+
// ── Utils ─────────────────────────────────────────────────────────────────
|
|
3464
|
+
function esc(s) {
|
|
3465
|
+
return String(s ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
function jsq(s) {
|
|
3469
|
+
return String(s ?? '')
|
|
3470
|
+
.replace(/\\/g, '\\\\')
|
|
3471
|
+
.replace(/'/g, "\\'")
|
|
3472
|
+
.replace(/\r/g, '\\r')
|
|
3473
|
+
.replace(/\n/g, '\\n')
|
|
3474
|
+
.replace(/\u2028/g, '\\u2028')
|
|
3475
|
+
.replace(/\u2029/g, '\\u2029');
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
// ── Toast Manager ─────────────────────────────────────────────────────────
|
|
3479
|
+
const ToastManager = (() => {
|
|
3480
|
+
let _id = 0;
|
|
3481
|
+
const _toasts = new Map();
|
|
3482
|
+
const _container = () => document.getElementById('toast-container');
|
|
3483
|
+
const ICONS = { ok: '\u2713', err: '\u2717', info: '\u24D8', warn: '\u26A0' };
|
|
3484
|
+
const MAX_VISIBLE = 5;
|
|
3485
|
+
|
|
3486
|
+
function _prune() {
|
|
3487
|
+
const container = _container();
|
|
3488
|
+
if (!container) return;
|
|
3489
|
+
const items = container.querySelectorAll('.toast-item');
|
|
3490
|
+
if (items.length > MAX_VISIBLE) {
|
|
3491
|
+
for (let i = 0; i < items.length - MAX_VISIBLE; i++) {
|
|
3492
|
+
_dismiss(items[i].dataset.tid);
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
function _dismiss(id) {
|
|
3498
|
+
const el = document.querySelector(`.toast-item[data-tid="${id}"]`);
|
|
3499
|
+
if (!el) return;
|
|
3500
|
+
el.classList.add('removing');
|
|
3501
|
+
el.classList.remove('show');
|
|
3502
|
+
const meta = _toasts.get(String(id));
|
|
3503
|
+
if (meta?.timer) clearTimeout(meta.timer);
|
|
3504
|
+
_toasts.delete(String(id));
|
|
3505
|
+
setTimeout(() => el.remove(), 200);
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3508
|
+
function show(msg, type = 'info', opts = {}) {
|
|
3509
|
+
const id = ++_id;
|
|
3510
|
+
const duration = opts.duration ?? (type === 'err' ? 6000 : 3400);
|
|
3511
|
+
const container = _container();
|
|
3512
|
+
if (!container) return id;
|
|
3513
|
+
|
|
3514
|
+
const el = document.createElement('div');
|
|
3515
|
+
el.className = `toast-item ${type}`;
|
|
3516
|
+
el.dataset.tid = id;
|
|
3517
|
+
el.innerHTML = `
|
|
3518
|
+
<span class="toast-icon">${ICONS[type] || ICONS.info}</span>
|
|
3519
|
+
<span class="toast-msg">${msg.replace(/</g,'<')}</span>
|
|
3520
|
+
<button class="toast-close" onclick="ToastManager.dismiss(${id})">×</button>
|
|
3521
|
+
`;
|
|
3522
|
+
container.appendChild(el);
|
|
3523
|
+
void el.offsetWidth;
|
|
3524
|
+
el.classList.add('show');
|
|
3525
|
+
|
|
3526
|
+
const meta = { el };
|
|
3527
|
+
if (duration > 0) {
|
|
3528
|
+
meta.timer = setTimeout(() => _dismiss(id), duration);
|
|
3529
|
+
}
|
|
3530
|
+
_toasts.set(String(id), meta);
|
|
3531
|
+
_prune();
|
|
3532
|
+
return id;
|
|
3533
|
+
}
|
|
3534
|
+
|
|
3535
|
+
function progress(msg, id) {
|
|
3536
|
+
return show(msg, 'info', { duration: 0 });
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
function update(id, patch) {
|
|
3540
|
+
const el = document.querySelector(`.toast-item[data-tid="${id}"]`);
|
|
3541
|
+
if (!el) return;
|
|
3542
|
+
if (patch.message) {
|
|
3543
|
+
const msgEl = el.querySelector('.toast-msg');
|
|
3544
|
+
if (msgEl) msgEl.textContent = patch.message;
|
|
3545
|
+
}
|
|
3546
|
+
if (patch.progress != null) {
|
|
3547
|
+
let bar = el.querySelector('.toast-progress');
|
|
3548
|
+
if (!bar) {
|
|
3549
|
+
bar = document.createElement('div');
|
|
3550
|
+
bar.className = 'toast-progress';
|
|
3551
|
+
el.appendChild(bar);
|
|
3552
|
+
}
|
|
3553
|
+
bar.style.width = Math.min(100, patch.progress) + '%';
|
|
3554
|
+
}
|
|
3555
|
+
if (patch.type) {
|
|
3556
|
+
el.className = `toast-item ${patch.type} show`;
|
|
3557
|
+
const icon = el.querySelector('.toast-icon');
|
|
3558
|
+
if (icon) icon.textContent = ICONS[patch.type] || ICONS.info;
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
function dismiss(id) { _dismiss(id); }
|
|
3563
|
+
|
|
3564
|
+
return { show, progress, update, dismiss };
|
|
3565
|
+
})();
|
|
3566
|
+
|
|
3567
|
+
// Backward-compatible wrapper
|
|
3568
|
+
function toast(msg, type = 'info') {
|
|
3569
|
+
return ToastManager.show(msg, type);
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
// ── Confirm Dialog ────────────────────────────────────────────────────────
|
|
3573
|
+
function confirmDialog({ title, message, confirmText = 'Confirm', danger = false }) {
|
|
3574
|
+
return new Promise(resolve => {
|
|
3575
|
+
const root = document.getElementById('confirm-root');
|
|
3576
|
+
const overlay = document.createElement('div');
|
|
3577
|
+
overlay.className = 'confirm-overlay';
|
|
3578
|
+
overlay.innerHTML = `
|
|
3579
|
+
<div class="confirm-dialog">
|
|
3580
|
+
<div class="confirm-title">${esc(title)}</div>
|
|
3581
|
+
<div class="confirm-message">${esc(message)}</div>
|
|
3582
|
+
<div class="confirm-actions">
|
|
3583
|
+
<button class="btn btn-ghost" id="confirm-cancel-btn">Cancel</button>
|
|
3584
|
+
<button class="btn ${danger ? 'btn-danger' : 'btn-primary'}" id="confirm-ok-btn">${esc(confirmText)}</button>
|
|
3585
|
+
</div>
|
|
3586
|
+
</div>
|
|
3587
|
+
`;
|
|
3588
|
+
root.appendChild(overlay);
|
|
3589
|
+
|
|
3590
|
+
const cleanup = (result) => {
|
|
3591
|
+
overlay.remove();
|
|
3592
|
+
resolve(result);
|
|
3593
|
+
};
|
|
3594
|
+
overlay.querySelector('#confirm-cancel-btn').onclick = () => cleanup(false);
|
|
3595
|
+
overlay.querySelector('#confirm-ok-btn').onclick = () => cleanup(true);
|
|
3596
|
+
overlay.addEventListener('click', e => { if (e.target === overlay) cleanup(false); });
|
|
3597
|
+
document.addEventListener('keydown', function handler(e) {
|
|
3598
|
+
if (e.key === 'Escape') { document.removeEventListener('keydown', handler); cleanup(false); }
|
|
3599
|
+
});
|
|
3600
|
+
});
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
// ── Batch Update (polling-based) ──────────────────────────────────────────
|
|
3604
|
+
let _batchPollTimer = null;
|
|
3605
|
+
let _batchOutdated = [];
|
|
3606
|
+
|
|
3607
|
+
async function runBatchUpdate() {
|
|
3608
|
+
const runBtn = document.getElementById('batch-update-run-btn');
|
|
3609
|
+
const cancelBtn = document.getElementById('batch-update-cancel');
|
|
3610
|
+
const cancelRunning = document.getElementById('batch-cancel-running');
|
|
3611
|
+
const log = document.getElementById('batch-update-log');
|
|
3612
|
+
const progressBar = document.getElementById('batch-progress-bar');
|
|
3613
|
+
const progressFill = document.getElementById('batch-progress-fill');
|
|
3614
|
+
|
|
3615
|
+
runBtn.disabled = true;
|
|
3616
|
+
runBtn.style.display = 'none';
|
|
3617
|
+
cancelBtn.style.display = 'none';
|
|
3618
|
+
cancelRunning.style.display = '';
|
|
3619
|
+
progressBar.style.display = '';
|
|
3620
|
+
progressFill.style.width = '0%';
|
|
3621
|
+
log.textContent = 'Starting batch update...\n';
|
|
3622
|
+
log.className = 'cmd-output';
|
|
3623
|
+
|
|
3624
|
+
try {
|
|
3625
|
+
const r = await fetch('/api/projects/run-init/batch', {
|
|
3626
|
+
method: 'POST',
|
|
3627
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3628
|
+
body: JSON.stringify({ projects: _batchOutdated.map(p => ({ path: p.path, name: p.name, ide: p.ide })) })
|
|
3629
|
+
});
|
|
3630
|
+
const d = await r.json();
|
|
3631
|
+
if (d.error) throw new Error(d.error);
|
|
3632
|
+
_startBatchPoll();
|
|
3633
|
+
} catch(e) {
|
|
3634
|
+
log.textContent += '\nError: ' + e.message;
|
|
3635
|
+
log.className = 'cmd-output failure';
|
|
3636
|
+
toast('Batch update error: ' + e.message, 'err');
|
|
3637
|
+
_batchResetButtons();
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3641
|
+
function _startBatchPoll() {
|
|
3642
|
+
clearInterval(_batchPollTimer);
|
|
3643
|
+
_batchPollTimer = setInterval(_pollBatchStatus, 1000);
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3646
|
+
async function _pollBatchStatus() {
|
|
3647
|
+
try {
|
|
3648
|
+
const r = await fetch('/api/projects/run-init/batch/status');
|
|
3649
|
+
const state = await r.json();
|
|
3650
|
+
_renderBatchProgress(state);
|
|
3651
|
+
if (state.done || (!state.running && state.results?.length > 0)) {
|
|
3652
|
+
clearInterval(_batchPollTimer);
|
|
3653
|
+
_batchPollTimer = null;
|
|
3654
|
+
_batchFinished(state);
|
|
3655
|
+
}
|
|
3656
|
+
} catch(e) {
|
|
3657
|
+
// network hiccup, keep polling
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
function _renderBatchProgress(state) {
|
|
3662
|
+
const total = state.total || 1;
|
|
3663
|
+
const done = (state.results || []).length;
|
|
3664
|
+
const pct = Math.round((done / total) * 100);
|
|
3665
|
+
const progressFill = document.getElementById('batch-progress-fill');
|
|
3666
|
+
if (progressFill) progressFill.style.width = pct + '%';
|
|
3667
|
+
|
|
3668
|
+
// Update individual items
|
|
3669
|
+
(state.results || []).forEach(res => {
|
|
3670
|
+
const cid = cardId(res.path);
|
|
3671
|
+
const el = document.getElementById(`batch-item-${cid}`);
|
|
3672
|
+
if (el) {
|
|
3673
|
+
const cls = res.success ? 'batch-status-done' : 'batch-status-failed';
|
|
3674
|
+
el.textContent = res.success ? 'DONE' : 'FAILED';
|
|
3675
|
+
el.className = 'batch-item-status ' + cls;
|
|
3676
|
+
}
|
|
3677
|
+
});
|
|
3678
|
+
|
|
3679
|
+
// Mark currently running
|
|
3680
|
+
if (state.current) {
|
|
3681
|
+
const list = document.getElementById('batch-update-list');
|
|
3682
|
+
const items = list?.querySelectorAll('.batch-item-status');
|
|
3683
|
+
items?.forEach(el => {
|
|
3684
|
+
if (el.textContent === 'running...') {
|
|
3685
|
+
el.textContent = 'pending';
|
|
3686
|
+
el.className = 'batch-item-status batch-status-pending';
|
|
3687
|
+
}
|
|
3688
|
+
});
|
|
3689
|
+
// Find the current one by index
|
|
3690
|
+
const allItems = list?.querySelectorAll('.batch-item');
|
|
3691
|
+
if (allItems && state.current_index < allItems.length) {
|
|
3692
|
+
const statusEl = allItems[state.current_index]?.querySelector('.batch-item-status');
|
|
3693
|
+
if (statusEl && statusEl.textContent === 'pending') {
|
|
3694
|
+
statusEl.textContent = 'running...';
|
|
3695
|
+
statusEl.className = 'batch-item-status batch-status-running';
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3700
|
+
const log = document.getElementById('batch-update-log');
|
|
3701
|
+
if (log && state.results?.length > 0) {
|
|
3702
|
+
log.textContent = state.results.map(res =>
|
|
3703
|
+
`[${res.success ? 'OK' : 'ERR'}] ${res.name || res.path}${!res.success && res.output ? '\n ' + res.output.split('\n').slice(-2).join('\n ') : ''}`
|
|
3704
|
+
).join('\n');
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
function _batchFinished(state) {
|
|
3709
|
+
const results = state.results || [];
|
|
3710
|
+
const successCount = results.filter(r => r.success).length;
|
|
3711
|
+
const failCount = results.filter(r => !r.success).length;
|
|
3712
|
+
const cancelled = state.cancelled;
|
|
3713
|
+
|
|
3714
|
+
const summary = document.getElementById('batch-summary');
|
|
3715
|
+
if (summary) {
|
|
3716
|
+
summary.style.display = '';
|
|
3717
|
+
summary.textContent = `Batch update ${cancelled ? 'cancelled' : 'finished'}. ${successCount} succeeded, ${failCount} failed` +
|
|
3718
|
+
(cancelled ? ` (${state.total - results.length} skipped)` : '') + '.';
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3721
|
+
const log = document.getElementById('batch-update-log');
|
|
3722
|
+
if (log) log.className = 'cmd-output ' + (failCount === 0 && !cancelled ? 'success' : 'failure');
|
|
3723
|
+
|
|
3724
|
+
toast(`Batch ${cancelled ? 'cancelled' : 'finished'} (${successCount} ok, ${failCount} err)`, failCount === 0 && !cancelled ? 'ok' : 'err');
|
|
3725
|
+
|
|
3726
|
+
// Show retry button if there were failures
|
|
3727
|
+
const retryBtn = document.getElementById('batch-retry-btn');
|
|
3728
|
+
if (retryBtn) retryBtn.style.display = failCount > 0 ? '' : 'none';
|
|
3729
|
+
|
|
3730
|
+
_batchResetButtons();
|
|
3731
|
+
loadProjects();
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
function _batchResetButtons() {
|
|
3735
|
+
const runBtn = document.getElementById('batch-update-run-btn');
|
|
3736
|
+
const cancelBtn = document.getElementById('batch-update-cancel');
|
|
3737
|
+
const cancelRunning = document.getElementById('batch-cancel-running');
|
|
3738
|
+
runBtn.style.display = '';
|
|
3739
|
+
runBtn.disabled = false;
|
|
3740
|
+
runBtn.textContent = 'Close';
|
|
3741
|
+
runBtn.onclick = () => closeModal('batch-update-modal');
|
|
3742
|
+
cancelBtn.style.display = 'none';
|
|
3743
|
+
cancelRunning.style.display = 'none';
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
async function cancelBatchUpdate() {
|
|
3747
|
+
if (_batchPollTimer) {
|
|
3748
|
+
try {
|
|
3749
|
+
await fetch('/api/projects/run-init/batch/cancel', { method: 'POST' });
|
|
3750
|
+
toast('Cancellation requested...', 'warn');
|
|
3751
|
+
} catch(e) {}
|
|
3752
|
+
} else {
|
|
3753
|
+
closeModal('batch-update-modal');
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3757
|
+
async function retryFailedBatch() {
|
|
3758
|
+
// Re-open the batch modal with only failed projects
|
|
3759
|
+
closeModal('batch-update-modal');
|
|
3760
|
+
toast('Re-run batch for the full project list to retry.', 'info');
|
|
3761
|
+
}
|
|
3762
|
+
</script>
|
|
3763
|
+
</body>
|
|
3764
|
+
</html>
|