agentdashpulse 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/package.json +38 -0
- package/public/index.html +1532 -0
- package/server.js +1636 -0
|
@@ -0,0 +1,1532 @@
|
|
|
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>AgentPulse — AI Agent Management Dashboard</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0d1117; --surface: #161b22; --surface2: #1c2128; --surface3: #21262d;
|
|
10
|
+
--border: #30363d; --text: #e6edf3; --text-dim: #8b949e; --text-faint: #484f58;
|
|
11
|
+
--accent: #58a6ff; --green: #3fb950; --red: #f85149; --orange: #d29922;
|
|
12
|
+
--purple: #bc8cff; --cyan: #39d2c0; --pink: #f778ba; --yellow: #e3b341;
|
|
13
|
+
}
|
|
14
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
15
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; }
|
|
16
|
+
|
|
17
|
+
header { background: var(--surface); border-bottom: 1px solid var(--border); padding: 12px 24px; display: flex; align-items: center; justify-content: space-between; }
|
|
18
|
+
header h1 { font-size: 16px; font-weight: 600; }
|
|
19
|
+
.header-right { display: flex; align-items: center; gap: 16px; font-size: 12px; }
|
|
20
|
+
.header-stats { display: flex; gap: 16px; color: var(--text-dim); }
|
|
21
|
+
.header-stats span { font-weight: 600; color: var(--text); }
|
|
22
|
+
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
|
23
|
+
.status-dot.alive { background: var(--green); box-shadow: 0 0 6px var(--green); }
|
|
24
|
+
.status-dot.dead { background: var(--text-dim); }
|
|
25
|
+
|
|
26
|
+
.filter-bar { background: var(--surface); border-bottom: 1px solid var(--border); padding: 8px 24px; display: flex; gap: 8px; align-items: center; }
|
|
27
|
+
.filter-bar button { background: var(--surface2); border: 1px solid var(--border); color: var(--text-dim); padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; }
|
|
28
|
+
.filter-bar button.active { border-color: var(--accent); color: var(--accent); background: #58a6ff15; }
|
|
29
|
+
.filter-bar button:hover { border-color: var(--text-dim); }
|
|
30
|
+
|
|
31
|
+
.container { display: flex; height: calc(100vh - 180px); }
|
|
32
|
+
|
|
33
|
+
/* Top-level navigation */
|
|
34
|
+
.top-nav { background: var(--surface); border-bottom: 2px solid var(--border); padding: 0 24px; display: flex; gap: 0; }
|
|
35
|
+
.top-nav-tab { padding: 10px 24px; font-size: 14px; font-weight: 600; color: var(--text-dim); cursor: pointer; border-bottom: 3px solid transparent; margin-bottom: -2px; transition: color 0.15s; }
|
|
36
|
+
.top-nav-tab:hover { color: var(--text); }
|
|
37
|
+
.top-nav-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
38
|
+
.top-nav-tab .nav-icon { margin-right: 6px; }
|
|
39
|
+
|
|
40
|
+
/* Settings page */
|
|
41
|
+
.settings-page { padding: 24px; overflow-y: auto; height: calc(100vh - 135px); }
|
|
42
|
+
.settings-page-section { margin-bottom: 28px; padding: 16px; border: 1px solid var(--border); border-radius: 10px; background: var(--bg); }
|
|
43
|
+
.settings-page-section > h2 { font-size: 16px; font-weight: 700; margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; }
|
|
44
|
+
.project-grid { display: flex; flex-direction: column; gap: 8px; }
|
|
45
|
+
.project-card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
|
|
46
|
+
.project-card-header { padding: 12px 16px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; cursor: pointer; }
|
|
47
|
+
.project-card-header:hover { background: var(--surface2); }
|
|
48
|
+
.project-card-header h3 { font-size: 13px; font-weight: 600; }
|
|
49
|
+
.project-card-body { padding: 14px 16px; display: none; }
|
|
50
|
+
.project-card-body.open { display: block; }
|
|
51
|
+
.project-card .project-stats { display: flex; gap: 12px; font-size: 11px; color: var(--text-dim); }
|
|
52
|
+
.project-card .project-stats .stat-val { font-weight: 600; color: var(--text); }
|
|
53
|
+
|
|
54
|
+
/* Session List */
|
|
55
|
+
.session-list { width: 360px; min-width: 360px; border-right: 1px solid var(--border); overflow-y: auto; background: var(--surface); }
|
|
56
|
+
.session-card { padding: 12px 16px; border-bottom: 1px solid var(--border); cursor: pointer; transition: background 0.1s; }
|
|
57
|
+
.session-card:hover { background: var(--surface2); }
|
|
58
|
+
.session-card.selected { background: var(--surface2); border-left: 3px solid var(--accent); padding-left: 13px; }
|
|
59
|
+
.session-title { font-size: 13px; font-weight: 600; margin-bottom: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: flex; align-items: center; gap: 6px; }
|
|
60
|
+
.session-meta { font-size: 11px; color: var(--text-dim); display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
61
|
+
.badge { padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; }
|
|
62
|
+
.badge.vscode { background: #1f6feb22; color: var(--accent); }
|
|
63
|
+
.badge.cli { background: #3fb95022; color: var(--green); }
|
|
64
|
+
.badge.web { background: #bc8cff22; color: var(--purple); }
|
|
65
|
+
.badge.unknown { background: #30363d; color: var(--text-dim); }
|
|
66
|
+
.badge.historical { background: #d2992222; color: var(--orange); }
|
|
67
|
+
.session-stats { font-size: 10px; color: var(--text-faint); margin-top: 3px; display: flex; gap: 8px; }
|
|
68
|
+
.agent-dots { display: flex; gap: 3px; margin-left: auto; }
|
|
69
|
+
.agent-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--cyan); }
|
|
70
|
+
.agent-dot.completed { background: var(--green); }
|
|
71
|
+
|
|
72
|
+
/* Detail Panel */
|
|
73
|
+
.detail-panel { flex: 1; overflow-y: auto; }
|
|
74
|
+
.detail-panel .empty-state { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--text-dim); font-size: 14px; }
|
|
75
|
+
|
|
76
|
+
/* Tabs */
|
|
77
|
+
.tab-bar { display: flex; border-bottom: 1px solid var(--border); background: var(--surface); position: sticky; top: 0; z-index: 10; }
|
|
78
|
+
.tab { padding: 10px 20px; font-size: 13px; color: var(--text-dim); cursor: pointer; border-bottom: 2px solid transparent; }
|
|
79
|
+
.tab:hover { color: var(--text); }
|
|
80
|
+
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
81
|
+
.tab-content { padding: 20px 24px; }
|
|
82
|
+
|
|
83
|
+
/* Info Cards */
|
|
84
|
+
.info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-bottom: 20px; }
|
|
85
|
+
.info-card { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 10px 14px; }
|
|
86
|
+
.info-card .label { font-size: 10px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; }
|
|
87
|
+
.info-card .value { font-size: 18px; font-weight: 700; }
|
|
88
|
+
.info-card .value.small { font-size: 12px; font-weight: 500; }
|
|
89
|
+
|
|
90
|
+
/* Agent Workflow */
|
|
91
|
+
.workflow-section { margin-bottom: 20px; }
|
|
92
|
+
.workflow-section h3 { font-size: 14px; font-weight: 600; margin-bottom: 10px; color: var(--text-dim); }
|
|
93
|
+
.agent-card { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 10px 14px; margin-bottom: 8px; display: flex; align-items: center; gap: 12px; }
|
|
94
|
+
.agent-card .agent-icon { width: 32px; height: 32px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 700; flex-shrink: 0; }
|
|
95
|
+
.agent-icon.main { background: #58a6ff22; color: var(--accent); }
|
|
96
|
+
.agent-icon.explore { background: #39d2c022; color: var(--cyan); }
|
|
97
|
+
.agent-icon.general { background: #bc8cff22; color: var(--purple); }
|
|
98
|
+
.agent-icon.guide { background: #f778ba22; color: var(--pink); }
|
|
99
|
+
.agent-card .agent-info { flex: 1; min-width: 0; }
|
|
100
|
+
.agent-card .agent-name { font-size: 12px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
101
|
+
.agent-card .agent-type { font-size: 10px; color: var(--text-dim); }
|
|
102
|
+
.agent-card .agent-status { font-size: 10px; padding: 2px 8px; border-radius: 10px; }
|
|
103
|
+
.agent-status.running { background: #d2992222; color: var(--orange); animation: pulse 1.5s infinite; }
|
|
104
|
+
.agent-status.completed { background: #3fb95022; color: var(--green); }
|
|
105
|
+
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
106
|
+
|
|
107
|
+
/* Compound Knowledge */
|
|
108
|
+
.knowledge-card { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 14px 16px; margin-bottom: 10px; }
|
|
109
|
+
.knowledge-card h4 { font-size: 13px; margin-bottom: 6px; color: var(--cyan); }
|
|
110
|
+
.knowledge-card p { font-size: 12px; color: var(--text-dim); line-height: 1.6; }
|
|
111
|
+
.knowledge-card .k-meta { font-size: 10px; color: var(--text-faint); margin-top: 6px; }
|
|
112
|
+
.knowledge-actions { display: flex; gap: 8px; margin-top: 8px; }
|
|
113
|
+
.knowledge-actions button { padding: 4px 12px; border-radius: 4px; font-size: 11px; cursor: pointer; border: 1px solid var(--border); }
|
|
114
|
+
.btn-approve { background: var(--green)22; color: var(--green); border-color: var(--green)44; }
|
|
115
|
+
.btn-skip { background: var(--surface2); color: var(--text-dim); }
|
|
116
|
+
.btn-extract { background: var(--cyan)22; color: var(--cyan); border-color: var(--cyan)44; padding: 8px 20px; font-size: 13px; }
|
|
117
|
+
.compound-intro { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 16px; margin-bottom: 16px; font-size: 12px; color: var(--text-dim); line-height: 1.6; }
|
|
118
|
+
.compound-intro strong { color: var(--text); }
|
|
119
|
+
.spinner { width:24px;height:24px;border:3px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite; }
|
|
120
|
+
@keyframes spin { to { transform:rotate(360deg); } }
|
|
121
|
+
|
|
122
|
+
/* Settings & Memory */
|
|
123
|
+
.settings-section { margin-bottom: 20px; }
|
|
124
|
+
.settings-header { display: flex; justify-content: space-between; align-items: center; cursor: pointer; padding: 12px 16px; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; }
|
|
125
|
+
.settings-header:hover { border-color: var(--accent)44; }
|
|
126
|
+
.settings-header h3 { font-size: 13px; margin: 0; }
|
|
127
|
+
.settings-body { border: 1px solid var(--border); border-top: none; border-radius: 0 0 6px 6px; padding: 14px 16px; background: var(--bg); }
|
|
128
|
+
.settings-code { background: var(--surface); border: 1px solid var(--border); border-radius: 4px; padding: 12px; font-family: 'Consolas','Monaco',monospace; font-size: 11px; color: var(--text); white-space: pre-wrap; word-break: break-all; max-height: 400px; overflow-y: auto; line-height: 1.6; }
|
|
129
|
+
.settings-code.editable { background: var(--bg); cursor: text; }
|
|
130
|
+
.memory-card { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 12px 14px; margin-bottom: 8px; }
|
|
131
|
+
.memory-card .mem-header { display: flex; justify-content: space-between; align-items: center; }
|
|
132
|
+
.memory-card .mem-type { font-size: 10px; padding: 2px 8px; border-radius: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.3px; }
|
|
133
|
+
.mem-type.user { background: var(--accent)22; color: var(--accent); }
|
|
134
|
+
.mem-type.feedback { background: var(--orange)22; color: var(--orange); }
|
|
135
|
+
.mem-type.project { background: var(--green)22; color: var(--green); }
|
|
136
|
+
.mem-type.reference { background: var(--cyan)22; color: var(--cyan); }
|
|
137
|
+
.mem-type.unknown { background: var(--surface2); color: var(--text-dim); }
|
|
138
|
+
|
|
139
|
+
/* TODO List */
|
|
140
|
+
.todo-item { padding: 8px 12px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 10px; font-size: 13px; }
|
|
141
|
+
.todo-item:last-child { border-bottom: none; }
|
|
142
|
+
.todo-check { width: 16px; height: 16px; border-radius: 3px; border: 2px solid var(--border); flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 10px; }
|
|
143
|
+
.todo-check.completed { background: var(--green); border-color: var(--green); color: #fff; }
|
|
144
|
+
.todo-check.in_progress { background: var(--orange); border-color: var(--orange); color: #fff; }
|
|
145
|
+
.todo-check.pending { }
|
|
146
|
+
.todo-text { flex: 1; }
|
|
147
|
+
.todo-text.completed { color: var(--text-dim); text-decoration: line-through; }
|
|
148
|
+
.todo-text.in_progress { color: var(--orange); font-weight: 600; }
|
|
149
|
+
|
|
150
|
+
/* Event Feed */
|
|
151
|
+
.event-feed { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
|
|
152
|
+
.event-feed-header { padding: 8px 14px; border-bottom: 1px solid var(--border); font-size: 12px; font-weight: 600; color: var(--text-dim); display: flex; justify-content: space-between; }
|
|
153
|
+
.event-item { padding: 8px 14px; border-bottom: 1px solid var(--border); font-size: 12px; line-height: 1.5; }
|
|
154
|
+
.event-item:last-child { border-bottom: none; }
|
|
155
|
+
.event-type { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.3px; display: inline-block; margin-right: 6px; }
|
|
156
|
+
.event-type.user { color: var(--accent); }
|
|
157
|
+
.event-type.assistant { color: var(--green); }
|
|
158
|
+
.event-type.skill { color: var(--pink); }
|
|
159
|
+
.event-text { color: var(--text); word-break: break-word; display: inline; }
|
|
160
|
+
.event-tools { margin-top: 3px; }
|
|
161
|
+
.tool-tag { display: inline-block; padding: 1px 5px; margin: 1px 3px 1px 0; border-radius: 3px; font-size: 10px; background: var(--surface2); border: 1px solid var(--border); color: var(--orange); }
|
|
162
|
+
.tool-tag.agent { color: var(--cyan); border-color: var(--cyan)44; }
|
|
163
|
+
.tool-input { color: var(--text-faint); font-size: 10px; margin-left: 2px; }
|
|
164
|
+
.event-time { font-size: 10px; color: var(--text-faint); float: right; }
|
|
165
|
+
|
|
166
|
+
.empty-state { display: flex; align-items: center; justify-content: center; min-height: 200px; color: var(--text-dim); font-size: 13px; }
|
|
167
|
+
|
|
168
|
+
.conn-status { font-size: 11px; display: flex; align-items: center; gap: 4px; }
|
|
169
|
+
.conn-status.ok { color: var(--green); }
|
|
170
|
+
.conn-status.err { color: var(--red); }
|
|
171
|
+
|
|
172
|
+
/* Event filter bar */
|
|
173
|
+
.event-filter-bar { display: flex; gap: 6px; align-items: center; margin-bottom: 12px; flex-wrap: wrap; }
|
|
174
|
+
.event-filter-bar .ef-btn { background: var(--surface2); border: 1px solid var(--border); color: var(--text-dim); padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 11px; }
|
|
175
|
+
.event-filter-bar .ef-btn.active { border-color: var(--accent); color: var(--accent); background: #58a6ff15; }
|
|
176
|
+
.event-filter-bar .ef-btn:hover { border-color: var(--text-dim); }
|
|
177
|
+
.event-filter-bar .ef-sep { width: 1px; height: 20px; background: var(--border); margin: 0 4px; }
|
|
178
|
+
.event-pager { display: flex; gap: 8px; align-items: center; justify-content: center; margin-top: 12px; }
|
|
179
|
+
.event-pager button { background: var(--surface2); border: 1px solid var(--border); color: var(--text-dim); padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 11px; }
|
|
180
|
+
.event-pager button:hover { border-color: var(--text-dim); }
|
|
181
|
+
.event-pager button:disabled { opacity: 0.3; cursor: default; }
|
|
182
|
+
.event-pager span { font-size: 11px; color: var(--text-dim); }
|
|
183
|
+
|
|
184
|
+
/* Workflow timeline */
|
|
185
|
+
.workflow-timeline { position: relative; padding-left: 24px; }
|
|
186
|
+
.workflow-timeline::before { content: ''; position: absolute; left: 10px; top: 0; bottom: 0; width: 2px; background: var(--border); }
|
|
187
|
+
.wf-node { position: relative; margin-bottom: 12px; }
|
|
188
|
+
.wf-node::before { content: ''; position: absolute; left: -18px; top: 6px; width: 10px; height: 10px; border-radius: 50%; border: 2px solid var(--border); background: var(--bg); }
|
|
189
|
+
.wf-node.user::before { border-color: var(--accent); background: var(--accent); }
|
|
190
|
+
.wf-node.agent::before { border-color: var(--cyan); background: var(--cyan); }
|
|
191
|
+
.wf-node.tool::before { border-color: var(--orange); background: var(--orange); }
|
|
192
|
+
.wf-node.skill::before { border-color: var(--pink); background: var(--pink); }
|
|
193
|
+
.wf-node .wf-label { font-size: 10px; color: var(--text-dim); text-transform: uppercase; }
|
|
194
|
+
.wf-node .wf-content { font-size: 12px; margin-top: 2px; }
|
|
195
|
+
.wf-node .wf-time { font-size: 10px; color: var(--text-faint); }
|
|
196
|
+
|
|
197
|
+
@media (max-width: 768px) {
|
|
198
|
+
.container { flex-direction: column; }
|
|
199
|
+
.session-list { width: 100%; min-width: 100%; max-height: 35vh; }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* Agent Sidebar */
|
|
203
|
+
.agent-sidebar { width: 60px; min-width: 60px; background: var(--surface); border-right: 1px solid var(--border); display: flex; flex-direction: column; align-items: center; padding-top: 8px; overflow: hidden; position: relative; z-index: 20; }
|
|
204
|
+
.agent-btn { width: 44px; height: 44px; border-radius: 8px; border: 2px solid transparent; background: var(--surface2); color: var(--text-dim); font-size: 16px; font-weight: 700; cursor: pointer; display: flex; align-items: center; justify-content: center; margin-bottom: 6px; transition: all 0.15s; position: relative; }
|
|
205
|
+
.agent-btn:hover { border-color: var(--text-dim); color: var(--text); }
|
|
206
|
+
.agent-btn.active { border-color: var(--accent); color: var(--accent); background: #58a6ff15; }
|
|
207
|
+
.agent-btn .agent-dot { position: absolute; top: 3px; right: 3px; width: 7px; height: 7px; border-radius: 50%; }
|
|
208
|
+
.agent-btn .agent-dot.has-active { background: var(--green); box-shadow: 0 0 4px var(--green); }
|
|
209
|
+
.sidebar-label { font-size: 8px; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
|
|
210
|
+
.agent-btn[title]:hover::after { content: attr(title); position: absolute; left: 52px; top: 50%; transform: translateY(-50%); background: var(--surface3); border: 1px solid var(--border); color: var(--text); padding: 4px 10px; border-radius: 4px; font-size: 11px; font-weight: 400; white-space: nowrap; z-index: 100; pointer-events: none; }
|
|
211
|
+
|
|
212
|
+
/* Main layout with sidebar */
|
|
213
|
+
.app-layout { display: flex; height: calc(100vh - 46px); }
|
|
214
|
+
.app-main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
215
|
+
</style>
|
|
216
|
+
</head>
|
|
217
|
+
<body>
|
|
218
|
+
<header>
|
|
219
|
+
<h1 id="headerTitle">AI Agent Management Dashboard</h1>
|
|
220
|
+
<div class="header-right">
|
|
221
|
+
<div class="header-stats" id="headerStats">
|
|
222
|
+
Active: <span id="activeCount">0</span> | Historical: <span id="totalCount">0</span> | Agents: <span id="agentCount">0</span>
|
|
223
|
+
</div>
|
|
224
|
+
<div id="connStatus" class="conn-status err"><span class="status-dot dead"></span> ...</div>
|
|
225
|
+
</div>
|
|
226
|
+
</header>
|
|
227
|
+
<div class="app-layout">
|
|
228
|
+
<!-- Agent Sidebar -->
|
|
229
|
+
<div class="agent-sidebar" id="agentSidebar">
|
|
230
|
+
<div class="sidebar-label">Agents</div>
|
|
231
|
+
<div id="agentButtons"></div>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<!-- Main content area -->
|
|
235
|
+
<div class="app-main">
|
|
236
|
+
<div class="top-nav">
|
|
237
|
+
<div class="top-nav-tab active" onclick="switchTopTab('sessions')" id="top-tab-sessions"><span class="nav-icon">▶</span>Sessions</div>
|
|
238
|
+
<div class="top-nav-tab" onclick="switchTopTab('config')" id="top-tab-config"><span class="nav-icon">⚙</span>Settings & Config</div>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<!-- Sessions view (existing layout) -->
|
|
242
|
+
<div id="view-sessions">
|
|
243
|
+
<div class="filter-bar">
|
|
244
|
+
<button class="active" onclick="setFilter('all')">All</button>
|
|
245
|
+
<button onclick="setFilter('active')">Active Only</button>
|
|
246
|
+
<button onclick="setFilter('historical')">Historical</button>
|
|
247
|
+
<span style="flex:1"></span>
|
|
248
|
+
<span style="font-size:11px;color:var(--text-faint)" id="projectList"></span>
|
|
249
|
+
</div>
|
|
250
|
+
<div class="container">
|
|
251
|
+
<div class="session-list" id="sessionList"><div class="empty-state">Loading...</div></div>
|
|
252
|
+
<div class="detail-panel" id="detailPanel"><div class="empty-state">Select a session to view details</div></div>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<!-- Settings & Config view -->
|
|
257
|
+
<div id="view-config" style="display:none">
|
|
258
|
+
<div class="settings-page" id="configPage">
|
|
259
|
+
<div style="text-align:center;padding:40px;color:var(--text-dim)">Loading settings...</div>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
</div><!-- end app-main -->
|
|
263
|
+
</div><!-- end app-layout -->
|
|
264
|
+
|
|
265
|
+
<script>
|
|
266
|
+
let sessions = [], selectedId = null, ws = null, filter = 'all', activeTab = 'overview';
|
|
267
|
+
let suppressDetailRefresh = false; // prevent WS refresh from destroying interactive UI state
|
|
268
|
+
const statefulTabs = new Set(['compound', 'cron', 'activity']); // tabs with interactive state that shouldn't auto-refresh
|
|
269
|
+
let globalCronsCache = null; // cache global crons to avoid re-fetch flicker
|
|
270
|
+
let eventFilter = 'substantive'; // 'all' | 'user' | 'substantive' | 'agents'
|
|
271
|
+
let eventOrder = 'oldest'; // 'oldest' | 'newest'
|
|
272
|
+
let eventPage = 0;
|
|
273
|
+
let activityView = 'table'; // 'timeline' | 'table' — default to curated event log
|
|
274
|
+
const EVENTS_PER_PAGE = 50;
|
|
275
|
+
let currentTopTab = 'sessions'; // 'sessions' | 'config'
|
|
276
|
+
let configCache = null; // cached /api/projects data
|
|
277
|
+
let currentAgent = 'claude-code'; // 'claude-code' | 'github-copilot'
|
|
278
|
+
let agentsList = []; // from /api/agents
|
|
279
|
+
let copilotSessions = []; // cached copilot sessions
|
|
280
|
+
let copilotSettingsCache = null; // cached copilot settings
|
|
281
|
+
|
|
282
|
+
// ── Agent Sidebar ──────────────────────────────────────
|
|
283
|
+
async function loadAgents() {
|
|
284
|
+
try {
|
|
285
|
+
const res = await fetch('/api/agents');
|
|
286
|
+
agentsList = await res.json();
|
|
287
|
+
renderAgentSidebar();
|
|
288
|
+
} catch {}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function renderAgentSidebar() {
|
|
292
|
+
const container = document.getElementById('agentButtons');
|
|
293
|
+
container.innerHTML = agentsList.map(a => {
|
|
294
|
+
const isActive = a.id === currentAgent;
|
|
295
|
+
const hasDot = a.activeSessions > 0;
|
|
296
|
+
return '<button class="agent-btn' + (isActive ? ' active' : '') + '" title="' + esc(a.name) + ' (' + a.totalSessions + ' sessions)" onclick="switchAgent(\'' + a.id + '\')" style="' + (isActive ? 'border-color:' + a.color + ';color:' + a.color : '') + '">' +
|
|
297
|
+
a.icon +
|
|
298
|
+
(hasDot ? '<span class="agent-dot has-active"></span>' : '') +
|
|
299
|
+
'</button>';
|
|
300
|
+
}).join('');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function switchAgent(agentId) {
|
|
304
|
+
if (agentId === currentAgent) return;
|
|
305
|
+
currentAgent = agentId;
|
|
306
|
+
selectedId = null;
|
|
307
|
+
configCache = null;
|
|
308
|
+
copilotSettingsCache = null;
|
|
309
|
+
renderAgentSidebar();
|
|
310
|
+
// Reload data for new agent
|
|
311
|
+
if (agentId === 'claude-code') {
|
|
312
|
+
document.title = 'AI Dashboard — Claude Code';
|
|
313
|
+
loadClaudeData();
|
|
314
|
+
} else if (agentId === 'github-copilot') {
|
|
315
|
+
document.title = 'AI Dashboard — GitHub Copilot';
|
|
316
|
+
loadCopilotData();
|
|
317
|
+
}
|
|
318
|
+
// Reset to sessions tab
|
|
319
|
+
switchTopTab('sessions');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
async function loadClaudeData() {
|
|
324
|
+
try {
|
|
325
|
+
const res = await fetch('/api/sessions');
|
|
326
|
+
sessions = await res.json();
|
|
327
|
+
refresh();
|
|
328
|
+
if (sessions.length > 0) {
|
|
329
|
+
const active = sessions.find(s => s.alive);
|
|
330
|
+
selectSession((active || sessions[0]).sessionId);
|
|
331
|
+
}
|
|
332
|
+
} catch {}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function loadCopilotData() {
|
|
336
|
+
try {
|
|
337
|
+
const res = await fetch('/api/copilot/sessions');
|
|
338
|
+
copilotSessions = await res.json();
|
|
339
|
+
sessions = copilotSessions; // reuse sessions array for rendering
|
|
340
|
+
refresh();
|
|
341
|
+
if (sessions.length > 0) selectSession(sessions[0].sessionId);
|
|
342
|
+
} catch {}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function switchTopTab(tab) {
|
|
346
|
+
currentTopTab = tab;
|
|
347
|
+
document.getElementById('top-tab-sessions').classList.toggle('active', tab === 'sessions');
|
|
348
|
+
document.getElementById('top-tab-config').classList.toggle('active', tab === 'config');
|
|
349
|
+
document.getElementById('view-sessions').style.display = tab === 'sessions' ? '' : 'none';
|
|
350
|
+
document.getElementById('view-config').style.display = tab === 'config' ? '' : 'none';
|
|
351
|
+
if (tab === 'config') {
|
|
352
|
+
if (currentAgent === 'claude-code') {
|
|
353
|
+
if (!configCache) loadConfigPage();
|
|
354
|
+
else renderConfigPage();
|
|
355
|
+
} else if (currentAgent === 'github-copilot') {
|
|
356
|
+
if (!copilotSettingsCache) loadCopilotConfigPage();
|
|
357
|
+
else renderCopilotConfigPage();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function loadConfigPage() {
|
|
363
|
+
try {
|
|
364
|
+
const res = await fetch('/api/projects');
|
|
365
|
+
configCache = await res.json();
|
|
366
|
+
renderConfigPage();
|
|
367
|
+
} catch (e) {
|
|
368
|
+
document.getElementById('configPage').innerHTML = '<div style="padding:40px;color:var(--red)">Failed to load: ' + esc(e.message) + '</div>';
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function renderConfigPage() {
|
|
373
|
+
if (!configCache) return;
|
|
374
|
+
const g = configCache.global;
|
|
375
|
+
const projects = configCache.projects || [];
|
|
376
|
+
|
|
377
|
+
let html = '';
|
|
378
|
+
|
|
379
|
+
// ─── Global Settings ───────────────────────────
|
|
380
|
+
html += '<div class="settings-page-section"><h2 style="color:var(--purple)">⚙ Global Settings</h2>';
|
|
381
|
+
|
|
382
|
+
// Global settings card
|
|
383
|
+
html += '<div class="settings-section"><div class="settings-header" onclick="toggleSettingsSection(\'cfg-global-settings\')"><h3 style="color:var(--purple)">Settings (permissions, hooks, env)</h3><span id="toggle-cfg-global-settings" style="color:var(--text-dim)">▶</span></div>';
|
|
384
|
+
html += '<div id="cfg-global-settings" style="display:none" class="settings-body">';
|
|
385
|
+
html += '<div style="font-size:11px;color:var(--text-dim);margin-bottom:8px">' + vscodeLink(g.settingsPath) + '</div>';
|
|
386
|
+
html += renderGlobalSettingsFromData(g.settings);
|
|
387
|
+
html += '</div></div>';
|
|
388
|
+
|
|
389
|
+
// Global MCP Servers
|
|
390
|
+
const globalMcpCount = Object.keys(g.mcpServers || {}).length;
|
|
391
|
+
html += '<div class="settings-section"><div class="settings-header" onclick="toggleSettingsSection(\'cfg-global-mcp\')" style="border-color:var(--pink)44"><h3 style="color:var(--pink)">MCP Servers (' + globalMcpCount + ')</h3><span id="toggle-cfg-global-mcp" style="color:var(--text-dim)">▶</span></div>';
|
|
392
|
+
html += '<div id="cfg-global-mcp" style="display:none" class="settings-body">';
|
|
393
|
+
if (globalMcpCount > 0) {
|
|
394
|
+
html += '<div style="font-size:10px;margin-bottom:8px">' + vscodeLink(g.mcpServersPath) + '</div>';
|
|
395
|
+
html += Object.entries(g.mcpServers).map(([name, config]) => renderMcpServerCard(name, config, 'global')).join('');
|
|
396
|
+
} else {
|
|
397
|
+
html += '<div style="color:var(--text-dim);font-size:12px">No global MCP servers configured</div>';
|
|
398
|
+
}
|
|
399
|
+
html += '</div></div>';
|
|
400
|
+
|
|
401
|
+
// Global CLAUDE.md
|
|
402
|
+
html += '<div class="settings-section"><div class="settings-header" onclick="toggleSettingsSection(\'cfg-global-claudemd\')"><h3 style="color:var(--orange)">CLAUDE.md Instructions</h3><span id="toggle-cfg-global-claudemd" style="color:var(--text-dim)">▶</span></div>';
|
|
403
|
+
html += '<div id="cfg-global-claudemd" style="display:none" class="settings-body">';
|
|
404
|
+
if (g.claudeMd) {
|
|
405
|
+
html += '<div style="font-size:12px;font-weight:600;color:var(--orange);margin-bottom:4px">Global CLAUDE.md <span style="color:var(--text-dim);font-weight:400">(' + g.claudeMdLines + ' lines)</span></div>';
|
|
406
|
+
html += '<div style="font-size:10px;margin-bottom:6px">' + vscodeLink(g.claudeMdPath) + '</div>';
|
|
407
|
+
html += '<div class="settings-code" style="max-height:400px">' + esc(g.claudeMd) + '</div>';
|
|
408
|
+
}
|
|
409
|
+
if (g.claudeLocalMd) {
|
|
410
|
+
html += '<div style="margin-top:12px"><div style="font-size:12px;font-weight:600;color:var(--orange);margin-bottom:4px">CLAUDE.local.md <span style="color:var(--text-dim);font-weight:400">(' + g.claudeLocalMdLines + ' lines)</span></div>';
|
|
411
|
+
html += '<div style="font-size:10px;margin-bottom:6px">' + vscodeLink(g.claudeLocalMdPath) + '</div>';
|
|
412
|
+
html += '<div class="settings-code" style="max-height:300px">' + esc(g.claudeLocalMd) + '</div></div>';
|
|
413
|
+
}
|
|
414
|
+
if (!g.claudeMd && !g.claudeLocalMd) html += '<div style="color:var(--text-dim);font-size:12px">No CLAUDE.md files found</div>';
|
|
415
|
+
html += '</div></div>';
|
|
416
|
+
|
|
417
|
+
// Global User Skills (async placeholder)
|
|
418
|
+
html += '<div class="settings-section" id="cc-global-skills-slot"><div class="settings-header"><h3 style="color:var(--purple)">Global User Skills</h3><span style="color:var(--text-dim);font-size:11px">Loading...</span></div></div>';
|
|
419
|
+
|
|
420
|
+
html += '</div>'; // end global section
|
|
421
|
+
|
|
422
|
+
// ─── Built-in Commands (reference) ─────────────
|
|
423
|
+
html += '<div class="settings-page-section" id="cc-builtin-section"><h2>⚡ Built-in Commands</h2>';
|
|
424
|
+
html += '<div style="color:var(--text-dim);font-size:12px">Loading...</div>';
|
|
425
|
+
html += '</div>';
|
|
426
|
+
|
|
427
|
+
// ─── Projects ──────────────────────────────────
|
|
428
|
+
html += '<div class="settings-page-section"><h2 style="color:var(--cyan)">📁 Projects (' + projects.length + ')</h2>';
|
|
429
|
+
if (projects.length === 0) {
|
|
430
|
+
html += '<div style="color:var(--text-dim);font-size:12px">No projects found under ~/.claude/projects/</div>';
|
|
431
|
+
} else {
|
|
432
|
+
html += '<div class="project-grid">';
|
|
433
|
+
projects.forEach((proj, i) => {
|
|
434
|
+
const mcpCount = Object.keys(proj.mcpServers || {}).length;
|
|
435
|
+
const settingsExists = !!proj.settings;
|
|
436
|
+
const hasInstructions = !!proj.claudeMd;
|
|
437
|
+
html += '<div class="project-card">';
|
|
438
|
+
html += '<div class="project-card-header" onclick="toggleProjectCard(' + i + ')">';
|
|
439
|
+
html += '<h3 style="color:var(--cyan)">' + esc(proj.displayName) + '</h3>';
|
|
440
|
+
html += '<span id="toggle-proj-' + i + '" style="color:var(--text-dim)">▶</span>';
|
|
441
|
+
html += '</div>';
|
|
442
|
+
html += '<div style="padding:6px 16px;font-size:11px;color:var(--text-dim);border-bottom:1px solid var(--border)">';
|
|
443
|
+
html += '<div class="project-stats">';
|
|
444
|
+
html += '<span>Memory: <span class="stat-val">' + proj.memoryCount + '</span></span>';
|
|
445
|
+
html += '<span>MCP: <span class="stat-val">' + mcpCount + '</span></span>';
|
|
446
|
+
html += '<span>Settings: <span class="stat-val">' + (settingsExists ? 'Yes' : 'No') + '</span></span>';
|
|
447
|
+
html += '<span>Instructions: <span class="stat-val">' + (hasInstructions ? 'Yes' : 'No') + '</span></span>';
|
|
448
|
+
html += '</div>';
|
|
449
|
+
html += '<div style="font-size:10px;color:var(--text-faint);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + esc(proj.slug) + '">' + esc(proj.slug) + '</div>';
|
|
450
|
+
html += '</div>';
|
|
451
|
+
html += '<div class="project-card-body" id="proj-body-' + i + '">';
|
|
452
|
+
html += renderProjectCardBody(proj);
|
|
453
|
+
html += '</div></div>';
|
|
454
|
+
});
|
|
455
|
+
html += '</div>';
|
|
456
|
+
}
|
|
457
|
+
html += '</div>';
|
|
458
|
+
|
|
459
|
+
document.getElementById('configPage').innerHTML = html;
|
|
460
|
+
|
|
461
|
+
// Async load skills and populate slots
|
|
462
|
+
fetch('/api/claude-code/skills').then(r => r.json()).then(data => {
|
|
463
|
+
// 1. Global User Skills slot
|
|
464
|
+
const globalSlot = document.getElementById('cc-global-skills-slot');
|
|
465
|
+
if (globalSlot) {
|
|
466
|
+
let gh = '<div class="settings-header" onclick="toggleSettingsSection(\'cc-global-skills\')"><h3 style="color:var(--purple)">Global User Skills (' + data.global.length + ')</h3><span id="toggle-cc-global-skills" style="color:var(--text-dim)">▶</span></div>';
|
|
467
|
+
gh += '<div id="cc-global-skills" style="display:none" class="settings-body">';
|
|
468
|
+
if (data.global.length > 0) {
|
|
469
|
+
gh += '<div style="font-size:10px;margin-bottom:8px">' + vscodeLink(data.globalDir) + '</div>';
|
|
470
|
+
gh += '<div style="display:flex;flex-wrap:wrap;gap:6px">';
|
|
471
|
+
data.global.forEach(cmd => {
|
|
472
|
+
gh += '<div style="display:inline-flex;align-items:center;gap:4px;background:var(--purple)08;border:1px solid var(--purple)33;border-radius:4px;padding:3px 8px" title="' + esc(cmd.description) + '"><code style="font-size:11px;color:var(--purple);font-weight:600">' + esc(cmd.name) + '</code></div>';
|
|
473
|
+
});
|
|
474
|
+
gh += '</div>';
|
|
475
|
+
} else {
|
|
476
|
+
gh += '<div style="color:var(--text-dim);font-size:12px">No global user skills in ~/.claude/commands/</div>';
|
|
477
|
+
}
|
|
478
|
+
gh += '</div>';
|
|
479
|
+
globalSlot.innerHTML = gh;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// 2. Built-in Commands section
|
|
483
|
+
const builtinSlot = document.getElementById('cc-builtin-section');
|
|
484
|
+
if (builtinSlot) {
|
|
485
|
+
let bh = '<h2>⚡ Built-in Commands (' + data.builtin.length + ')</h2>';
|
|
486
|
+
bh += '<div style="display:flex;flex-wrap:wrap;gap:6px">';
|
|
487
|
+
data.builtin.forEach(cmd => {
|
|
488
|
+
bh += '<div style="display:inline-flex;align-items:center;gap:4px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:3px 8px"><code style="font-size:11px;color:var(--accent);font-weight:600">' + esc(cmd.name) + '</code><span style="font-size:10px;color:var(--text-dim)">' + esc(cmd.description) + '</span></div>';
|
|
489
|
+
});
|
|
490
|
+
bh += '</div>';
|
|
491
|
+
builtinSlot.innerHTML = bh;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// 3. Project Skills slots
|
|
495
|
+
(data.projects || []).forEach(proj => {
|
|
496
|
+
const slotId = 'proj-skills-' + proj.project.replace(/[^a-zA-Z0-9]/g, '');
|
|
497
|
+
// Try matching by project name suffix (slug ends with project name)
|
|
498
|
+
const slots = document.querySelectorAll('[id^="proj-skills-"]');
|
|
499
|
+
for (const slot of slots) {
|
|
500
|
+
if (slot.id.endsWith(proj.project.replace(/[^a-zA-Z0-9]/g, ''))) {
|
|
501
|
+
let ph = '<div style="font-size:12px;font-weight:600;color:var(--cyan);margin-bottom:4px">Skills (' + proj.commands.length + ')</div>';
|
|
502
|
+
ph += '<div style="display:flex;flex-wrap:wrap;gap:4px">';
|
|
503
|
+
proj.commands.forEach(cmd => {
|
|
504
|
+
ph += '<div style="display:inline-flex;align-items:center;gap:3px;background:var(--cyan)08;border:1px solid var(--cyan)33;border-radius:4px;padding:2px 6px" title="' + esc(cmd.description) + '"><code style="font-size:10px;color:var(--cyan);font-weight:600">' + esc(cmd.name) + '</code></div>';
|
|
505
|
+
});
|
|
506
|
+
ph += '</div>';
|
|
507
|
+
slot.innerHTML = ph;
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
}).catch(() => {});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function toggleProjectCard(idx) {
|
|
516
|
+
const body = document.getElementById('proj-body-' + idx);
|
|
517
|
+
const toggle = document.getElementById('toggle-proj-' + idx);
|
|
518
|
+
if (!body) return;
|
|
519
|
+
body.classList.toggle('open');
|
|
520
|
+
if (toggle) toggle.innerHTML = body.classList.contains('open') ? '▼' : '▶';
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function renderProjectCardBody(proj) {
|
|
524
|
+
let html = '';
|
|
525
|
+
|
|
526
|
+
// Project settings
|
|
527
|
+
if (proj.settings) {
|
|
528
|
+
html += '<div style="margin-bottom:12px">';
|
|
529
|
+
html += '<div style="font-size:12px;font-weight:600;color:var(--text);margin-bottom:6px">Settings</div>';
|
|
530
|
+
const s = proj.settings;
|
|
531
|
+
if (s.permissions) {
|
|
532
|
+
const p = s.permissions;
|
|
533
|
+
if (p.allow?.length) html += '<div style="font-size:11px;margin-bottom:4px"><span style="color:var(--green);font-weight:600">Allow (' + p.allow.length + '):</span> ' + p.allow.map(r => '<code style="font-size:10px;background:var(--green)11;color:var(--green);padding:1px 4px;border-radius:3px;margin:1px">' + esc(r) + '</code>').join(' ') + '</div>';
|
|
534
|
+
if (p.deny?.length) html += '<div style="font-size:11px;margin-bottom:4px"><span style="color:var(--red);font-weight:600">Deny (' + p.deny.length + '):</span> ' + p.deny.map(r => '<code style="font-size:10px;background:var(--red)11;color:var(--red);padding:1px 4px;border-radius:3px;margin:1px">' + esc(r) + '</code>').join(' ') + '</div>';
|
|
535
|
+
if (p.additionalDirectories?.length) html += '<div style="font-size:11px;margin-top:4px"><span style="color:var(--text);font-weight:600">Additional Dirs:</span><br>' + p.additionalDirectories.map(d => '<code style="font-size:10px;color:var(--text-dim)">' + esc(d) + '</code>').join('<br>') + '</div>';
|
|
536
|
+
}
|
|
537
|
+
html += '</div>';
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Project MCP servers
|
|
541
|
+
const mcpEntries = Object.entries(proj.mcpServers || {});
|
|
542
|
+
if (mcpEntries.length > 0) {
|
|
543
|
+
html += '<div style="margin-bottom:12px">';
|
|
544
|
+
html += '<div style="font-size:12px;font-weight:600;color:var(--pink);margin-bottom:6px">MCP Servers (' + mcpEntries.length + ')</div>';
|
|
545
|
+
html += mcpEntries.map(([name, config]) => renderMcpServerCard(name, config, 'project')).join('');
|
|
546
|
+
html += '</div>';
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Project CLAUDE.md instructions (collapsible, collapsed by default)
|
|
550
|
+
if (proj.claudeMd) {
|
|
551
|
+
const instrId = 'proj-instr-' + proj.slug.replace(/[^a-zA-Z0-9]/g, '');
|
|
552
|
+
html += '<div style="margin-bottom:12px">';
|
|
553
|
+
html += '<div style="display:flex;align-items:center;gap:8px;cursor:pointer" onclick="var el=document.getElementById(\'' + instrId + '\');var t=document.getElementById(\'t-' + instrId + '\');el.style.display=el.style.display===\'none\'?\'block\':\'none\';t.innerHTML=el.style.display===\'none\'?\'▶\':\'▼\'">';
|
|
554
|
+
html += '<span id="t-' + instrId + '" style="color:var(--text-dim);font-size:10px">▶</span>';
|
|
555
|
+
html += '<span style="font-size:12px;font-weight:600;color:var(--orange)">CLAUDE.md <span style="color:var(--text-dim);font-weight:400">(' + proj.claudeMdLines + ' lines)</span></span>';
|
|
556
|
+
html += '</div>';
|
|
557
|
+
html += '<div style="font-size:10px;margin:4px 0 6px 18px">' + vscodeLink(proj.claudeMdPath) + '</div>';
|
|
558
|
+
html += '<div id="' + instrId + '" style="display:none;margin-left:18px"><div class="settings-code" style="max-height:300px">' + esc(proj.claudeMd.length > 3000 ? proj.claudeMd.slice(0, 3000) + '\n... (truncated)' : proj.claudeMd) + '</div></div>';
|
|
559
|
+
html += '</div>';
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Memory
|
|
563
|
+
const memories = (proj.memories || []).filter(m => !m.isIndex);
|
|
564
|
+
const index = (proj.memories || []).find(m => m.isIndex);
|
|
565
|
+
if (memories.length > 0) {
|
|
566
|
+
html += '<div style="margin-bottom:12px">';
|
|
567
|
+
html += '<div style="font-size:12px;font-weight:600;color:var(--green);margin-bottom:6px">Memory (' + memories.length + ' files)</div>';
|
|
568
|
+
memories.forEach((m, mi) => {
|
|
569
|
+
const uid = proj.slug.replace(/[^a-zA-Z0-9]/g, '') + mi;
|
|
570
|
+
html += '<div class="memory-card">';
|
|
571
|
+
html += '<div class="mem-header"><div><span style="font-size:12px;font-weight:600">' + esc(m.name) + '</span> <span class="mem-type ' + m.type + '" style="margin-left:6px">' + esc(m.type) + '</span></div>';
|
|
572
|
+
html += '<button style="background:none;border:1px solid var(--border);color:var(--text-dim);padding:2px 8px;border-radius:3px;cursor:pointer;font-size:10px" onclick="document.getElementById(\'cfg-mem-' + uid + '\').style.display=document.getElementById(\'cfg-mem-' + uid + '\').style.display===\'none\'?\'block\':\'none\'">view</button>';
|
|
573
|
+
html += '</div>';
|
|
574
|
+
if (m.description) html += '<div style="font-size:11px;color:var(--text-dim);margin-top:4px">' + esc(m.description) + '</div>';
|
|
575
|
+
html += '<div id="cfg-mem-' + uid + '" style="display:none;margin-top:8px"><div class="settings-code" style="max-height:200px;font-size:11px">' + esc(m.body) + '</div></div>';
|
|
576
|
+
html += '</div>';
|
|
577
|
+
});
|
|
578
|
+
if (index) {
|
|
579
|
+
html += '<div style="margin-top:8px"><div style="font-size:11px;font-weight:600;color:var(--text-dim);margin-bottom:4px">MEMORY.md Index</div>';
|
|
580
|
+
html += '<div style="font-size:10px;margin-bottom:4px">' + vscodeLink(index.path) + '</div>';
|
|
581
|
+
html += '<div class="settings-code" style="max-height:150px;font-size:10px">' + esc(index.content) + '</div></div>';
|
|
582
|
+
}
|
|
583
|
+
html += '</div>';
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Project Skills (async placeholder, filled after skills API loads)
|
|
587
|
+
const skillSlotId = 'proj-skills-' + proj.slug.replace(/[^a-zA-Z0-9]/g, '');
|
|
588
|
+
html += '<div id="' + skillSlotId + '" style="margin-bottom:12px"></div>';
|
|
589
|
+
|
|
590
|
+
if (!proj.settings && mcpEntries.length === 0 && memories.length === 0 && !proj.claudeMd) {
|
|
591
|
+
html += '<div style="color:var(--text-dim);font-size:12px;padding:8px 0">No settings, MCP servers, or memory files for this project.</div>';
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return html;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function renderGlobalSettingsFromData(s) {
|
|
598
|
+
if (!s) return '<div style="color:var(--text-dim);font-size:12px">No global settings found</div>';
|
|
599
|
+
const sections = [];
|
|
600
|
+
if (s.permissions) {
|
|
601
|
+
const p = s.permissions;
|
|
602
|
+
sections.push('<div style="margin-bottom:12px">' +
|
|
603
|
+
'<div style="font-size:12px;font-weight:600;color:var(--text);margin-bottom:6px">Permission Mode: <span style="color:var(--accent)">' + esc(p.defaultMode || 'ask') + '</span></div>' +
|
|
604
|
+
(p.allow?.length ? '<div style="font-size:11px;margin-bottom:4px"><span style="color:var(--green);font-weight:600">Allow (' + p.allow.length + '):</span> ' + p.allow.map(r => '<code style="font-size:10px;background:var(--green)11;color:var(--green);padding:1px 4px;border-radius:3px;margin:1px">' + esc(r) + '</code>').join(' ') + '</div>' : '') +
|
|
605
|
+
(p.deny?.length ? '<div style="font-size:11px;margin-bottom:4px"><span style="color:var(--red);font-weight:600">Deny (' + p.deny.length + '):</span> ' + p.deny.map(r => '<code style="font-size:10px;background:var(--red)11;color:var(--red);padding:1px 4px;border-radius:3px;margin:1px">' + esc(r) + '</code>').join(' ') + '</div>' : '') +
|
|
606
|
+
(p.ask?.length ? '<div style="font-size:11px"><span style="color:var(--orange);font-weight:600">Ask (' + p.ask.length + '):</span> <span style="color:var(--text-dim);cursor:pointer" onclick="this.nextElementSibling.style.display=this.nextElementSibling.style.display===\'none\'?\'inline\':\'none\';this.style.display=\'none\'">click to expand...</span><span style="display:none">' + p.ask.map(r => '<code style="font-size:10px;background:var(--orange)11;color:var(--orange);padding:1px 4px;border-radius:3px;margin:1px">' + esc(r) + '</code>').join(' ') + '</span></div>' : '') +
|
|
607
|
+
'</div>');
|
|
608
|
+
}
|
|
609
|
+
if (s.env) {
|
|
610
|
+
sections.push('<div style="margin-bottom:12px"><div style="font-size:12px;font-weight:600;color:var(--text);margin-bottom:6px">Environment Variables</div>' +
|
|
611
|
+
Object.entries(s.env).map(([k,v]) => '<div style="font-size:11px"><code style="color:var(--cyan)">' + esc(k) + '</code> = <code style="color:var(--text-dim)">' + esc(String(v)) + '</code></div>').join('') + '</div>');
|
|
612
|
+
}
|
|
613
|
+
if (s.hooks) {
|
|
614
|
+
sections.push('<div><div style="font-size:12px;font-weight:600;color:var(--text);margin-bottom:6px">Hooks</div>' +
|
|
615
|
+
Object.entries(s.hooks).map(([event, hooks]) => '<div style="font-size:11px;margin-bottom:4px"><span style="color:var(--pink);font-weight:600">' + esc(event) + ':</span> ' +
|
|
616
|
+
(Array.isArray(hooks) ? hooks : [hooks]).map(h => {
|
|
617
|
+
const cmds = h.hooks || [h];
|
|
618
|
+
return cmds.map(c => '<code style="font-size:10px;background:var(--pink)11;color:var(--pink);padding:1px 4px;border-radius:3px">' + esc(c.command || JSON.stringify(c)) + '</code>').join(' ');
|
|
619
|
+
}).join(' ') + '</div>').join('') + '</div>');
|
|
620
|
+
}
|
|
621
|
+
return sections.join('') || '<div class="settings-code">' + esc(JSON.stringify(s, null, 2)) + '</div>';
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ── Copilot Config Page ────────────────────────────────
|
|
625
|
+
async function loadCopilotConfigPage() {
|
|
626
|
+
try {
|
|
627
|
+
const res = await fetch('/api/copilot/settings');
|
|
628
|
+
copilotSettingsCache = await res.json();
|
|
629
|
+
renderCopilotConfigPage();
|
|
630
|
+
} catch (e) {
|
|
631
|
+
document.getElementById('configPage').innerHTML = '<div style="padding:40px;color:var(--red)">Failed to load: ' + esc(e.message) + '</div>';
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function renderCopilotConfigPage() {
|
|
636
|
+
if (!copilotSettingsCache) return;
|
|
637
|
+
const s = copilotSettingsCache;
|
|
638
|
+
let html = '';
|
|
639
|
+
|
|
640
|
+
// MCP Servers
|
|
641
|
+
const mcpCount = Object.keys(s.mcpServers || {}).length;
|
|
642
|
+
html += '<div class="settings-page-section"><h2 style="color:var(--green)">⚙ GitHub Copilot Settings</h2>';
|
|
643
|
+
|
|
644
|
+
html += '<div class="settings-section"><div class="settings-header" onclick="toggleSettingsSection(\'cop-mcp\')" style="border-color:var(--pink)44"><h3 style="color:var(--pink)">MCP Servers (' + mcpCount + ')</h3><span id="toggle-cop-mcp" style="color:var(--text-dim)">▶</span></div>';
|
|
645
|
+
html += '<div id="cop-mcp" style="display:none" class="settings-body">';
|
|
646
|
+
if (mcpCount > 0) {
|
|
647
|
+
html += '<div style="font-size:10px;margin-bottom:8px">' + vscodeLink(s.mcpConfigPath) + '</div>';
|
|
648
|
+
html += Object.entries(s.mcpServers).map(([name, config]) => renderMcpServerCard(name, config, 'global')).join('');
|
|
649
|
+
} else {
|
|
650
|
+
html += '<div style="color:var(--text-dim);font-size:12px">No MCP servers configured</div>';
|
|
651
|
+
}
|
|
652
|
+
html += '</div></div>';
|
|
653
|
+
|
|
654
|
+
// Instructions (per-project)
|
|
655
|
+
html += '<div class="settings-section"><div class="settings-header" onclick="toggleSettingsSection(\'cop-instr\')"><h3 style="color:var(--orange)">Copilot Instructions (' + (s.projectInstructions || []).length + ' projects)</h3><span id="toggle-cop-instr" style="color:var(--text-dim)">▶</span></div>';
|
|
656
|
+
html += '<div id="cop-instr" style="display:none" class="settings-body">';
|
|
657
|
+
if ((s.projectInstructions || []).length > 0) {
|
|
658
|
+
s.projectInstructions.forEach(inst => {
|
|
659
|
+
html += '<div style="margin-bottom:12px"><div style="font-size:12px;font-weight:600;color:var(--orange);margin-bottom:4px">' + esc(inst.project) + ' <span style="color:var(--text-dim);font-weight:400">(' + inst.lines + ' lines)</span></div>';
|
|
660
|
+
html += '<div style="font-size:10px;margin-bottom:6px">' + vscodeLink(inst.path) + '</div>';
|
|
661
|
+
html += '<div class="settings-code" style="max-height:300px">' + esc(inst.content) + '</div></div>';
|
|
662
|
+
});
|
|
663
|
+
} else {
|
|
664
|
+
html += '<div style="color:var(--text-dim);font-size:12px">No copilot-instructions.md found in any project</div>';
|
|
665
|
+
}
|
|
666
|
+
html += '</div></div>';
|
|
667
|
+
|
|
668
|
+
// Custom Agents
|
|
669
|
+
html += '<div class="settings-section"><div class="settings-header" onclick="toggleSettingsSection(\'cop-agents\')"><h3 style="color:var(--cyan)">Custom Agents (' + (s.agents || []).length + ')</h3><span id="toggle-cop-agents" style="color:var(--text-dim)">▶</span></div>';
|
|
670
|
+
html += '<div id="cop-agents" style="display:none" class="settings-body">';
|
|
671
|
+
if (s.agents && s.agents.length > 0) {
|
|
672
|
+
html += '<div style="font-size:10px;margin-bottom:8px">' + vscodeLink(s.agentsDir) + '</div>';
|
|
673
|
+
s.agents.forEach((a, i) => {
|
|
674
|
+
html += '<div class="agent-card" style="flex-direction:column;align-items:stretch"><div style="display:flex;align-items:center;gap:10px">';
|
|
675
|
+
html += '<div class="agent-icon" style="background:var(--cyan)22;color:var(--cyan)">A</div>';
|
|
676
|
+
html += '<div style="flex:1"><div style="font-size:13px;font-weight:600">' + esc(a.name) + '</div>';
|
|
677
|
+
html += '<div style="font-size:11px;color:var(--text-dim)">' + esc(a.description) + '</div></div>';
|
|
678
|
+
html += '<div style="font-size:10px;color:var(--text-faint)">' + esc(a.file) + '</div>';
|
|
679
|
+
html += '</div></div>';
|
|
680
|
+
});
|
|
681
|
+
} else {
|
|
682
|
+
html += '<div style="color:var(--text-dim);font-size:12px">No custom agents</div>';
|
|
683
|
+
}
|
|
684
|
+
html += '</div></div>';
|
|
685
|
+
|
|
686
|
+
// Reusable Prompts (grouped by project)
|
|
687
|
+
html += '<div class="settings-section"><div class="settings-header" onclick="toggleSettingsSection(\'cop-prompts\')"><h3 style="color:var(--purple)">Reusable Prompts (' + (s.prompts || []).length + ')</h3><span id="toggle-cop-prompts" style="color:var(--text-dim)">▶</span></div>';
|
|
688
|
+
html += '<div id="cop-prompts" style="display:none" class="settings-body">';
|
|
689
|
+
if (s.prompts && s.prompts.length > 0) {
|
|
690
|
+
// Group prompts by project
|
|
691
|
+
const byProject = {};
|
|
692
|
+
s.prompts.forEach(p => {
|
|
693
|
+
const proj = p.project || 'unknown';
|
|
694
|
+
if (!byProject[proj]) byProject[proj] = [];
|
|
695
|
+
byProject[proj].push(p);
|
|
696
|
+
});
|
|
697
|
+
Object.entries(byProject).forEach(([proj, prompts]) => {
|
|
698
|
+
html += '<div style="margin-bottom:8px"><div style="font-size:11px;font-weight:600;color:var(--cyan);margin-bottom:4px">' + esc(proj) + ' (' + prompts.length + ')</div>';
|
|
699
|
+
html += '<div style="display:flex;flex-wrap:wrap;gap:6px">';
|
|
700
|
+
prompts.forEach(p => {
|
|
701
|
+
html += '<code style="font-size:10px;background:var(--purple)11;color:var(--purple);padding:2px 8px;border-radius:3px">' + esc(p.name) + '</code>';
|
|
702
|
+
});
|
|
703
|
+
html += '</div></div>';
|
|
704
|
+
});
|
|
705
|
+
} else {
|
|
706
|
+
html += '<div style="color:var(--text-dim);font-size:12px">No reusable prompts found</div>';
|
|
707
|
+
}
|
|
708
|
+
html += '</div></div>';
|
|
709
|
+
|
|
710
|
+
html += '</div>'; // end global settings section
|
|
711
|
+
|
|
712
|
+
// ─── Built-in Commands & Participants ──────────
|
|
713
|
+
html += '<div class="settings-page-section" id="cop-builtin-section"><h2>⚡ Built-in Commands & Participants</h2>';
|
|
714
|
+
html += '<div style="color:var(--text-dim);font-size:12px">Loading...</div>';
|
|
715
|
+
html += '</div>';
|
|
716
|
+
|
|
717
|
+
// Config
|
|
718
|
+
if (s.config) {
|
|
719
|
+
html += '<div class="settings-page-section"><h2>⚙ General Config</h2>';
|
|
720
|
+
html += '<div style="font-size:10px;margin-bottom:8px">' + vscodeLink(s.configPath) + '</div>';
|
|
721
|
+
html += '<div class="settings-code">' + esc(JSON.stringify(s.config, null, 2)) + '</div>';
|
|
722
|
+
html += '</div>';
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
document.getElementById('configPage').innerHTML = html;
|
|
726
|
+
|
|
727
|
+
// Load built-in commands async
|
|
728
|
+
fetch('/api/copilot/skills').then(r => r.json()).then(data => {
|
|
729
|
+
const el = document.getElementById('cop-builtin-section');
|
|
730
|
+
if (!el) return;
|
|
731
|
+
let h = '<h2>⚡ Built-in Commands & Participants</h2>';
|
|
732
|
+
h += '<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px">';
|
|
733
|
+
data.builtinCommands.forEach(cmd => {
|
|
734
|
+
h += '<div style="display:inline-flex;align-items:center;gap:4px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:3px 8px"><code style="font-size:11px;color:var(--accent);font-weight:600">' + esc(cmd.name) + '</code><span style="font-size:10px;color:var(--text-dim)">' + esc(cmd.description) + '</span></div>';
|
|
735
|
+
});
|
|
736
|
+
h += '</div>';
|
|
737
|
+
h += '<div style="font-size:12px;font-weight:600;color:var(--text);margin-bottom:6px">Chat Participants</div>';
|
|
738
|
+
h += '<div style="display:flex;flex-wrap:wrap;gap:6px">';
|
|
739
|
+
data.builtinParticipants.forEach(p => {
|
|
740
|
+
h += '<div style="display:inline-flex;align-items:center;gap:4px;background:var(--green)08;border:1px solid var(--green)33;border-radius:4px;padding:3px 8px"><code style="font-size:11px;color:var(--green);font-weight:600">' + esc(p.name) + '</code><span style="font-size:10px;color:var(--text-dim)">' + esc(p.description) + '</span></div>';
|
|
741
|
+
});
|
|
742
|
+
h += '</div>';
|
|
743
|
+
el.innerHTML = h;
|
|
744
|
+
}).catch(() => {});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function connectWS() {
|
|
748
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
749
|
+
ws = new WebSocket(`${proto}//${location.host}`);
|
|
750
|
+
ws.onopen = () => { setConn(true); };
|
|
751
|
+
ws.onclose = () => { setConn(false); setTimeout(connectWS, 3000); };
|
|
752
|
+
ws.onmessage = (e) => {
|
|
753
|
+
const msg = JSON.parse(e.data);
|
|
754
|
+
if (msg.type === 'sessions' && currentAgent === 'claude-code') { sessions = msg.data; refresh(); }
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function setConn(ok) {
|
|
759
|
+
const el = document.getElementById('connStatus');
|
|
760
|
+
el.innerHTML = ok ? '<span class="status-dot alive"></span> Live' : '<span class="status-dot dead"></span> Reconnecting';
|
|
761
|
+
el.className = ok ? 'conn-status ok' : 'conn-status err';
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function setFilter(f) {
|
|
765
|
+
filter = f;
|
|
766
|
+
document.querySelectorAll('.filter-bar button').forEach(b => b.classList.toggle('active', b.textContent.toLowerCase().includes(f === 'all' ? 'all' : f)));
|
|
767
|
+
renderList();
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function refresh() {
|
|
771
|
+
const active = sessions.filter(s => s.alive).length;
|
|
772
|
+
const totalAgents = sessions.reduce((s, x) => s + (x.agents?.length || 0), 0);
|
|
773
|
+
document.getElementById('activeCount').textContent = active;
|
|
774
|
+
document.getElementById('totalCount').textContent = sessions.length;
|
|
775
|
+
document.getElementById('agentCount').textContent = totalAgents;
|
|
776
|
+
const projects = [...new Set(sessions.map(s => s.project).filter(Boolean))];
|
|
777
|
+
document.getElementById('projectList').textContent = projects.map(p => p.split('--').pop()).join(' | ');
|
|
778
|
+
renderList();
|
|
779
|
+
// Don't re-render detail panel if on a stateful tab or if user is typing in an input
|
|
780
|
+
const inputFocused = document.activeElement && (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA');
|
|
781
|
+
if (selectedId && !statefulTabs.has(activeTab) && !inputFocused) renderDetail(selectedId);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function filtered() {
|
|
785
|
+
if (filter === 'active') return sessions.filter(s => s.alive);
|
|
786
|
+
if (filter === 'historical') return sessions.filter(s => !s.alive);
|
|
787
|
+
return sessions;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function esc(s) { return s ? s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') : ''; }
|
|
791
|
+
function timeAgo(ts) {
|
|
792
|
+
if (!ts) return '';
|
|
793
|
+
const d = typeof ts === 'number' ? ts : new Date(ts).getTime();
|
|
794
|
+
const diff = Date.now() - d;
|
|
795
|
+
if (diff < 60000) return 'now';
|
|
796
|
+
if (diff < 3600000) return Math.floor(diff/60000) + 'm';
|
|
797
|
+
if (diff < 86400000) return Math.floor(diff/3600000) + 'h';
|
|
798
|
+
return Math.floor(diff/86400000) + 'd';
|
|
799
|
+
}
|
|
800
|
+
function fmtTime(ts) {
|
|
801
|
+
if (!ts) return '';
|
|
802
|
+
const d = typeof ts === 'number' ? new Date(ts) : new Date(ts);
|
|
803
|
+
return d.toLocaleTimeString('en-US', {hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
|
804
|
+
}
|
|
805
|
+
function fmtDate(ts) {
|
|
806
|
+
if (!ts) return '';
|
|
807
|
+
const d = typeof ts === 'number' ? new Date(ts) : new Date(ts);
|
|
808
|
+
return d.toLocaleDateString('en-US', {month:'short',day:'numeric'}) + ' ' + d.toLocaleTimeString('en-US', {hour:'2-digit',minute:'2-digit'});
|
|
809
|
+
}
|
|
810
|
+
function shortPath(p) {
|
|
811
|
+
if (!p) return '';
|
|
812
|
+
const parts = p.replace(/\\/g,'/').split('/');
|
|
813
|
+
return parts.length > 2 ? '.../' + parts.slice(-2).join('/') : p;
|
|
814
|
+
}
|
|
815
|
+
function epBadge(ep) {
|
|
816
|
+
if (ep?.includes('vscode')) return '<span class="badge vscode">VSCode</span>';
|
|
817
|
+
if (ep?.includes('cli') || ep === 'interactive') return '<span class="badge cli">CLI</span>';
|
|
818
|
+
if (ep?.includes('web')) return '<span class="badge web">Web</span>';
|
|
819
|
+
return `<span class="badge unknown">${esc(ep||'?')}</span>`;
|
|
820
|
+
}
|
|
821
|
+
function agentIcon(type) {
|
|
822
|
+
if (type?.includes('Explore') || type?.includes('explore')) return 'explore';
|
|
823
|
+
if (type?.includes('guide')) return 'guide';
|
|
824
|
+
return 'general';
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function renderList() {
|
|
828
|
+
const el = document.getElementById('sessionList');
|
|
829
|
+
const list = filtered();
|
|
830
|
+
if (!list.length) { el.innerHTML = '<div class="empty-state">No sessions</div>'; return; }
|
|
831
|
+
el.innerHTML = list.map(s => {
|
|
832
|
+
const agentDots = (s.agents || []).slice(-8).map(a =>
|
|
833
|
+
`<span class="agent-dot ${a.status}" title="${esc(a.description)}"></span>`
|
|
834
|
+
).join('');
|
|
835
|
+
const todoProgress = (s.todos || []).length > 0
|
|
836
|
+
? ` | TODO: ${s.todos.filter(t=>t.status==='completed').length}/${s.todos.length}`
|
|
837
|
+
: '';
|
|
838
|
+
return `
|
|
839
|
+
<div class="session-card ${s.sessionId===selectedId?'selected':''}" onclick="selectSession('${s.sessionId}')">
|
|
840
|
+
<div class="session-title">
|
|
841
|
+
<span class="status-dot ${s.alive?'alive':'dead'}"></span>
|
|
842
|
+
${esc(s.displayName || s.title || s.sessionId.slice(0,8))}
|
|
843
|
+
${s.customName ? '<span style="font-size:9px;color:var(--cyan);margin-left:4px">✎</span>' : ''}
|
|
844
|
+
${!s.alive && !s.hasMetadata ? '<span class="badge historical">hist</span>' : ''}
|
|
845
|
+
</div>
|
|
846
|
+
<div class="session-meta">
|
|
847
|
+
${epBadge(s.entrypoint)}
|
|
848
|
+
<span>${esc(shortPath(s.cwd))}</span>
|
|
849
|
+
<span>${timeAgo(s.startedAt)} ago</span>
|
|
850
|
+
${agentDots ? `<span class="agent-dots">${agentDots}</span>` : ''}
|
|
851
|
+
</div>
|
|
852
|
+
<div class="session-stats">
|
|
853
|
+
<span>Msgs: ${s.totalUserMessages+s.totalAssistantMessages}</span>
|
|
854
|
+
<span>Tools: ${s.totalToolCalls}</span>
|
|
855
|
+
<span>Agents: ${(s.agents||[]).length}</span>
|
|
856
|
+
<span>${Math.round(s.fileSizeKB||0)}KB</span>
|
|
857
|
+
${todoProgress}
|
|
858
|
+
</div>
|
|
859
|
+
</div>`;
|
|
860
|
+
}).join('');
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function selectSession(id) {
|
|
864
|
+
selectedId = id; renderList(); renderDetail(id);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function setTab(tab) { activeTab = tab; if (selectedId) renderDetail(selectedId); }
|
|
868
|
+
|
|
869
|
+
async function renameSession(sessionId, name) {
|
|
870
|
+
try {
|
|
871
|
+
const res = await fetch(`/api/sessions/${sessionId}/name`, {
|
|
872
|
+
method: 'PUT',
|
|
873
|
+
headers: { 'Content-Type': 'application/json' },
|
|
874
|
+
body: JSON.stringify({ name })
|
|
875
|
+
});
|
|
876
|
+
if (res.ok) {
|
|
877
|
+
// Update local session data
|
|
878
|
+
const s = sessions.find(x => x.sessionId === sessionId);
|
|
879
|
+
if (s) {
|
|
880
|
+
s.customName = name.trim() || null;
|
|
881
|
+
s.displayName = s.customName || s.title || s.sessionId.slice(0, 8);
|
|
882
|
+
}
|
|
883
|
+
renderList();
|
|
884
|
+
renderDetail(sessionId);
|
|
885
|
+
}
|
|
886
|
+
} catch (e) {
|
|
887
|
+
console.error('Rename failed:', e);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
async function closeSession(sessionId, pid) {
|
|
892
|
+
if (!confirm('Close this Claude Code session (PID ' + pid + ')? The process will be terminated.')) return;
|
|
893
|
+
const btn = document.getElementById('close-btn-' + sessionId);
|
|
894
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Closing...'; btn.style.opacity = '0.5'; }
|
|
895
|
+
try {
|
|
896
|
+
const res = await fetch('/api/sessions/' + sessionId + '/close', {
|
|
897
|
+
method: 'POST',
|
|
898
|
+
headers: { 'Content-Type': 'application/json' }
|
|
899
|
+
});
|
|
900
|
+
const data = await res.json();
|
|
901
|
+
if (res.ok) {
|
|
902
|
+
// Mark session as dead locally
|
|
903
|
+
const s = sessions.find(x => x.sessionId === sessionId);
|
|
904
|
+
if (s) s.alive = false;
|
|
905
|
+
renderList();
|
|
906
|
+
renderDetail(sessionId);
|
|
907
|
+
} else {
|
|
908
|
+
alert('Close failed: ' + (data.error || 'Unknown error'));
|
|
909
|
+
if (btn) { btn.disabled = false; btn.textContent = 'Close Session (PID ' + pid + ')'; btn.style.opacity = '1'; }
|
|
910
|
+
}
|
|
911
|
+
} catch (e) {
|
|
912
|
+
alert('Close failed: ' + e.message);
|
|
913
|
+
if (btn) { btn.disabled = false; btn.textContent = 'Close Session (PID ' + pid + ')'; btn.style.opacity = '1'; }
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function renderDetail(id) {
|
|
918
|
+
const s = sessions.find(x => x.sessionId === id);
|
|
919
|
+
if (!s) return;
|
|
920
|
+
const el = document.getElementById('detailPanel');
|
|
921
|
+
|
|
922
|
+
el.innerHTML = `
|
|
923
|
+
<div class="tab-bar">
|
|
924
|
+
<div class="tab ${activeTab==='overview'?'active':''}" onclick="setTab('overview')">Overview</div>
|
|
925
|
+
<div class="tab ${activeTab==='agents'?'active':''}" onclick="setTab('agents')">Agents (${(s.agents||[]).length})</div>
|
|
926
|
+
<div class="tab ${activeTab==='activity'?'active':''}" onclick="setTab('activity')">Activity (${(s.recentEvents||[]).length})</div>
|
|
927
|
+
<div class="tab ${activeTab==='compound'?'active':''}" onclick="setTab('compound')" style="color:${activeTab==='compound'?'var(--cyan)':''}">AI Summary</div>
|
|
928
|
+
<div class="tab ${activeTab==='cron'?'active':''}" onclick="setTab('cron')">Cron (${(s.cronJobs||[]).length})</div>
|
|
929
|
+
</div>
|
|
930
|
+
<div class="tab-content">
|
|
931
|
+
${activeTab==='overview' ? renderOverview(s) : ''}
|
|
932
|
+
${activeTab==='agents' ? renderAgents(s) : ''}
|
|
933
|
+
${activeTab==='activity' ? renderActivity(s) : ''}
|
|
934
|
+
${activeTab==='compound' ? renderCompound(s) : ''}
|
|
935
|
+
${activeTab==='cron' ? renderCron(s) : ''}
|
|
936
|
+
</div>
|
|
937
|
+
`;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function renderOverview(s) {
|
|
941
|
+
const nameSection = `
|
|
942
|
+
<div class="workflow-section">
|
|
943
|
+
<h3>Session Name</h3>
|
|
944
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
|
|
945
|
+
<input id="rename-input" type="text" value="${esc(s.customName || s.title || '')}"
|
|
946
|
+
placeholder="${esc(s.title || s.sessionId.slice(0,8))}"
|
|
947
|
+
style="flex:1;background:var(--surface);border:1px solid var(--border);border-radius:4px;padding:6px 10px;color:var(--text);font-size:13px;outline:none"
|
|
948
|
+
onfocus="this.style.borderColor='var(--accent)'" onblur="this.style.borderColor='var(--border)'"
|
|
949
|
+
onkeydown="if(event.key==='Enter')renameSession('${s.sessionId}',this.value)">
|
|
950
|
+
<button onclick="renameSession('${s.sessionId}',document.getElementById('rename-input').value)"
|
|
951
|
+
style="background:var(--accent)22;color:var(--accent);border:1px solid var(--accent)44;padding:6px 12px;border-radius:4px;cursor:pointer;font-size:12px">Rename</button>
|
|
952
|
+
${s.customName ? `<button onclick="renameSession('${s.sessionId}','')"
|
|
953
|
+
style="background:var(--surface2);color:var(--text-dim);border:1px solid var(--border);padding:6px 12px;border-radius:4px;cursor:pointer;font-size:12px">Reset</button>` : ''}
|
|
954
|
+
</div>
|
|
955
|
+
${s.suggestedName ? `
|
|
956
|
+
<div style="font-size:11px;color:var(--text-dim);margin-bottom:4px">Suggested name based on content:</div>
|
|
957
|
+
<div style="font-size:12px;color:var(--text-faint);background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:8px;cursor:pointer;max-height:60px;overflow:hidden"
|
|
958
|
+
onclick="document.getElementById('rename-input').value=this.textContent.trim()" title="Click to use this name">
|
|
959
|
+
${esc(s.suggestedName.slice(0, 200))}
|
|
960
|
+
</div>
|
|
961
|
+
` : ''}
|
|
962
|
+
</div>`;
|
|
963
|
+
|
|
964
|
+
const todoHtml = (s.todos || []).length > 0
|
|
965
|
+
? `<div class="workflow-section">
|
|
966
|
+
<h3>Current Tasks (TODO)</h3>
|
|
967
|
+
<div style="background:var(--surface);border:1px solid var(--border);border-radius:6px;overflow:hidden">
|
|
968
|
+
${s.todos.map(t => `
|
|
969
|
+
<div class="todo-item">
|
|
970
|
+
<div class="todo-check ${t.status}">${t.status==='completed'?'✓':t.status==='in_progress'?'▶':''}</div>
|
|
971
|
+
<div class="todo-text ${t.status}">${esc(t.status==='in_progress'?t.activeForm:t.content)}</div>
|
|
972
|
+
</div>
|
|
973
|
+
`).join('')}
|
|
974
|
+
</div>
|
|
975
|
+
</div>` : '';
|
|
976
|
+
|
|
977
|
+
// Compact AI Summary card for Overview (uses cached or server-provided data)
|
|
978
|
+
const aiData = window['ai-summary-' + s.sessionId] || s.aiSummary;
|
|
979
|
+
if (aiData && !window['ai-summary-' + s.sessionId]) window['ai-summary-' + s.sessionId] = aiData;
|
|
980
|
+
const aiCardHtml = aiData
|
|
981
|
+
? `<div class="workflow-section">
|
|
982
|
+
<h3 style="color:var(--cyan)">AI Summary</h3>
|
|
983
|
+
<div style="background:var(--surface);border:1px solid var(--cyan)33;border-radius:6px;padding:12px">
|
|
984
|
+
<div style="font-weight:600;font-size:13px;margin-bottom:6px">${esc(aiData.briefName || aiData.summary?.split(/[.\n]/)[0]?.slice(0,60) || '')}</div>
|
|
985
|
+
<div style="font-size:12px;color:var(--text-dim);line-height:1.6;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden">${esc(aiData.summary?.slice(0,200) || '')}</div>
|
|
986
|
+
<div style="margin-top:8px;text-align:right"><span style="font-size:11px;color:var(--accent);cursor:pointer" onclick="setTab('compound')">View full summary →</span></div>
|
|
987
|
+
</div>
|
|
988
|
+
</div>`
|
|
989
|
+
: `<div class="workflow-section">
|
|
990
|
+
<h3 style="color:var(--cyan)">AI Summary</h3>
|
|
991
|
+
<div style="background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:12px;display:flex;align-items:center;justify-content:space-between">
|
|
992
|
+
<span style="font-size:12px;color:var(--text-dim)">No AI summary generated yet</span>
|
|
993
|
+
<button onclick="setTab('compound')" style="background:var(--cyan)22;color:var(--cyan);border:1px solid var(--cyan)44;padding:4px 12px;border-radius:4px;cursor:pointer;font-size:11px;font-weight:600">Generate →</button>
|
|
994
|
+
</div>
|
|
995
|
+
</div>`;
|
|
996
|
+
|
|
997
|
+
return `
|
|
998
|
+
<div class="info-grid">
|
|
999
|
+
<div class="info-card"><div class="label">Status</div><div class="value" style="color:${s.alive?'var(--green)':'var(--text-dim)'}">${s.alive?'Active':'Ended'}${s.alive?` <span style="font-size:10px;color:var(--text-faint)">(PID ${s.pid})</span>`:''}</div></div>
|
|
1000
|
+
<div class="info-card"><div class="label">Messages</div><div class="value">${s.totalUserMessages+s.totalAssistantMessages}</div></div>
|
|
1001
|
+
<div class="info-card"><div class="label">Tool Calls</div><div class="value">${s.totalToolCalls}</div></div>
|
|
1002
|
+
<div class="info-card"><div class="label">Agents</div><div class="value">${(s.agents||[]).length}</div></div>
|
|
1003
|
+
<div class="info-card"><div class="label">Started</div><div class="value small">${fmtDate(s.startedAt)}</div></div>
|
|
1004
|
+
<div class="info-card"><div class="label">Last Activity</div><div class="value small">${s.lastActivity?timeAgo(s.lastActivity)+' ago':'N/A'}</div></div>
|
|
1005
|
+
<div class="info-card"><div class="label">Project</div><div class="value small">${esc(s.project?.split('--').pop()||'?')}</div></div>
|
|
1006
|
+
<div class="info-card"><div class="label">Entrypoint</div><div class="value small">${esc(s.entrypoint)}</div></div>
|
|
1007
|
+
</div>
|
|
1008
|
+
${s.alive ? `
|
|
1009
|
+
<div class="workflow-section">
|
|
1010
|
+
<div style="display:flex;align-items:center;gap:12px">
|
|
1011
|
+
<button onclick="closeSession('${s.sessionId}','${s.pid}')"
|
|
1012
|
+
style="background:var(--red)22;color:var(--red);border:1px solid var(--red)44;padding:6px 16px;border-radius:4px;cursor:pointer;font-size:12px;font-weight:600"
|
|
1013
|
+
id="close-btn-${s.sessionId}">Close Session (PID ${s.pid})</button>
|
|
1014
|
+
<span style="font-size:11px;color:var(--text-faint)">Sends SIGTERM to terminate this Claude Code process</span>
|
|
1015
|
+
</div>
|
|
1016
|
+
</div>` : ''}
|
|
1017
|
+
${nameSection}
|
|
1018
|
+
${aiCardHtml}
|
|
1019
|
+
${todoHtml}
|
|
1020
|
+
`;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function renderAgents(s) {
|
|
1024
|
+
const agents = (s.agents || []).slice().sort((a, b) => {
|
|
1025
|
+
const ta = a.spawnedAt ? new Date(a.spawnedAt).getTime() : 0;
|
|
1026
|
+
const tb = b.spawnedAt ? new Date(b.spawnedAt).getTime() : 0;
|
|
1027
|
+
return ta - tb;
|
|
1028
|
+
});
|
|
1029
|
+
if (!agents.length) return '<div class="empty-state">No sub-agents spawned in this session</div>';
|
|
1030
|
+
|
|
1031
|
+
const running = agents.filter(a => a.status === 'running');
|
|
1032
|
+
const completed = agents.filter(a => a.status === 'completed');
|
|
1033
|
+
|
|
1034
|
+
// Group agents by time proximity (within 5s = parallel)
|
|
1035
|
+
const groups = [];
|
|
1036
|
+
let currentGroup = [];
|
|
1037
|
+
for (const a of agents) {
|
|
1038
|
+
const t = a.spawnedAt ? new Date(a.spawnedAt).getTime() : 0;
|
|
1039
|
+
if (currentGroup.length === 0) {
|
|
1040
|
+
currentGroup.push(a);
|
|
1041
|
+
} else {
|
|
1042
|
+
const prevT = currentGroup[0].spawnedAt ? new Date(currentGroup[0].spawnedAt).getTime() : 0;
|
|
1043
|
+
if (Math.abs(t - prevT) < 5000) {
|
|
1044
|
+
currentGroup.push(a); // parallel
|
|
1045
|
+
} else {
|
|
1046
|
+
groups.push(currentGroup);
|
|
1047
|
+
currentGroup = [a];
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
if (currentGroup.length > 0) groups.push(currentGroup);
|
|
1052
|
+
|
|
1053
|
+
let agentIdx = 0;
|
|
1054
|
+
const groupsHtml = groups.map(group => {
|
|
1055
|
+
const isParallel = group.length > 1;
|
|
1056
|
+
const timeLabel = fmtTime(group[0].spawnedAt);
|
|
1057
|
+
const agentsHtml = group.map(a => {
|
|
1058
|
+
agentIdx++;
|
|
1059
|
+
return `
|
|
1060
|
+
<div class="agent-card" style="${a.status==='running'?'border-color:var(--orange)44':''}${isParallel?';margin-left:20px':''}">
|
|
1061
|
+
<div class="agent-icon ${agentIcon(a.subagentType)}">${agentIdx}</div>
|
|
1062
|
+
<div class="agent-info">
|
|
1063
|
+
<div class="agent-name">${esc(a.description)}</div>
|
|
1064
|
+
<div class="agent-type">${esc(a.subagentType)} ${a.runInBackground?'· bg':''} · ${fmtTime(a.spawnedAt)}</div>
|
|
1065
|
+
</div>
|
|
1066
|
+
<span class="agent-status ${a.status}">${a.status === 'running' ? '● running' : a.status}</span>
|
|
1067
|
+
</div>`;
|
|
1068
|
+
}).join('');
|
|
1069
|
+
|
|
1070
|
+
if (isParallel) {
|
|
1071
|
+
return `
|
|
1072
|
+
<div style="margin-bottom:4px">
|
|
1073
|
+
<div style="font-size:10px;color:var(--cyan);margin-bottom:4px;display:flex;align-items:center;gap:6px">
|
|
1074
|
+
<span style="display:inline-block;width:16px;height:1px;background:var(--cyan)"></span>
|
|
1075
|
+
PARALLEL (${group.length} agents @ ${timeLabel})
|
|
1076
|
+
<span style="flex:1;height:1px;background:var(--border)"></span>
|
|
1077
|
+
</div>
|
|
1078
|
+
${agentsHtml}
|
|
1079
|
+
</div>`;
|
|
1080
|
+
}
|
|
1081
|
+
return agentsHtml;
|
|
1082
|
+
}).join('');
|
|
1083
|
+
|
|
1084
|
+
return `
|
|
1085
|
+
<div class="workflow-section">
|
|
1086
|
+
<h3>Agent Workflow — Main Agent + ${agents.length} Sub-Agents (${running.length} active, ${completed.length} completed)</h3>
|
|
1087
|
+
<div class="agent-card" style="border-color:var(--accent)">
|
|
1088
|
+
<div class="agent-icon main">M</div>
|
|
1089
|
+
<div class="agent-info">
|
|
1090
|
+
<div class="agent-name">${esc(s.title || 'Main Agent')}</div>
|
|
1091
|
+
<div class="agent-type">orchestrator · ${s.entrypoint} · started ${fmtDate(s.startedAt)}</div>
|
|
1092
|
+
</div>
|
|
1093
|
+
<span class="agent-status ${s.alive?'running':'completed'}">${s.alive?'running':'completed'}</span>
|
|
1094
|
+
</div>
|
|
1095
|
+
<div style="padding-left:20px;border-left:2px solid var(--border);margin-left:16px">
|
|
1096
|
+
${groupsHtml}
|
|
1097
|
+
</div>
|
|
1098
|
+
</div>
|
|
1099
|
+
`;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// ── Activity Tab (merged Timeline + Events) ──────────
|
|
1103
|
+
function setActivityView(v) { activityView = v; if (selectedId) renderDetail(selectedId); }
|
|
1104
|
+
|
|
1105
|
+
function renderActivity(s) {
|
|
1106
|
+
const toggleHtml = `
|
|
1107
|
+
<div style="display:flex;gap:4px;margin-bottom:12px;align-items:center">
|
|
1108
|
+
<button onclick="setActivityView('table')" title="Curated view · Oldest first · Duplicate events merged" style="padding:4px 12px;border-radius:4px;border:1px solid ${activityView==='table'?'var(--accent)':'var(--border)'};background:${activityView==='table'?'var(--accent)22':'var(--surface)'};color:${activityView==='table'?'var(--accent)':'var(--text-dim)'};cursor:pointer;font-size:12px">Event Log</button>
|
|
1109
|
+
<button onclick="setActivityView('timeline')" title="Full detail · Newest first · Visual workflow timeline" style="padding:4px 12px;border-radius:4px;border:1px solid ${activityView==='timeline'?'var(--accent)':'var(--border)'};background:${activityView==='timeline'?'var(--accent)22':'var(--surface)'};color:${activityView==='timeline'?'var(--accent)':'var(--text-dim)'};cursor:pointer;font-size:12px">Timeline</button>
|
|
1110
|
+
</div>`;
|
|
1111
|
+
return toggleHtml + (activityView === 'timeline' ? renderTimeline(s) : renderEvents(s));
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function renderTimeline(s) {
|
|
1115
|
+
const events = s.recentEvents || [];
|
|
1116
|
+
if (!events.length) return '<div class="empty-state">No timeline events</div>';
|
|
1117
|
+
|
|
1118
|
+
// Build workflow nodes from events — show user messages, agent spawns, skills
|
|
1119
|
+
const nodes = [];
|
|
1120
|
+
for (const evt of events) {
|
|
1121
|
+
if (evt.type === 'user' && evt.text) {
|
|
1122
|
+
nodes.push({ cls: 'user', label: 'User', content: evt.text.slice(0,200), time: evt.timestamp });
|
|
1123
|
+
}
|
|
1124
|
+
if (evt.type === 'assistant') {
|
|
1125
|
+
// Check for agent/skill tool calls
|
|
1126
|
+
const agentTools = (evt.tools||[]).filter(t => t.tool === 'Agent');
|
|
1127
|
+
const skillTools = (evt.tools||[]).filter(t => t.tool === 'Skill');
|
|
1128
|
+
const otherTools = (evt.tools||[]).filter(t => t.tool !== 'Agent' && t.tool !== 'Skill');
|
|
1129
|
+
|
|
1130
|
+
if (agentTools.length > 0) {
|
|
1131
|
+
for (const a of agentTools) {
|
|
1132
|
+
nodes.push({ cls: 'agent', label: 'Agent Spawn', content: a.input, time: evt.timestamp });
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (skillTools.length > 0) {
|
|
1136
|
+
for (const sk of skillTools) {
|
|
1137
|
+
nodes.push({ cls: 'skill', label: 'Skill', content: sk.input, time: evt.timestamp });
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
if (evt.text && !agentTools.length) {
|
|
1141
|
+
const toolSummary = otherTools.length > 0 ? ` [${otherTools.map(t=>t.tool).join(', ')}]` : '';
|
|
1142
|
+
nodes.push({ cls: 'tool', label: 'Assistant', content: (evt.text.slice(0,150) + toolSummary).slice(0,200), time: evt.timestamp });
|
|
1143
|
+
} else if (otherTools.length > 0 && !agentTools.length) {
|
|
1144
|
+
nodes.push({ cls: 'tool', label: 'Tools', content: otherTools.map(t => `${t.tool}: ${t.input?.slice(0,60)||''}`).join(' | ').slice(0,200), time: evt.timestamp });
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
if (evt.type === 'skill') {
|
|
1148
|
+
nodes.push({ cls: 'skill', label: 'Skill', content: evt.skill + (evt.args ? ' ' + evt.args : ''), time: evt.timestamp });
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
return `
|
|
1153
|
+
<div class="workflow-section"><h3>Activity Timeline <span style="font-size:11px;font-weight:400;color:var(--text-faint)">(newest first)</span></h3></div>
|
|
1154
|
+
<div class="workflow-timeline">
|
|
1155
|
+
${nodes.slice(-40).reverse().map(n => `
|
|
1156
|
+
<div class="wf-node ${n.cls}">
|
|
1157
|
+
<span class="wf-label">${n.label}</span> <span class="wf-time">${fmtTime(n.time)}</span>
|
|
1158
|
+
<div class="wf-content">${esc(n.content)}</div>
|
|
1159
|
+
</div>
|
|
1160
|
+
`).join('')}
|
|
1161
|
+
</div>
|
|
1162
|
+
`;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function setEventFilter(f) { eventFilter = f; eventPage = 0; if (selectedId) renderDetail(selectedId); }
|
|
1166
|
+
function setEventOrder(o) { eventOrder = o; eventPage = 0; if (selectedId) renderDetail(selectedId); }
|
|
1167
|
+
function setEventPage(p) { eventPage = p; if (selectedId) renderDetail(selectedId); }
|
|
1168
|
+
|
|
1169
|
+
function renderEvents(s) {
|
|
1170
|
+
let events = (s.recentEvents || []).slice();
|
|
1171
|
+
|
|
1172
|
+
// Apply filter
|
|
1173
|
+
if (eventFilter === 'user') {
|
|
1174
|
+
events = events.filter(e => e.type === 'user' || e.type === 'remote-input');
|
|
1175
|
+
} else if (eventFilter === 'feishu') {
|
|
1176
|
+
events = events.filter(e => e.type === 'remote-input');
|
|
1177
|
+
} else if (eventFilter === 'substantive') {
|
|
1178
|
+
events = events.filter(e => e.hasContent === true);
|
|
1179
|
+
} else if (eventFilter === 'agents') {
|
|
1180
|
+
events = events.filter(e => e.type === 'user' || e.type === 'remote-input' || e.hasAgent === true || e.type === 'skill');
|
|
1181
|
+
}
|
|
1182
|
+
// 'all' shows everything including cron triggers
|
|
1183
|
+
|
|
1184
|
+
// Merge consecutive duplicate events (e.g. repeated empty cron polls)
|
|
1185
|
+
const merged = [];
|
|
1186
|
+
for (const evt of events) {
|
|
1187
|
+
const prev = merged.length > 0 ? merged[merged.length - 1] : null;
|
|
1188
|
+
if (prev && prev.type === evt.type && prev.type === 'cron-trigger') {
|
|
1189
|
+
// Collapse consecutive cron triggers
|
|
1190
|
+
if (!prev._mergeCount) prev._mergeCount = 1;
|
|
1191
|
+
prev._mergeCount++;
|
|
1192
|
+
prev._lastTimestamp = evt.timestamp;
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
if (prev && prev.type === evt.type && prev.type === 'assistant' && !prev.hasContent && !evt.hasContent && !prev.hasAgent && !evt.hasAgent) {
|
|
1196
|
+
// Collapse consecutive empty assistant responses
|
|
1197
|
+
if (!prev._mergeCount) prev._mergeCount = 1;
|
|
1198
|
+
prev._mergeCount++;
|
|
1199
|
+
prev._lastTimestamp = evt.timestamp;
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
merged.push({ ...evt });
|
|
1203
|
+
}
|
|
1204
|
+
events = merged;
|
|
1205
|
+
|
|
1206
|
+
// Apply order (default=oldest first)
|
|
1207
|
+
if (eventOrder === 'newest') {
|
|
1208
|
+
events = events.slice().reverse();
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const totalFiltered = events.length;
|
|
1212
|
+
const totalPages = Math.ceil(totalFiltered / EVENTS_PER_PAGE);
|
|
1213
|
+
const pageEvents = events.slice(eventPage * EVENTS_PER_PAGE, (eventPage + 1) * EVENTS_PER_PAGE);
|
|
1214
|
+
|
|
1215
|
+
const filterBtns = [
|
|
1216
|
+
['substantive', 'Substantive'],
|
|
1217
|
+
['user', 'User Only'],
|
|
1218
|
+
['feishu', 'Remote Input'],
|
|
1219
|
+
['agents', 'User + Agents'],
|
|
1220
|
+
['all', 'All (incl. cron)']
|
|
1221
|
+
].map(([k, label]) =>
|
|
1222
|
+
`<button class="ef-btn ${eventFilter===k?'active':''}" onclick="setEventFilter('${k}')">${label}</button>`
|
|
1223
|
+
).join('');
|
|
1224
|
+
|
|
1225
|
+
const orderBtns = [
|
|
1226
|
+
['oldest', 'Oldest First'],
|
|
1227
|
+
['newest', 'Newest First']
|
|
1228
|
+
].map(([k, label]) =>
|
|
1229
|
+
`<button class="ef-btn ${eventOrder===k?'active':''}" onclick="setEventOrder('${k}')">${label}</button>`
|
|
1230
|
+
).join('');
|
|
1231
|
+
|
|
1232
|
+
return `
|
|
1233
|
+
<div class="event-filter-bar">
|
|
1234
|
+
${filterBtns}
|
|
1235
|
+
<div class="ef-sep"></div>
|
|
1236
|
+
${orderBtns}
|
|
1237
|
+
<div class="ef-sep"></div>
|
|
1238
|
+
<span style="font-size:11px;color:var(--text-faint)">${totalFiltered} events${totalFiltered < (s.recentEvents||[]).length ? ' (from ' + (s.recentEvents||[]).length + ')':''}</span>
|
|
1239
|
+
</div>
|
|
1240
|
+
<div class="event-feed">
|
|
1241
|
+
<div class="event-feed-header">
|
|
1242
|
+
<span>Events (page ${eventPage+1}/${totalPages || 1})</span>
|
|
1243
|
+
<span>Total: ${s.totalUserMessages + s.totalAssistantMessages} messages, ${s.totalToolCalls} tool calls</span>
|
|
1244
|
+
</div>
|
|
1245
|
+
${pageEvents.length === 0 ? '<div class="event-item" style="color:var(--text-dim)">No events match the current filter</div>' : ''}
|
|
1246
|
+
${pageEvents.map(evt => {
|
|
1247
|
+
if (evt.type === 'user') {
|
|
1248
|
+
return `<div class="event-item" style="border-left:3px solid var(--accent)">
|
|
1249
|
+
<span class="event-time">${fmtDate(evt.timestamp)}</span>
|
|
1250
|
+
<span class="event-type user">USER</span>
|
|
1251
|
+
<span class="event-text">${esc(evt.text)}</span>
|
|
1252
|
+
</div>`;
|
|
1253
|
+
}
|
|
1254
|
+
if (evt.type === 'assistant') {
|
|
1255
|
+
const toolsHtml = (evt.tools||[]).map(t =>
|
|
1256
|
+
`<span class="tool-tag ${t.tool==='Agent'?'agent':''}">${esc(t.tool)}</span><span class="tool-input">${esc(t.input?.slice(0,80))}</span>`
|
|
1257
|
+
).join(' ');
|
|
1258
|
+
const borderColor = evt.hasAgent ? 'var(--cyan)' : evt.hasContent ? 'var(--green)' : 'var(--border)';
|
|
1259
|
+
const mergeInfo = evt._mergeCount ? ` <span style="color:var(--text-faint);font-size:10px">(×${evt._mergeCount + 1} similar)</span>` : '';
|
|
1260
|
+
return `<div class="event-item" style="border-left:3px solid ${borderColor}">
|
|
1261
|
+
<span class="event-time">${fmtDate(evt.timestamp)}</span>
|
|
1262
|
+
<span class="event-type assistant">ASST</span>
|
|
1263
|
+
${evt.text ? `<span class="event-text">${esc(evt.text.slice(0,400))}${mergeInfo}</span>` : mergeInfo}
|
|
1264
|
+
${toolsHtml ? `<div class="event-tools">${toolsHtml}</div>` : ''}
|
|
1265
|
+
</div>`;
|
|
1266
|
+
}
|
|
1267
|
+
if (evt.type === 'skill') {
|
|
1268
|
+
return `<div class="event-item" style="border-left:3px solid var(--pink)">
|
|
1269
|
+
<span class="event-time">${fmtDate(evt.timestamp)}</span>
|
|
1270
|
+
<span class="event-type skill">SKILL</span>
|
|
1271
|
+
<span class="event-text">${esc(evt.skill)} ${esc(evt.args||'')}</span>
|
|
1272
|
+
</div>`;
|
|
1273
|
+
}
|
|
1274
|
+
if (evt.type === 'remote-input') {
|
|
1275
|
+
const channelColors = { feishu: 'var(--yellow)', teams: 'var(--purple)', wechat: 'var(--green)', slack: 'var(--pink)', remote: 'var(--orange)' };
|
|
1276
|
+
const channelNames = { feishu: 'FEISHU', teams: 'TEAMS', wechat: 'WECHAT', slack: 'SLACK', remote: 'REMOTE' };
|
|
1277
|
+
const color = channelColors[evt.channel] || channelColors.remote;
|
|
1278
|
+
const label = channelNames[evt.channel] || 'REMOTE';
|
|
1279
|
+
return `<div class="event-item" style="border-left:3px solid ${color};background:var(--surface2)">
|
|
1280
|
+
<span class="event-time">${fmtDate(evt.timestamp)}</span>
|
|
1281
|
+
<span class="event-type" style="color:${color}">${label}</span>
|
|
1282
|
+
<span class="event-text">${esc(evt.text)}</span>
|
|
1283
|
+
</div>`;
|
|
1284
|
+
}
|
|
1285
|
+
if (evt.type === 'cron-trigger') {
|
|
1286
|
+
const mergeInfo = evt._mergeCount ? ` <span style="color:var(--text-faint);font-size:10px">(×${evt._mergeCount + 1}, ${fmtDate(evt.timestamp)} – ${fmtDate(evt._lastTimestamp)})</span>` : '';
|
|
1287
|
+
return `<div class="event-item" style="border-left:3px solid var(--text-faint);opacity:0.6">
|
|
1288
|
+
<span class="event-time">${fmtDate(evt.timestamp)}</span>
|
|
1289
|
+
<span class="event-type" style="color:var(--text-faint)">CRON</span>
|
|
1290
|
+
<span class="event-text" style="color:var(--text-faint)">${esc(evt.text?.slice(0,120))}...${mergeInfo}</span>
|
|
1291
|
+
</div>`;
|
|
1292
|
+
}
|
|
1293
|
+
return '';
|
|
1294
|
+
}).join('')}
|
|
1295
|
+
</div>
|
|
1296
|
+
${totalPages > 1 ? `
|
|
1297
|
+
<div class="event-pager">
|
|
1298
|
+
<button onclick="setEventPage(${eventPage - 1})" ${eventPage <= 0 ? 'disabled' : ''}>← Prev</button>
|
|
1299
|
+
<span>Page ${eventPage + 1} of ${totalPages}</span>
|
|
1300
|
+
<button onclick="setEventPage(${eventPage + 1})" ${eventPage >= totalPages - 1 ? 'disabled' : ''}>Next →</button>
|
|
1301
|
+
<button onclick="setEventPage(0)" ${eventPage <= 0 ? 'disabled' : ''}>First</button>
|
|
1302
|
+
<button onclick="setEventPage(${totalPages - 1})" ${eventPage >= totalPages - 1 ? 'disabled' : ''}>Last</button>
|
|
1303
|
+
</div>
|
|
1304
|
+
` : ''}
|
|
1305
|
+
`;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function renderCompound(s) {
|
|
1309
|
+
// AI-powered summary — on-demand via claude -p
|
|
1310
|
+
const cachedKey = 'ai-summary-' + s.sessionId;
|
|
1311
|
+
const cached = window[cachedKey];
|
|
1312
|
+
const isLoading = window['ai-summary-loading-' + s.sessionId];
|
|
1313
|
+
|
|
1314
|
+
const summaryHtml = cached ? `
|
|
1315
|
+
<div class="knowledge-card" style="border-color:var(--accent)44;margin-bottom:12px">
|
|
1316
|
+
<h4 style="color:var(--accent)">Session Name</h4>
|
|
1317
|
+
<div style="display:flex;align-items:center;gap:8px;margin-top:8px">
|
|
1318
|
+
<span style="flex:1;font-size:13px;color:var(--text);font-weight:600">${esc(cached.briefName || (cached.summary ? cached.summary.split(/[.\n]/)[0].slice(0,60) : 'AI Summary'))}</span>
|
|
1319
|
+
<button class="btn-approve" onclick="renameSession('${s.sessionId}','${esc((cached.briefName || (cached.summary ? cached.summary.split(/[.\\n]/)[0].slice(0,60) : '')).replace(/'/g,''))}')">Use as Name</button>
|
|
1320
|
+
</div>
|
|
1321
|
+
</div>
|
|
1322
|
+
<div class="knowledge-card" style="border-color:var(--green)44">
|
|
1323
|
+
<h4 style="color:var(--green)">Summary</h4>
|
|
1324
|
+
<p style="white-space:pre-wrap;line-height:1.8">${esc(cached.summary)}</p>
|
|
1325
|
+
<div class="k-meta">Generated ${fmtDate(cached.generatedAt)}</div>
|
|
1326
|
+
</div>
|
|
1327
|
+
<div class="knowledge-card" style="border-color:var(--cyan)44">
|
|
1328
|
+
<h4>Lessons Learned</h4>
|
|
1329
|
+
<p style="white-space:pre-wrap;line-height:1.8">${esc(cached.lessons)}</p>
|
|
1330
|
+
</div>
|
|
1331
|
+
<div class="knowledge-actions" style="margin-top:12px">
|
|
1332
|
+
<button class="btn-extract" onclick="generateAISummary('${s.sessionId}', true)" id="ai-btn-${s.sessionId}">Regenerate</button>
|
|
1333
|
+
</div>
|
|
1334
|
+
` : isLoading ? `
|
|
1335
|
+
<div style="text-align:center;padding:40px 20px">
|
|
1336
|
+
<div style="font-size:14px;color:var(--accent);margin-bottom:16px">Generating AI Summary...</div>
|
|
1337
|
+
<div class="spinner" style="margin:0 auto 16px"></div>
|
|
1338
|
+
<div style="font-size:11px;color:var(--text-faint)">Please wait. The summary will appear here when ready.</div>
|
|
1339
|
+
</div>
|
|
1340
|
+
` : `
|
|
1341
|
+
<div style="text-align:center;padding:40px 20px">
|
|
1342
|
+
${(window._claudeCliStatus && !window._claudeCliStatus.available) ? `
|
|
1343
|
+
<div style="background:#ff990022;border:1px solid #ff990055;border-radius:6px;padding:12px 16px;margin-bottom:20px;text-align:left">
|
|
1344
|
+
<div style="font-weight:600;color:#ff9900;font-size:13px;margin-bottom:6px">Claude CLI not found</div>
|
|
1345
|
+
<div style="font-size:12px;color:var(--text-dim);line-height:1.6">AI Summary requires Claude Code CLI. Install it with:<br>
|
|
1346
|
+
<code style="color:var(--cyan);background:var(--bg);padding:2px 8px;border-radius:3px;display:inline-block;margin-top:4px">npm install -g @anthropic-ai/claude-code</code>
|
|
1347
|
+
</div>
|
|
1348
|
+
</div>` : ''}
|
|
1349
|
+
<div style="font-size:14px;color:var(--text-dim);margin-bottom:16px">Click the button to generate an AI-powered summary using <code style="color:var(--cyan)">claude -p</code></div>
|
|
1350
|
+
<button class="btn-extract" onclick="generateAISummary('${s.sessionId}')" id="ai-btn-${s.sessionId}" style="font-size:14px;padding:12px 32px"${(window._claudeCliStatus && !window._claudeCliStatus.available) ? ' disabled title="Install Claude CLI first"' : ''}>
|
|
1351
|
+
Generate AI Summary
|
|
1352
|
+
</button>
|
|
1353
|
+
<div style="font-size:11px;color:var(--text-faint);margin-top:12px">This will invoke Claude CLI in pipe mode. May take 30-60 seconds.</div>
|
|
1354
|
+
</div>
|
|
1355
|
+
`;
|
|
1356
|
+
|
|
1357
|
+
return `
|
|
1358
|
+
<div class="compound-intro">
|
|
1359
|
+
<strong>AI Session Analysis</strong> (powered by <code style="color:var(--cyan)">claude -p</code>)<br>
|
|
1360
|
+
On-demand AI-generated summary and lessons learned from this session's conversation history.
|
|
1361
|
+
</div>
|
|
1362
|
+
${summaryHtml}
|
|
1363
|
+
`;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
async function generateAISummary(sessionId, force) {
|
|
1367
|
+
const btn = document.getElementById('ai-btn-' + sessionId);
|
|
1368
|
+
if (btn) {
|
|
1369
|
+
btn.disabled = true;
|
|
1370
|
+
btn.textContent = 'Generating... (30-60s)';
|
|
1371
|
+
btn.style.opacity = '0.6';
|
|
1372
|
+
}
|
|
1373
|
+
// Store in-flight state so it survives tab switches
|
|
1374
|
+
window['ai-summary-loading-' + sessionId] = true;
|
|
1375
|
+
try {
|
|
1376
|
+
const res = await fetch('/api/sessions/' + sessionId + '/ai-summary', {
|
|
1377
|
+
method: 'POST',
|
|
1378
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1379
|
+
body: JSON.stringify({ force: !!force })
|
|
1380
|
+
});
|
|
1381
|
+
if (!res.ok) {
|
|
1382
|
+
const err = await res.json();
|
|
1383
|
+
throw new Error(err.error || 'Request failed');
|
|
1384
|
+
}
|
|
1385
|
+
const data = await res.json();
|
|
1386
|
+
window['ai-summary-' + sessionId] = data;
|
|
1387
|
+
delete window['ai-summary-loading-' + sessionId];
|
|
1388
|
+
// Re-render only if AI Summary tab is currently visible
|
|
1389
|
+
const el = document.getElementById('ai-btn-' + sessionId) || document.querySelector('.compound-intro');
|
|
1390
|
+
if (el) renderDetail(sessionId);
|
|
1391
|
+
} catch (e) {
|
|
1392
|
+
delete window['ai-summary-loading-' + sessionId];
|
|
1393
|
+
const retryBtn = document.getElementById('ai-btn-' + sessionId);
|
|
1394
|
+
if (retryBtn) {
|
|
1395
|
+
retryBtn.disabled = false;
|
|
1396
|
+
retryBtn.textContent = 'Retry';
|
|
1397
|
+
retryBtn.style.opacity = '1';
|
|
1398
|
+
}
|
|
1399
|
+
alert('AI Summary failed: ' + e.message);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// ── Shared Helpers (used by config page) ────────────────
|
|
1404
|
+
|
|
1405
|
+
function vscodeLink(filePath) {
|
|
1406
|
+
if (!filePath) return '<span style="color:var(--text-dim);font-size:11px">(path unavailable)</span>';
|
|
1407
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
1408
|
+
return '<a href="vscode://file/' + encodeURI(normalized) + '" onclick="event.stopPropagation()" style="color:var(--accent);text-decoration:none;font-size:11px" title="Open in VS Code">' + esc(normalized) + '</a>';
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function toggleSettingsSection(id) {
|
|
1412
|
+
const el = document.getElementById(id);
|
|
1413
|
+
const toggle = document.getElementById('toggle-' + id);
|
|
1414
|
+
if (!el) return;
|
|
1415
|
+
if (el.style.display === 'none') {
|
|
1416
|
+
el.style.display = 'block';
|
|
1417
|
+
if (toggle) toggle.textContent = '▼';
|
|
1418
|
+
} else {
|
|
1419
|
+
el.style.display = 'none';
|
|
1420
|
+
if (toggle) toggle.textContent = '▶';
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function renderMcpServerCard(name, config, scope) {
|
|
1425
|
+
const typeIcon = config.type === 'stdio' ? 'STDIO' : config.type === 'sse' ? 'SSE' : (config.type || '?').toUpperCase();
|
|
1426
|
+
const cmdLine = [config.command || '', ...(config.args || [])].join(' ');
|
|
1427
|
+
const envEntries = config.env ? Object.entries(config.env) : [];
|
|
1428
|
+
|
|
1429
|
+
return `<div class="agent-card" style="border-color:var(--pink)33;flex-direction:column;align-items:stretch;gap:8px">
|
|
1430
|
+
<div style="display:flex;align-items:center;gap:10px">
|
|
1431
|
+
<div class="agent-icon" style="background:var(--pink)22;color:var(--pink);font-size:9px;width:40px">${typeIcon}</div>
|
|
1432
|
+
<div style="flex:1;min-width:0">
|
|
1433
|
+
<div style="font-size:13px;font-weight:600;color:var(--text)">${esc(name)}</div>
|
|
1434
|
+
<div style="font-size:11px;color:var(--text-dim);font-family:monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="${esc(cmdLine)}">${esc(cmdLine)}</div>
|
|
1435
|
+
</div>
|
|
1436
|
+
<span style="font-size:10px;padding:2px 8px;border-radius:10px;background:var(--pink)11;color:var(--pink)">${scope}</span>
|
|
1437
|
+
</div>
|
|
1438
|
+
${envEntries.length > 0 ? `
|
|
1439
|
+
<div style="margin-left:50px">
|
|
1440
|
+
<div style="font-size:10px;color:var(--text-faint);margin-bottom:3px;cursor:pointer" onclick="this.nextElementSibling.style.display=this.nextElementSibling.style.display==='none'?'block':'none'">ENV (${envEntries.length} vars) — click to toggle</div>
|
|
1441
|
+
<div style="display:none">
|
|
1442
|
+
${envEntries.map(([k, v]) => `<div style="font-size:10px;font-family:monospace"><span style="color:var(--cyan)">${esc(k)}</span>=<span style="color:var(--text-dim)">${esc(String(v))}</span></div>`).join('')}
|
|
1443
|
+
</div>
|
|
1444
|
+
</div>` : ''}
|
|
1445
|
+
</div>`;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function renderCron(s) {
|
|
1449
|
+
const crons = s.cronJobs || [];
|
|
1450
|
+
|
|
1451
|
+
// Fetch global cron jobs (use cache if available, refresh in background)
|
|
1452
|
+
const renderGlobal = (allCrons) => {
|
|
1453
|
+
const el = document.getElementById('global-crons');
|
|
1454
|
+
if (!el) return;
|
|
1455
|
+
if (allCrons.length === 0) {
|
|
1456
|
+
el.innerHTML = '<div style="color:var(--text-dim);font-size:12px">No cron jobs found across any session</div>';
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
el.innerHTML = allCrons.map(c => `
|
|
1460
|
+
<div class="agent-card">
|
|
1461
|
+
<div class="agent-icon" style="background:var(--orange)22;color:var(--orange);font-size:11px">${esc(c.cron)}</div>
|
|
1462
|
+
<div class="agent-info">
|
|
1463
|
+
<div class="agent-name" style="font-size:11px">${esc(c.prompt?.slice(0, 100))}</div>
|
|
1464
|
+
<div class="agent-type">
|
|
1465
|
+
Session: ${esc(c.sessionTitle?.slice(0,30) || c.sessionId?.slice(0,8))}
|
|
1466
|
+
· ${c.sessionAlive ? '<span style="color:var(--green)">alive</span>' : '<span style="color:var(--text-dim)">ended</span>'}
|
|
1467
|
+
· ${c.recurring ? 'recurring' : 'one-shot'}
|
|
1468
|
+
${c.durable ? '· durable' : '· session-only'}
|
|
1469
|
+
· created ${fmtTime(c.createdAt)}
|
|
1470
|
+
</div>
|
|
1471
|
+
</div>
|
|
1472
|
+
<span class="agent-status ${c.sessionAlive ? 'running' : 'completed'}">${c.sessionAlive ? '● active' : '○ expired'}</span>
|
|
1473
|
+
</div>
|
|
1474
|
+
`).join('');
|
|
1475
|
+
};
|
|
1476
|
+
|
|
1477
|
+
// Use cached data immediately, then refresh in background
|
|
1478
|
+
if (globalCronsCache) {
|
|
1479
|
+
setTimeout(() => renderGlobal(globalCronsCache), 0);
|
|
1480
|
+
}
|
|
1481
|
+
fetch('/api/cron-jobs').then(r => r.json()).then(allCrons => {
|
|
1482
|
+
globalCronsCache = allCrons;
|
|
1483
|
+
renderGlobal(allCrons);
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
return `
|
|
1487
|
+
<div class="workflow-section">
|
|
1488
|
+
<h3>Cron Jobs — This Session (${crons.length})</h3>
|
|
1489
|
+
${crons.length === 0 ? '<div style="color:var(--text-dim);font-size:12px;margin-bottom:16px">No cron jobs in this session</div>' : ''}
|
|
1490
|
+
${crons.map(c => `
|
|
1491
|
+
<div class="agent-card">
|
|
1492
|
+
<div class="agent-icon" style="background:var(--orange)22;color:var(--orange);font-size:11px">${esc(c.cron)}</div>
|
|
1493
|
+
<div class="agent-info">
|
|
1494
|
+
<div class="agent-name" style="font-size:11px">${esc(c.prompt?.slice(0, 120))}</div>
|
|
1495
|
+
<div class="agent-type">${c.recurring?'recurring':'one-shot'} ${c.durable?'· durable':'· session-only'} · created ${fmtTime(c.createdAt)}</div>
|
|
1496
|
+
</div>
|
|
1497
|
+
<span class="agent-status running">active</span>
|
|
1498
|
+
</div>
|
|
1499
|
+
`).join('')}
|
|
1500
|
+
</div>
|
|
1501
|
+
|
|
1502
|
+
<div class="workflow-section">
|
|
1503
|
+
<h3>All Cron Jobs — Across All Sessions</h3>
|
|
1504
|
+
<div id="global-crons">${globalCronsCache ? '' : '<div style="color:var(--text-dim);font-size:12px">Loading global cron jobs...</div>'}</div>
|
|
1505
|
+
</div>
|
|
1506
|
+
|
|
1507
|
+
<div style="margin-top:20px;padding:16px;background:var(--surface);border:1px solid var(--border);border-radius:6px">
|
|
1508
|
+
<h3 style="font-size:13px;margin-bottom:8px">About Cron Jobs</h3>
|
|
1509
|
+
<div style="font-size:12px;color:var(--text-dim);line-height:1.8">
|
|
1510
|
+
<strong>Session-only</strong> cron jobs (like remote message polling) live in the Claude Code process memory and expire when the session ends.<br>
|
|
1511
|
+
<strong>Durable</strong> cron jobs are saved to <code>~/.claude/scheduled_tasks.json</code> and survive restarts.<br>
|
|
1512
|
+
<strong>Note:</strong> Cron jobs from ended sessions (session-only) are no longer running, even if shown here — they are historical records from the JSONL log.<br>
|
|
1513
|
+
<strong>Why only 1 session polls?</strong> Each session creates its own session-only cron jobs on startup, but those die with the session. Only the <em>currently active</em> session's crons are running. Historical sessions' cron records here are just logs of what <em>was</em> scheduled.
|
|
1514
|
+
</div>
|
|
1515
|
+
</div>
|
|
1516
|
+
`;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// Boot
|
|
1520
|
+
loadAgents();
|
|
1521
|
+
connectWS();
|
|
1522
|
+
fetch('/api/claude-cli-status').then(r=>r.json()).then(d => { window._claudeCliStatus = d; }).catch(()=>{});
|
|
1523
|
+
fetch('/api/sessions').then(r=>r.json()).then(data => {
|
|
1524
|
+
sessions = data; refresh();
|
|
1525
|
+
if (sessions.length > 0) {
|
|
1526
|
+
const active = sessions.find(s => s.alive);
|
|
1527
|
+
selectSession((active||sessions[0]).sessionId);
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
</script>
|
|
1531
|
+
</body>
|
|
1532
|
+
</html>
|