cc-prompter 0.1.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 +210 -0
- package/dist/index.cjs +924 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +28 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +885 -0
- package/dist/index.js.map +1 -0
- package/dist/inject.js +329 -0
- package/dist/panel.html +822 -0
- package/package.json +67 -0
package/dist/panel.html
ADDED
|
@@ -0,0 +1,822 @@
|
|
|
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>CC Prompter</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
:root {
|
|
10
|
+
--border: #e0e0e0; --border-lt: #ebebeb; --border-active: #b0b0b0;
|
|
11
|
+
--text: #333; --text-sec: #888; --text-dim: #bbb;
|
|
12
|
+
--bg: #fff; --bg-code: #f7f7f7; --bg-hover: #f5f5f5;
|
|
13
|
+
--accent: #1677ff; --success: #52c41a; --err: #ff4d4f;
|
|
14
|
+
--r: 6px;
|
|
15
|
+
--mono: "SF Mono","Fira Code","Menlo","Consolas",monospace;
|
|
16
|
+
--sans: -apple-system,BlinkMacSystemFont,"SF Pro Text","Segoe UI","PingFang SC",sans-serif;
|
|
17
|
+
}
|
|
18
|
+
body { font-family: var(--sans); font-size: 13px; color: var(--text); background: var(--bg); height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
|
|
19
|
+
.header { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; border-bottom: 1px solid var(--border-lt); flex-shrink: 0; }
|
|
20
|
+
.header-title { font-weight: 600; font-size: 13px; display: flex; align-items: center; gap: 6px; }
|
|
21
|
+
.header-title svg { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
|
22
|
+
.session-tabs { display: flex; gap: 4px; padding: 6px 12px; border-bottom: 1px solid var(--border-lt); flex-shrink: 0; overflow-x: auto; }
|
|
23
|
+
.session-tabs::-webkit-scrollbar { display: none; }
|
|
24
|
+
.tab { padding: 4px 10px; border-radius: 4px; font-size: 11px; cursor: pointer; border: 1px solid var(--border-lt); background: var(--bg); color: var(--text-sec); white-space: nowrap; transition: all .15s; }
|
|
25
|
+
.tab:hover { border-color: var(--border-active); color: var(--text); }
|
|
26
|
+
.tab.active { border-color: var(--accent); color: var(--accent); font-weight: 500; }
|
|
27
|
+
.tab.new { border-style: dashed; color: var(--text-dim); }
|
|
28
|
+
.tab .tab-close { margin-left: 6px; font-size: 10px; opacity: 0.5; }
|
|
29
|
+
.tab .tab-close:hover { opacity: 1; color: var(--err); }
|
|
30
|
+
.tab .tab-status { display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-right: 4px; }
|
|
31
|
+
.tab .tab-status.ready { background: var(--success); }
|
|
32
|
+
.tab .tab-status.busy { background: var(--accent); animation: pulse 1s infinite; }
|
|
33
|
+
.tab .tab-status.exited { background: var(--text-dim); }
|
|
34
|
+
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
|
|
35
|
+
.messages { flex: 1; overflow-y: auto; padding: 8px 12px; }
|
|
36
|
+
.messages::-webkit-scrollbar { width: 4px; }
|
|
37
|
+
.messages::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
|
38
|
+
|
|
39
|
+
/* ── Message segments ── */
|
|
40
|
+
.seg { margin-bottom: 8px; }
|
|
41
|
+
.seg-user { }
|
|
42
|
+
.seg-user .seg-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--accent); margin-bottom: 2px; }
|
|
43
|
+
.seg-user .seg-body { font-size: 12px; line-height: 1.6; color: var(--text); white-space: pre-wrap; word-break: break-word; }
|
|
44
|
+
|
|
45
|
+
.seg-asst { }
|
|
46
|
+
.seg-asst .seg-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--success); margin-bottom: 4px; }
|
|
47
|
+
.seg-asst .seg-body { font-size: 12px; line-height: 1.65; color: var(--text); }
|
|
48
|
+
.seg-asst .seg-body .cursor { display: inline-block; width: 2px; height: 14px; background: var(--accent); vertical-align: text-bottom; margin-left: 1px; animation: blink 0.8s step-end infinite; }
|
|
49
|
+
@keyframes blink { 50% { opacity: 0; } }
|
|
50
|
+
|
|
51
|
+
.seg-tool { padding: 4px 8px; background: var(--bg-code); border-radius: 4px; border: 1px solid var(--border-lt); font-size: 11px; font-family: var(--mono); color: var(--text-sec); margin: 2px 0 6px 0; word-break: break-all; overflow-wrap: break-word; }
|
|
52
|
+
.seg-tool .tool-name { font-weight: 600; color: var(--text); }
|
|
53
|
+
.seg-tool .tool-result { color: var(--text-dim); font-size: 10px; margin-top: 2px; word-break: break-all; overflow-wrap: break-word; }
|
|
54
|
+
|
|
55
|
+
.seg-sys { }
|
|
56
|
+
.seg-sys .seg-body { font-size: 10px; color: var(--text-dim); font-style: italic; text-align: center; padding: 4px 0; }
|
|
57
|
+
|
|
58
|
+
/* ── Markdown rendered styles ── */
|
|
59
|
+
.seg-body h1, .seg-body h2, .seg-body h3, .seg-body h4 { font-weight: 600; margin: 0.8em 0 0.4em; line-height: 1.35; }
|
|
60
|
+
.seg-body h1 { font-size: 1.3em; }
|
|
61
|
+
.seg-body h2 { font-size: 1.15em; }
|
|
62
|
+
.seg-body h3 { font-size: 1.05em; }
|
|
63
|
+
.seg-body h4 { font-size: 1em; }
|
|
64
|
+
.seg-body h1:first-child, .seg-body h2:first-child, .seg-body h3:first-child { margin-top: 0; }
|
|
65
|
+
.seg-body p { margin: 0.4em 0; }
|
|
66
|
+
.seg-body ul, .seg-body ol { margin: 0.3em 0; padding-left: 1.8em; }
|
|
67
|
+
.seg-body li { margin: 0.15em 0; }
|
|
68
|
+
.seg-body code { font-family: var(--mono); font-size: 0.9em; background: var(--bg-code); padding: 1px 5px; border-radius: 3px; border: 1px solid var(--border-lt); }
|
|
69
|
+
.seg-body pre { background: var(--bg-code); border: 1px solid var(--border-lt); border-radius: var(--r); padding: 8px 10px; margin: 0.4em 0; overflow-x: auto; }
|
|
70
|
+
.seg-body pre code { background: none; padding: 0; border: none; font-size: 0.85em; line-height: 1.5; }
|
|
71
|
+
.seg-body blockquote { border-left: 3px solid var(--accent); padding: 2px 10px; margin: 0.4em 0; color: var(--text-sec); }
|
|
72
|
+
.seg-body strong { font-weight: 600; }
|
|
73
|
+
.seg-body em { font-style: italic; }
|
|
74
|
+
.seg-body hr { border: none; border-top: 1px solid var(--border-lt); margin: 0.6em 0; }
|
|
75
|
+
.seg-body a { color: var(--accent); }
|
|
76
|
+
|
|
77
|
+
.msg-thinking { font-size: 12px; color: var(--text-dim); padding: 4px 0 8px 0; display: flex; align-items: center; gap: 8px; }
|
|
78
|
+
.msg-thinking .dots span { display: inline-block; width: 4px; height: 4px; background: var(--text-dim); border-radius: 50%; animation: dot-pulse 1.4s ease-in-out infinite; }
|
|
79
|
+
.msg-thinking .dots span:nth-child(2) { animation-delay: 0.2s; }
|
|
80
|
+
.msg-thinking .dots span:nth-child(3) { animation-delay: 0.4s; }
|
|
81
|
+
@keyframes dot-pulse { 0%,80%,100% { opacity: 0.3; transform: scale(0.8); } 40% { opacity: 1; transform: scale(1.2); } }
|
|
82
|
+
|
|
83
|
+
.source-bar { padding: 6px 12px; border-bottom: 1px solid var(--border-lt); display: none; flex-shrink: 0; }
|
|
84
|
+
.source-bar.visible { display: flex; align-items: center; gap: 6px; }
|
|
85
|
+
.source-label { font-size: 10px; color: var(--text-sec); text-transform: uppercase; letter-spacing: 0.5px; }
|
|
86
|
+
.source-path { font-size: 12px; color: var(--accent); font-family: var(--mono); }
|
|
87
|
+
.source-elem { font-size: 10px; color: var(--text-dim); font-family: var(--mono); margin-left: auto; max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
88
|
+
.source-close { font-size: 14px; color: var(--text-dim); cursor: pointer; margin-left: 4px; }
|
|
89
|
+
.source-close:hover { color: var(--err); }
|
|
90
|
+
.input-area { padding: 8px 12px; border-top: 1px solid var(--border-lt); display: flex; flex-direction: column; gap: 6px; flex-shrink: 0; }
|
|
91
|
+
.input-row { display: flex; gap: 6px; }
|
|
92
|
+
.input-row textarea { flex: 1; min-height: 38px; max-height: 100px; padding: 7px 10px; font-size: 13px; line-height: 1.5; font-family: var(--sans); border: 1px solid var(--border); border-radius: var(--r); color: var(--text); background: var(--bg); outline: none; resize: vertical; transition: border-color .15s; }
|
|
93
|
+
.input-row textarea:focus { border-color: var(--accent); }
|
|
94
|
+
.input-row textarea::placeholder { color: var(--text-dim); }
|
|
95
|
+
.input-row textarea:disabled { background: var(--bg-code); color: var(--text-sec); }
|
|
96
|
+
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 4px; padding: 5px 12px; border-radius: 5px; font-size: 12px; line-height: 20px; cursor: pointer; outline: none; border: 1px solid var(--border); background: var(--bg); color: var(--text-sec); transition: all .15s; }
|
|
97
|
+
.btn:hover { border-color: var(--border-active); color: var(--text); }
|
|
98
|
+
.btn.primary { border-color: var(--accent); background: var(--accent); color: #fff; font-weight: 500; }
|
|
99
|
+
.btn.primary:hover { background: #4096ff; }
|
|
100
|
+
.btn.primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
101
|
+
.btn svg { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
|
102
|
+
.input-actions { display: flex; justify-content: space-between; align-items: center; }
|
|
103
|
+
.input-hint { font-size: 10px; color: var(--text-dim); }
|
|
104
|
+
.input-status { font-size: 11px; }
|
|
105
|
+
.input-status.running { color: var(--accent); }
|
|
106
|
+
.input-status.done { color: var(--success); }
|
|
107
|
+
</style>
|
|
108
|
+
</head>
|
|
109
|
+
<body>
|
|
110
|
+
|
|
111
|
+
<div class="header">
|
|
112
|
+
<div class="header-title">
|
|
113
|
+
<svg viewBox="0 0 24 24"><path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20ZM12 6a6 6 0 1 1 0 12 6 6 0 0 1 0-12ZM12 10a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z"/></svg>
|
|
114
|
+
CC Sessions
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div class="session-tabs" id="sessionTabs">
|
|
119
|
+
<div class="tab new" id="tabNew" title="New Session">+ New</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div class="source-bar" id="sourceBar">
|
|
123
|
+
<span class="source-label">SRC</span>
|
|
124
|
+
<span class="source-path" id="sourcePath"></span>
|
|
125
|
+
<span class="source-elem" id="sourceElem"></span>
|
|
126
|
+
<span class="source-close" id="sourceClose" title="Clear source">×</span>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div class="messages" id="messages"></div>
|
|
130
|
+
|
|
131
|
+
<div class="input-area" id="inputArea">
|
|
132
|
+
<div class="input-row">
|
|
133
|
+
<textarea id="input" placeholder="Describe the change, Enter to send..." rows="1"></textarea>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="input-actions">
|
|
136
|
+
<span class="input-hint" id="inputHint">Shift+Enter newline</span>
|
|
137
|
+
<span class="input-status" id="inputStatus"></span>
|
|
138
|
+
<div style="display:flex;gap:6px;">
|
|
139
|
+
<button class="btn" id="btnCompact" title="/compact">Compact</button>
|
|
140
|
+
<button class="btn" id="btnNew" title="/new">New</button>
|
|
141
|
+
<button class="btn" id="btnStop" title="Stop generation" style="display:none;">
|
|
142
|
+
<svg viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
|
|
143
|
+
Stop
|
|
144
|
+
</button>
|
|
145
|
+
<button class="btn primary" id="btnSend">
|
|
146
|
+
<svg viewBox="0 0 24 24"><path d="M22 2 11 13M22 2l-7 20-4-9-9-4 20-7z"/></svg>
|
|
147
|
+
Send
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<script>
|
|
154
|
+
(function() {
|
|
155
|
+
var API = window.location.origin + '/api';
|
|
156
|
+
// Each session: { info, segments: [], sourceInfo: null, streaming: false }
|
|
157
|
+
var sessions = {};
|
|
158
|
+
var activeId = null;
|
|
159
|
+
|
|
160
|
+
// ── Segment-based rendering ──
|
|
161
|
+
// Each session has .segments[] — a flat list of rendered blocks.
|
|
162
|
+
// segment kinds: 'user' | 'asst' | 'tool' | 'sys'
|
|
163
|
+
// 'asst' segments have `finalized: bool` for streaming cursor.
|
|
164
|
+
|
|
165
|
+
var $tabs = document.getElementById('sessionTabs');
|
|
166
|
+
var $tabNew = document.getElementById('tabNew');
|
|
167
|
+
var $sourceBar = document.getElementById('sourceBar');
|
|
168
|
+
var $sourcePath = document.getElementById('sourcePath');
|
|
169
|
+
var $sourceElem = document.getElementById('sourceElem');
|
|
170
|
+
var $sourceClose = document.getElementById('sourceClose');
|
|
171
|
+
var $messages = document.getElementById('messages');
|
|
172
|
+
var $input = document.getElementById('input');
|
|
173
|
+
var $inputStatus = document.getElementById('inputStatus');
|
|
174
|
+
var $btnSend = document.getElementById('btnSend');
|
|
175
|
+
var $btnCompact = document.getElementById('btnCompact');
|
|
176
|
+
var $btnNew = document.getElementById('btnNew');
|
|
177
|
+
var $btnStop = document.getElementById('btnStop');
|
|
178
|
+
|
|
179
|
+
// ── Simple Markdown Renderer ──
|
|
180
|
+
|
|
181
|
+
function renderMarkdown(text) {
|
|
182
|
+
if (!text) return '';
|
|
183
|
+
var lines = text.replace(/\r\n/g, '\n').split('\n');
|
|
184
|
+
var out = '';
|
|
185
|
+
var inCodeBlock = false;
|
|
186
|
+
var codeLang = '';
|
|
187
|
+
var codeBuf = '';
|
|
188
|
+
var inList = false;
|
|
189
|
+
|
|
190
|
+
function flushList() {
|
|
191
|
+
if (!inList) return;
|
|
192
|
+
out += '</' + inList + '>';
|
|
193
|
+
inList = false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function flushCode() {
|
|
197
|
+
if (!inCodeBlock) return;
|
|
198
|
+
var langAttr = codeLang ? ' class="lang-' + escAttr(codeLang) + '"' : '';
|
|
199
|
+
out += '<pre><code' + langAttr + '>' + escHtml(codeBuf.replace(/\n$/, '')) + '</code></pre>';
|
|
200
|
+
inCodeBlock = false;
|
|
201
|
+
codeBuf = '';
|
|
202
|
+
codeLang = '';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (var i = 0; i < lines.length; i++) {
|
|
206
|
+
var raw = lines[i];
|
|
207
|
+
var trimmed = raw.trim();
|
|
208
|
+
|
|
209
|
+
if (trimmed.indexOf('```') === 0) {
|
|
210
|
+
if (inCodeBlock) {
|
|
211
|
+
flushCode();
|
|
212
|
+
} else {
|
|
213
|
+
flushList();
|
|
214
|
+
inCodeBlock = true;
|
|
215
|
+
codeLang = trimmed.slice(3).trim();
|
|
216
|
+
codeBuf = '';
|
|
217
|
+
}
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (inCodeBlock) {
|
|
222
|
+
codeBuf += raw + '\n';
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!trimmed) {
|
|
227
|
+
flushList();
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (/^#{1,4}\s/.test(trimmed)) {
|
|
232
|
+
flushList();
|
|
233
|
+
var m = trimmed.match(/^(#{1,4})\s+(.+)/);
|
|
234
|
+
var lvl = m[1].length;
|
|
235
|
+
out += '<h' + lvl + '>' + inlineMd(m[2]) + '</h' + lvl + '>';
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (/^[-*_]{3,}\s*$/.test(trimmed)) {
|
|
240
|
+
flushList();
|
|
241
|
+
out += '<hr>';
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (/^>\s?/.test(trimmed)) {
|
|
246
|
+
flushList();
|
|
247
|
+
out += '<blockquote><p>' + inlineMd(trimmed.replace(/^>\s?/, '')) + '</p></blockquote>';
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (/^[-*]\s+/.test(trimmed)) {
|
|
252
|
+
if (inList !== 'ul') { flushList(); inList = 'ul'; out += '<ul>'; }
|
|
253
|
+
out += '<li>' + inlineMd(trimmed.replace(/^[-*]\s+/, '')) + '</li>';
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (/^\d+\.\s+/.test(trimmed)) {
|
|
258
|
+
if (inList !== 'ol') { flushList(); inList = 'ol'; out += '<ol>'; }
|
|
259
|
+
out += '<li>' + inlineMd(trimmed.replace(/^\d+\.\s+/, '')) + '</li>';
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
flushList();
|
|
264
|
+
out += '<p>' + inlineMd(trimmed) + '</p>';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
flushList();
|
|
268
|
+
flushCode();
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function inlineMd(s) {
|
|
273
|
+
return s
|
|
274
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
275
|
+
.replace(/__(.+?)__/g, '<strong>$1</strong>')
|
|
276
|
+
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
277
|
+
.replace(/_(.+?)_/g, '<em>$1</em>')
|
|
278
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
279
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function escHtml(s) {
|
|
283
|
+
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function escAttr(s) {
|
|
287
|
+
return String(s).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ── Session management ──
|
|
291
|
+
|
|
292
|
+
function api(method, path, body) {
|
|
293
|
+
var opts = { method: method, headers: { 'Content-Type': 'application/json' } };
|
|
294
|
+
if (body) opts.body = JSON.stringify(body);
|
|
295
|
+
return fetch(API + path, opts).then(function(r) {
|
|
296
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
297
|
+
return r.json();
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function createSession() {
|
|
302
|
+
return api('POST', '/sessions', {}).then(function(info) {
|
|
303
|
+
sessions[info.id] = { info: info, segments: [], sourceInfo: null, streaming: false };
|
|
304
|
+
renderTabs();
|
|
305
|
+
selectSession(info.id);
|
|
306
|
+
return info;
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function renderTabs() {
|
|
311
|
+
var old = $tabs.querySelectorAll('.tab:not(.new)');
|
|
312
|
+
for (var i = 0; i < old.length; i++) old[i].remove();
|
|
313
|
+
var ids = Object.keys(sessions);
|
|
314
|
+
for (var j = 0; j < ids.length; j++) {
|
|
315
|
+
var id = ids[j];
|
|
316
|
+
var s = sessions[id];
|
|
317
|
+
var tab = document.createElement('div');
|
|
318
|
+
tab.className = 'tab' + (id === activeId ? ' active' : '');
|
|
319
|
+
tab.dataset.id = id;
|
|
320
|
+
// Use frontend streaming flag for more accurate status
|
|
321
|
+
var statusClass = s.streaming ? 'busy' : (s.info.status || 'ready');
|
|
322
|
+
tab.innerHTML = '<span class="tab-status ' + statusClass + '"></span>'
|
|
323
|
+
+ escHtml(s.info.title || 'Session')
|
|
324
|
+
+ (id === activeId ? '<span class="tab-close" data-close="' + id + '">x</span>' : '');
|
|
325
|
+
$tabs.insertBefore(tab, $tabNew);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function selectSession(id) {
|
|
330
|
+
activeId = id;
|
|
331
|
+
renderTabs();
|
|
332
|
+
renderAllSegments();
|
|
333
|
+
updateSourceBarDisplay();
|
|
334
|
+
updateInputState();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ── Source bar per-session display ──
|
|
338
|
+
|
|
339
|
+
function updateSourceBarDisplay() {
|
|
340
|
+
var s = sessions[activeId];
|
|
341
|
+
if (s && s.sourceInfo && s.sourceInfo.path) {
|
|
342
|
+
$sourceBar.className = 'source-bar visible';
|
|
343
|
+
var idx = s.sourceInfo.path.lastIndexOf('/src/');
|
|
344
|
+
var shortPath = idx >= 0 ? s.sourceInfo.path.substring(idx + 1) : s.sourceInfo.path.split('/').slice(-3).join('/');
|
|
345
|
+
$sourcePath.textContent = shortPath + ':' + s.sourceInfo.line + ':' + s.sourceInfo.column;
|
|
346
|
+
$sourceElem.textContent = s.sourceInfo.elementInfo || '';
|
|
347
|
+
} else {
|
|
348
|
+
$sourceBar.className = 'source-bar';
|
|
349
|
+
$sourcePath.textContent = '';
|
|
350
|
+
$sourceElem.textContent = '';
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ── Segment rendering ──
|
|
355
|
+
|
|
356
|
+
function renderAllSegments() {
|
|
357
|
+
$messages.innerHTML = '';
|
|
358
|
+
var s = sessions[activeId];
|
|
359
|
+
if (!s) return;
|
|
360
|
+
for (var i = 0; i < s.segments.length; i++) {
|
|
361
|
+
s.segments[i]._domOwn = false; // reset for re-render
|
|
362
|
+
appendSegmentDom(s.segments[i]);
|
|
363
|
+
}
|
|
364
|
+
scrollToBottom();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function appendSegmentDom(seg) {
|
|
368
|
+
var last = $messages.lastElementChild;
|
|
369
|
+
|
|
370
|
+
if (seg.kind === 'asst' && last && last.dataset && last.dataset.segKind === 'asst' && !seg._domOwn) {
|
|
371
|
+
var body = last.querySelector('.seg-body');
|
|
372
|
+
if (body) {
|
|
373
|
+
body.innerHTML = renderMarkdown(seg.content || '');
|
|
374
|
+
if (!seg.finalized) {
|
|
375
|
+
var cursor = document.createElement('span');
|
|
376
|
+
cursor.className = 'cursor';
|
|
377
|
+
body.appendChild(cursor);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
seg._domOwn = true;
|
|
381
|
+
scrollToBottom();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
var div = document.createElement('div');
|
|
386
|
+
div.className = 'seg seg-' + seg.kind;
|
|
387
|
+
div.dataset.segKind = seg.kind;
|
|
388
|
+
|
|
389
|
+
switch (seg.kind) {
|
|
390
|
+
case 'user':
|
|
391
|
+
div.innerHTML = '<div class="seg-label">User</div><div class="seg-body">' + escHtml(seg.content || '') + '</div>';
|
|
392
|
+
break;
|
|
393
|
+
case 'asst':
|
|
394
|
+
var bodyHtml = renderMarkdown(seg.content || '');
|
|
395
|
+
if (!seg.finalized) {
|
|
396
|
+
bodyHtml += '<span class="cursor"></span>';
|
|
397
|
+
}
|
|
398
|
+
div.innerHTML = '<div class="seg-label">Claude</div><div class="seg-body">' + bodyHtml + '</div>';
|
|
399
|
+
break;
|
|
400
|
+
case 'tool':
|
|
401
|
+
div.className = 'seg seg-tool';
|
|
402
|
+
var inputStr = typeof seg.input === 'string' ? seg.input : JSON.stringify(seg.input || {}).slice(0, 200);
|
|
403
|
+
div.innerHTML = '<span class="tool-name">' + escHtml(seg.name) + '</span> ' + escHtml(inputStr);
|
|
404
|
+
if (seg.result) {
|
|
405
|
+
div.innerHTML += '<div class="tool-result">' + escHtml(String(seg.result).slice(0, 300)) + '</div>';
|
|
406
|
+
}
|
|
407
|
+
break;
|
|
408
|
+
case 'sys':
|
|
409
|
+
div.innerHTML = '<div class="seg-body">' + escHtml(seg.content || '') + '</div>';
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
seg._domOwn = true;
|
|
414
|
+
$messages.appendChild(div);
|
|
415
|
+
scrollToBottom();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function getLastAsstSeg(s) {
|
|
419
|
+
for (var i = s.segments.length - 1; i >= 0; i--) {
|
|
420
|
+
if (s.segments[i].kind === 'asst') return s.segments[i];
|
|
421
|
+
}
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function scrollToBottom() {
|
|
426
|
+
$messages.scrollTop = $messages.scrollHeight;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function updateInputState() {
|
|
430
|
+
var s = sessions[activeId];
|
|
431
|
+
var thisBusy = s && s.streaming;
|
|
432
|
+
var disabled = !s || s.info.status === 'exited' || thisBusy;
|
|
433
|
+
$input.disabled = disabled;
|
|
434
|
+
$btnSend.disabled = disabled;
|
|
435
|
+
|
|
436
|
+
// Toggle Stop / Send button
|
|
437
|
+
if (thisBusy) {
|
|
438
|
+
$btnStop.style.display = 'inline-flex';
|
|
439
|
+
$btnSend.style.display = 'none';
|
|
440
|
+
$inputStatus.textContent = 'running...';
|
|
441
|
+
$inputStatus.className = 'input-status running';
|
|
442
|
+
} else {
|
|
443
|
+
$btnStop.style.display = 'none';
|
|
444
|
+
$btnSend.style.display = 'inline-flex';
|
|
445
|
+
if (!s || s.info.status !== 'exited') {
|
|
446
|
+
$inputStatus.textContent = '';
|
|
447
|
+
$inputStatus.className = 'input-status';
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ── Send message ──
|
|
453
|
+
|
|
454
|
+
function sendMessage() {
|
|
455
|
+
var content = $input.value.trim();
|
|
456
|
+
var sendId = activeId;
|
|
457
|
+
if (!content || !sendId) return;
|
|
458
|
+
|
|
459
|
+
var s = sessions[sendId];
|
|
460
|
+
if (!s || s.streaming) return;
|
|
461
|
+
|
|
462
|
+
var sourceInfo = (s.sourceInfo && s.sourceInfo.path) ? s.sourceInfo : null;
|
|
463
|
+
|
|
464
|
+
// Add user segment
|
|
465
|
+
var userSeg = { kind: 'user', content: content };
|
|
466
|
+
s.segments.push(userSeg);
|
|
467
|
+
if (sendId === activeId) appendSegmentDom(userSeg);
|
|
468
|
+
|
|
469
|
+
$input.value = '';
|
|
470
|
+
s.streaming = true;
|
|
471
|
+
updateInputState();
|
|
472
|
+
|
|
473
|
+
if (sendId === activeId) showThinking();
|
|
474
|
+
|
|
475
|
+
// Clear consumed source info
|
|
476
|
+
s.sourceInfo = null;
|
|
477
|
+
if (sendId === activeId) updateSourceBarDisplay();
|
|
478
|
+
|
|
479
|
+
fetch(API + '/sessions/' + sendId + '/message', {
|
|
480
|
+
method: 'POST',
|
|
481
|
+
headers: { 'Content-Type': 'application/json' },
|
|
482
|
+
body: JSON.stringify({ content: content, sourceInfo: sourceInfo })
|
|
483
|
+
}).then(function(resp) {
|
|
484
|
+
if (!resp.ok || !resp.body) {
|
|
485
|
+
throw new Error('HTTP ' + resp.status);
|
|
486
|
+
}
|
|
487
|
+
var reader = resp.body.getReader();
|
|
488
|
+
var decoder = new TextDecoder();
|
|
489
|
+
var sseBuf = '';
|
|
490
|
+
|
|
491
|
+
function read() {
|
|
492
|
+
reader.read().then(function(result) {
|
|
493
|
+
if (result.done) {
|
|
494
|
+
finishStream(sendId);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
sseBuf += decoder.decode(result.value, { stream: true });
|
|
498
|
+
var parts = sseBuf.split('\n\n');
|
|
499
|
+
sseBuf = parts.pop();
|
|
500
|
+
for (var i = 0; i < parts.length; i++) {
|
|
501
|
+
parseSsePart(parts[i], sendId);
|
|
502
|
+
}
|
|
503
|
+
read();
|
|
504
|
+
}).catch(function() {
|
|
505
|
+
finishStream(sendId);
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
read();
|
|
509
|
+
}).catch(function(err) {
|
|
510
|
+
addSysSegment('[error] ' + err.message, sendId);
|
|
511
|
+
finishStream(sendId);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function parseSsePart(part, sessionId) {
|
|
516
|
+
var lines = part.split('\n');
|
|
517
|
+
for (var i = 0; i < lines.length; i++) {
|
|
518
|
+
var line = lines[i];
|
|
519
|
+
if (line.indexOf('data: ') !== 0) continue;
|
|
520
|
+
var raw = line.slice(6);
|
|
521
|
+
try {
|
|
522
|
+
var evt = JSON.parse(raw);
|
|
523
|
+
handleEvent(evt, sessionId);
|
|
524
|
+
} catch(e) {}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function handleEvent(evt, sessionId) {
|
|
529
|
+
var s = sessions[sessionId];
|
|
530
|
+
if (!s) return;
|
|
531
|
+
|
|
532
|
+
var isViewing = (sessionId === activeId);
|
|
533
|
+
|
|
534
|
+
switch (evt.type) {
|
|
535
|
+
case 'user':
|
|
536
|
+
break;
|
|
537
|
+
|
|
538
|
+
case 'assistant_text': {
|
|
539
|
+
if (isViewing) {
|
|
540
|
+
removeThinking();
|
|
541
|
+
updateProgress('');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
var lastAsst = getLastAsstSeg(s);
|
|
545
|
+
if (lastAsst && !lastAsst.finalized) {
|
|
546
|
+
lastAsst.content += evt.content;
|
|
547
|
+
lastAsst._domOwn = false;
|
|
548
|
+
if (isViewing) appendSegmentDom(lastAsst);
|
|
549
|
+
else lastAsst._domOwn = true;
|
|
550
|
+
} else {
|
|
551
|
+
var seg = { kind: 'asst', content: evt.content || '', finalized: false };
|
|
552
|
+
s.segments.push(seg);
|
|
553
|
+
if (isViewing) appendSegmentDom(seg);
|
|
554
|
+
else seg._domOwn = true;
|
|
555
|
+
}
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
case 'assistant_tool': {
|
|
560
|
+
var toolSeg = { kind: 'tool', name: evt.tool.name, input: evt.tool.input, result: evt.tool.result };
|
|
561
|
+
s.segments.push(toolSeg);
|
|
562
|
+
if (isViewing) appendSegmentDom(toolSeg);
|
|
563
|
+
else toolSeg._domOwn = true;
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
case 'system': {
|
|
568
|
+
var sysSeg = { kind: 'sys', content: evt.content || '' };
|
|
569
|
+
s.segments.push(sysSeg);
|
|
570
|
+
if (isViewing) appendSegmentDom(sysSeg);
|
|
571
|
+
else sysSeg._domOwn = true;
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
case 'done': {
|
|
576
|
+
if (isViewing) removeThinking();
|
|
577
|
+
// Finalize the last assistant segment
|
|
578
|
+
var lastAsst = getLastAsstSeg(s);
|
|
579
|
+
if (lastAsst && !lastAsst.finalized) {
|
|
580
|
+
lastAsst.finalized = true;
|
|
581
|
+
lastAsst._domOwn = false;
|
|
582
|
+
if (isViewing) {
|
|
583
|
+
var lastDom = $messages.querySelector('.seg-asst:last-child');
|
|
584
|
+
if (lastDom) {
|
|
585
|
+
var body = lastDom.querySelector('.seg-body');
|
|
586
|
+
if (body) {
|
|
587
|
+
body.innerHTML = renderMarkdown(lastAsst.content || '');
|
|
588
|
+
var cur = body.querySelector('.cursor');
|
|
589
|
+
if (cur) cur.remove();
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (evt.durationMs && isViewing) {
|
|
595
|
+
addSysSegment('Turn completed (' + (evt.durationMs / 1000).toFixed(1) + 's)', sessionId);
|
|
596
|
+
}
|
|
597
|
+
// Refresh session info
|
|
598
|
+
api('GET', '/sessions').then(function(list) {
|
|
599
|
+
for (var i = 0; i < list.length; i++) {
|
|
600
|
+
if (sessions[list[i].id]) {
|
|
601
|
+
sessions[list[i].id].info = list[i];
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
renderTabs();
|
|
605
|
+
});
|
|
606
|
+
finishStream(sessionId);
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
case 'error': {
|
|
611
|
+
if (isViewing) {
|
|
612
|
+
addSysSegment('[error] ' + (evt.content || 'Unknown error'), sessionId);
|
|
613
|
+
} else {
|
|
614
|
+
var errSeg = { kind: 'sys', content: '[error] ' + (evt.content || 'Unknown error') };
|
|
615
|
+
s.segments.push(errSeg);
|
|
616
|
+
}
|
|
617
|
+
finishStream(sessionId);
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
case 'progress': {
|
|
622
|
+
if (isViewing) updateProgress(evt.content || '');
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function addSysSegment(text, sessionId) {
|
|
629
|
+
var sid = sessionId || activeId;
|
|
630
|
+
var s = sessions[sid];
|
|
631
|
+
var seg = { kind: 'sys', content: text };
|
|
632
|
+
if (s) s.segments.push(seg);
|
|
633
|
+
if (sid === activeId) appendSegmentDom(seg);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// ── Thinking / Progress indicators ──
|
|
637
|
+
|
|
638
|
+
function showThinking() {
|
|
639
|
+
removeThinking();
|
|
640
|
+
var div = document.createElement('div');
|
|
641
|
+
div.className = 'msg-thinking';
|
|
642
|
+
div.id = 'thinkingIndicator';
|
|
643
|
+
div.innerHTML = 'Thinking <span class="dots"><span></span><span></span><span></span></span>';
|
|
644
|
+
$messages.appendChild(div);
|
|
645
|
+
scrollToBottom();
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function removeThinking() {
|
|
649
|
+
var el = document.getElementById('thinkingIndicator');
|
|
650
|
+
if (el) el.remove();
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function updateProgress(text) {
|
|
654
|
+
var el = document.getElementById('thinkingIndicator');
|
|
655
|
+
if (text) {
|
|
656
|
+
if (el) {
|
|
657
|
+
el.textContent = text;
|
|
658
|
+
} else {
|
|
659
|
+
el = document.createElement('div');
|
|
660
|
+
el.className = 'msg-thinking';
|
|
661
|
+
el.id = 'thinkingIndicator';
|
|
662
|
+
el.textContent = text;
|
|
663
|
+
$messages.appendChild(el);
|
|
664
|
+
scrollToBottom();
|
|
665
|
+
}
|
|
666
|
+
} else {
|
|
667
|
+
if (el) el.innerHTML = 'Processing <span class="dots"><span></span><span></span><span></span></span>';
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function finishStream(sessionId) {
|
|
672
|
+
var s = sessions[sessionId];
|
|
673
|
+
if (!s || !s.streaming) return;
|
|
674
|
+
s.streaming = false;
|
|
675
|
+
updateInputState();
|
|
676
|
+
|
|
677
|
+
if (sessionId === activeId) {
|
|
678
|
+
$inputStatus.textContent = 'completed';
|
|
679
|
+
$inputStatus.className = 'input-status done';
|
|
680
|
+
setTimeout(function() {
|
|
681
|
+
if (!sessions[activeId] || !sessions[activeId].streaming) {
|
|
682
|
+
$inputStatus.textContent = '';
|
|
683
|
+
$inputStatus.className = 'input-status';
|
|
684
|
+
}
|
|
685
|
+
}, 3000);
|
|
686
|
+
$input.focus();
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ── Commands ──
|
|
691
|
+
|
|
692
|
+
function sendCommand(cmd) {
|
|
693
|
+
if (!activeId) return;
|
|
694
|
+
api('POST', '/sessions/' + activeId + '/command', { command: cmd }).then(function() {
|
|
695
|
+
if (cmd === '/new') {
|
|
696
|
+
sessions[activeId].segments = [];
|
|
697
|
+
sessions[activeId].info.title = 'New Session';
|
|
698
|
+
renderAllSegments();
|
|
699
|
+
renderTabs();
|
|
700
|
+
}
|
|
701
|
+
addSysSegment('Sent: ' + cmd);
|
|
702
|
+
}).catch(function(err) {
|
|
703
|
+
addSysSegment('[error] ' + err.message);
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ── Source info handling ──
|
|
708
|
+
|
|
709
|
+
function handleSourceInfo(source) {
|
|
710
|
+
// Find a suitable session: prefer active if non-exited, else any non-exited
|
|
711
|
+
var ids = Object.keys(sessions);
|
|
712
|
+
var nonExitedIds = ids.filter(function(id) { return sessions[id].info.status !== 'exited'; });
|
|
713
|
+
var targetId = null;
|
|
714
|
+
|
|
715
|
+
if (activeId && nonExitedIds.indexOf(activeId) >= 0) {
|
|
716
|
+
targetId = activeId;
|
|
717
|
+
} else if (nonExitedIds.length > 0) {
|
|
718
|
+
targetId = nonExitedIds[0];
|
|
719
|
+
selectSession(targetId);
|
|
720
|
+
} else {
|
|
721
|
+
// No sessions at all — auto-create
|
|
722
|
+
createSession().then(function(info) {
|
|
723
|
+
sessions[info.id].sourceInfo = source;
|
|
724
|
+
updateSourceBarDisplay();
|
|
725
|
+
$input.focus();
|
|
726
|
+
});
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Store source info on the target session
|
|
731
|
+
sessions[targetId].sourceInfo = source;
|
|
732
|
+
if (targetId !== activeId) selectSession(targetId);
|
|
733
|
+
else updateSourceBarDisplay();
|
|
734
|
+
$input.focus();
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Clear source info
|
|
738
|
+
$sourceClose.addEventListener('click', function() {
|
|
739
|
+
if (activeId && sessions[activeId]) {
|
|
740
|
+
sessions[activeId].sourceInfo = null;
|
|
741
|
+
}
|
|
742
|
+
$sourceBar.className = 'source-bar';
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
// ── Tab click handler ──
|
|
746
|
+
|
|
747
|
+
$tabs.addEventListener('click', function(e) {
|
|
748
|
+
var tab = e.target.closest('.tab');
|
|
749
|
+
if (!tab) return;
|
|
750
|
+
if (e.target.dataset.close) {
|
|
751
|
+
var closeId = e.target.dataset.close;
|
|
752
|
+
if (confirm('Close this session?')) {
|
|
753
|
+
api('DELETE', '/sessions/' + closeId).then(function() {
|
|
754
|
+
delete sessions[closeId];
|
|
755
|
+
if (activeId === closeId) {
|
|
756
|
+
var remaining = Object.keys(sessions);
|
|
757
|
+
selectSession(remaining.length ? remaining[0] : null);
|
|
758
|
+
}
|
|
759
|
+
renderTabs();
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (tab.id === 'tabNew') {
|
|
765
|
+
createSession();
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
selectSession(tab.dataset.id);
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
// ── Buttons ──
|
|
772
|
+
$btnSend.addEventListener('click', sendMessage);
|
|
773
|
+
$btnNew.addEventListener('click', function() { sendCommand('/new'); });
|
|
774
|
+
$btnCompact.addEventListener('click', function() { sendCommand('/compact'); });
|
|
775
|
+
|
|
776
|
+
// Stop button — interrupt current generation
|
|
777
|
+
$btnStop.addEventListener('click', function() {
|
|
778
|
+
if (!activeId || !sessions[activeId] || !sessions[activeId].streaming) return;
|
|
779
|
+
api('POST', '/sessions/' + activeId + '/interrupt').then(function() {
|
|
780
|
+
addSysSegment('Interrupt sent');
|
|
781
|
+
}).catch(function(err) {
|
|
782
|
+
addSysSegment('[error] ' + err.message);
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
$input.addEventListener('keydown', function(e) {
|
|
787
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
788
|
+
e.preventDefault();
|
|
789
|
+
sendMessage();
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
// ── Keyboard shortcuts ──
|
|
794
|
+
|
|
795
|
+
document.addEventListener('keydown', function(e) {
|
|
796
|
+
if (e.key === 'Escape') {
|
|
797
|
+
window.parent.postMessage({ type: 'cc-prompter:hide' }, '*');
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
// ── Receive source info from parent ──
|
|
802
|
+
window.addEventListener('message', function(e) {
|
|
803
|
+
if (e.data && e.data.type === 'cc-prompter:source-info') {
|
|
804
|
+
handleSourceInfo(e.data.source);
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
// ── Init ──
|
|
809
|
+
api('GET', '/sessions').then(function(list) {
|
|
810
|
+
for (var i = 0; i < list.length; i++) {
|
|
811
|
+
sessions[list[i].id] = { info: list[i], segments: [], sourceInfo: null, streaming: false };
|
|
812
|
+
}
|
|
813
|
+
renderTabs();
|
|
814
|
+
if (list.length > 0) selectSession(list[0].id);
|
|
815
|
+
}).catch(function() {
|
|
816
|
+
// Sidecar might not be up yet
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
})();
|
|
820
|
+
</script>
|
|
821
|
+
</body>
|
|
822
|
+
</html>
|