akemon 0.3.5 → 0.3.7
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/DATA_POLICY.md +128 -0
- package/README.md +156 -19
- package/TRADEMARK.md +74 -0
- package/dist/akemon-home.js +56 -0
- package/dist/akemon-message.js +107 -0
- package/dist/best-effort.js +8 -0
- package/dist/cli.js +1411 -132
- package/dist/cognitive-artifact-store.js +101 -0
- package/dist/cognitive-event-log.js +47 -0
- package/dist/config.js +45 -9
- package/dist/context.js +27 -6
- package/dist/core/contracts/layers.js +1 -0
- package/dist/core/contracts/permission.js +1 -0
- package/dist/core/contracts/workspace.js +1 -0
- package/dist/core-cognitive-module.js +768 -0
- package/dist/engine-peripheral.js +127 -26
- package/dist/engine-routing.js +58 -17
- package/dist/interactive-session.js +361 -0
- package/dist/local-interconnect.js +156 -0
- package/dist/local-registry.js +178 -0
- package/dist/mcp-server.js +4 -1
- package/dist/memory-proposal.js +379 -0
- package/dist/memory-recorder.js +368 -0
- package/dist/orphan-scan.js +36 -24
- package/dist/passive-reflection-cognitive-module.js +172 -0
- package/dist/peripheral-registry.js +235 -0
- package/dist/permission-audit.js +132 -0
- package/dist/relay-client.js +68 -9
- package/dist/relay-mode.js +34 -0
- package/dist/relay-peripheral.js +139 -49
- package/dist/runtime-platform.js +122 -0
- package/dist/secretariat/client.js +87 -0
- package/dist/self.js +15 -6
- package/dist/server.js +3695 -439
- package/dist/social-discovery.js +231 -0
- package/dist/software-agent-peripheral.js +314 -235
- package/dist/software-agent-result-cli.js +69 -0
- package/dist/software-agent-stream-cli.js +23 -0
- package/dist/software-agent-transport.js +177 -0
- package/dist/task-module.js +243 -0
- package/dist/task-registry.js +756 -0
- package/dist/vendor/xterm/addon-fit.js +2 -0
- package/dist/vendor/xterm/addon-search.js +2 -0
- package/dist/vendor/xterm/addon-web-links.js +2 -0
- package/dist/vendor/xterm/xterm.css +285 -0
- package/dist/vendor/xterm/xterm.js +2 -0
- package/dist/work-memory.js +339 -0
- package/dist/workbench-peripheral-guide.js +79 -0
- package/dist/workbench-session.js +1074 -0
- package/dist/workbench.html +4011 -0
- package/package.json +11 -4
- package/scripts/build.cjs +24 -0
- package/scripts/check-architecture-baseline.cjs +68 -0
- package/scripts/test.cjs +38 -0
|
@@ -0,0 +1,4011 @@
|
|
|
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>Akemon Workbench</title>
|
|
7
|
+
<link rel="stylesheet" href="/vendor/xterm/xterm.css">
|
|
8
|
+
<style>
|
|
9
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
10
|
+
html, body { height: 100%; }
|
|
11
|
+
body {
|
|
12
|
+
margin: 0;
|
|
13
|
+
background: #f8f7f6;
|
|
14
|
+
color: #1a1a1a;
|
|
15
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
}
|
|
18
|
+
button, input, select, textarea {
|
|
19
|
+
font: inherit;
|
|
20
|
+
}
|
|
21
|
+
.app {
|
|
22
|
+
display: grid;
|
|
23
|
+
grid-template-rows: 54px minmax(0, 1fr) 30px;
|
|
24
|
+
height: 100vh;
|
|
25
|
+
}
|
|
26
|
+
.topbar {
|
|
27
|
+
display: grid;
|
|
28
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
29
|
+
align-items: center;
|
|
30
|
+
gap: 14px;
|
|
31
|
+
padding: 0 14px;
|
|
32
|
+
background: #ffffff;
|
|
33
|
+
border-bottom: 1px solid #e0ddd8;
|
|
34
|
+
}
|
|
35
|
+
.brand {
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: baseline;
|
|
38
|
+
gap: 8px;
|
|
39
|
+
min-width: 168px;
|
|
40
|
+
}
|
|
41
|
+
.brand-title {
|
|
42
|
+
font-size: 15px;
|
|
43
|
+
font-weight: 650;
|
|
44
|
+
}
|
|
45
|
+
.brand-subtitle {
|
|
46
|
+
color: #888480;
|
|
47
|
+
font-size: 12px;
|
|
48
|
+
}
|
|
49
|
+
.page-tabs {
|
|
50
|
+
min-width: 0;
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
justify-content: center;
|
|
54
|
+
gap: 4px;
|
|
55
|
+
overflow-x: auto;
|
|
56
|
+
scrollbar-width: none;
|
|
57
|
+
}
|
|
58
|
+
.page-tabs::-webkit-scrollbar {
|
|
59
|
+
display: none;
|
|
60
|
+
}
|
|
61
|
+
.page-tab {
|
|
62
|
+
height: 34px;
|
|
63
|
+
min-width: 86px;
|
|
64
|
+
border: 1px solid transparent;
|
|
65
|
+
background: transparent;
|
|
66
|
+
color: #555250;
|
|
67
|
+
border-radius: 7px;
|
|
68
|
+
padding: 0 12px;
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
font-size: 13px;
|
|
71
|
+
font-weight: 600;
|
|
72
|
+
}
|
|
73
|
+
.page-tab:hover {
|
|
74
|
+
background: #f0eeeb;
|
|
75
|
+
}
|
|
76
|
+
.page-tab.active {
|
|
77
|
+
background: #e8f0e4;
|
|
78
|
+
border-color: #9cc498;
|
|
79
|
+
color: #2d6f3e;
|
|
80
|
+
}
|
|
81
|
+
.start-form {
|
|
82
|
+
display: grid;
|
|
83
|
+
grid-template-columns: minmax(120px, 1fr) minmax(96px, 150px);
|
|
84
|
+
gap: 10px;
|
|
85
|
+
align-items: end;
|
|
86
|
+
min-width: 0;
|
|
87
|
+
}
|
|
88
|
+
.start-form .field-wide {
|
|
89
|
+
grid-column: 1 / -1;
|
|
90
|
+
}
|
|
91
|
+
.start-form .checkbox-field {
|
|
92
|
+
align-self: center;
|
|
93
|
+
}
|
|
94
|
+
.start-form .btn-primary {
|
|
95
|
+
justify-self: start;
|
|
96
|
+
}
|
|
97
|
+
.field {
|
|
98
|
+
min-width: 0;
|
|
99
|
+
}
|
|
100
|
+
.field input,
|
|
101
|
+
.field select,
|
|
102
|
+
.token-input {
|
|
103
|
+
width: 100%;
|
|
104
|
+
height: 32px;
|
|
105
|
+
border: 1px solid #d0ccc5;
|
|
106
|
+
background: #ffffff;
|
|
107
|
+
color: #1a1a1a;
|
|
108
|
+
border-radius: 6px;
|
|
109
|
+
padding: 0 9px;
|
|
110
|
+
outline: none;
|
|
111
|
+
}
|
|
112
|
+
.field input:focus,
|
|
113
|
+
.field select:focus,
|
|
114
|
+
.token-input:focus {
|
|
115
|
+
border-color: #4a8f4a;
|
|
116
|
+
}
|
|
117
|
+
.checkbox-field {
|
|
118
|
+
display: flex;
|
|
119
|
+
align-items: center;
|
|
120
|
+
gap: 6px;
|
|
121
|
+
color: #666360;
|
|
122
|
+
font-size: 12px;
|
|
123
|
+
white-space: nowrap;
|
|
124
|
+
}
|
|
125
|
+
.btn {
|
|
126
|
+
height: 32px;
|
|
127
|
+
border: 1px solid #d0ccc5;
|
|
128
|
+
background: #f0eeeb;
|
|
129
|
+
color: #333130;
|
|
130
|
+
border-radius: 6px;
|
|
131
|
+
padding: 0 12px;
|
|
132
|
+
cursor: pointer;
|
|
133
|
+
}
|
|
134
|
+
.btn:hover {
|
|
135
|
+
background: #e5e2de;
|
|
136
|
+
}
|
|
137
|
+
.btn-primary {
|
|
138
|
+
background: #3a8a52;
|
|
139
|
+
border-color: #2e7a42;
|
|
140
|
+
color: #ffffff;
|
|
141
|
+
}
|
|
142
|
+
.btn-primary:hover {
|
|
143
|
+
background: #4a9a62;
|
|
144
|
+
}
|
|
145
|
+
.btn-danger {
|
|
146
|
+
background: #dc4840;
|
|
147
|
+
border-color: #c43830;
|
|
148
|
+
color: #ffffff;
|
|
149
|
+
}
|
|
150
|
+
.btn-danger:hover {
|
|
151
|
+
background: #e05850;
|
|
152
|
+
}
|
|
153
|
+
.auth-strip {
|
|
154
|
+
display: none;
|
|
155
|
+
align-items: center;
|
|
156
|
+
gap: 8px;
|
|
157
|
+
min-width: 280px;
|
|
158
|
+
}
|
|
159
|
+
.auth-strip.show {
|
|
160
|
+
display: flex;
|
|
161
|
+
}
|
|
162
|
+
.auth-message {
|
|
163
|
+
color: #9a3a30;
|
|
164
|
+
font-size: 12px;
|
|
165
|
+
white-space: nowrap;
|
|
166
|
+
}
|
|
167
|
+
.layout {
|
|
168
|
+
min-width: 0;
|
|
169
|
+
min-height: 0;
|
|
170
|
+
display: block;
|
|
171
|
+
overflow: hidden;
|
|
172
|
+
background: #fafaf9;
|
|
173
|
+
}
|
|
174
|
+
.page-view {
|
|
175
|
+
min-width: 0;
|
|
176
|
+
min-height: 0;
|
|
177
|
+
height: 100%;
|
|
178
|
+
overflow: hidden;
|
|
179
|
+
display: none;
|
|
180
|
+
}
|
|
181
|
+
.page-view.active {
|
|
182
|
+
display: grid;
|
|
183
|
+
}
|
|
184
|
+
.sessions-layout {
|
|
185
|
+
min-width: 0;
|
|
186
|
+
min-height: 0;
|
|
187
|
+
height: 100%;
|
|
188
|
+
display: grid;
|
|
189
|
+
grid-template-columns: minmax(320px, 420px) minmax(0, 1fr);
|
|
190
|
+
overflow: hidden;
|
|
191
|
+
}
|
|
192
|
+
.start-panel {
|
|
193
|
+
min-width: 0;
|
|
194
|
+
min-height: 0;
|
|
195
|
+
display: grid;
|
|
196
|
+
grid-template-rows: 40px minmax(0, 1fr);
|
|
197
|
+
border-right: 1px solid #e0ddd8;
|
|
198
|
+
background: #f4f3f1;
|
|
199
|
+
overflow: hidden;
|
|
200
|
+
}
|
|
201
|
+
.start-panel-body {
|
|
202
|
+
min-height: 0;
|
|
203
|
+
overflow: auto;
|
|
204
|
+
padding: 14px;
|
|
205
|
+
}
|
|
206
|
+
.sidebar {
|
|
207
|
+
min-height: 0;
|
|
208
|
+
display: grid;
|
|
209
|
+
grid-template-rows: 40px minmax(0, 1fr);
|
|
210
|
+
background: #ffffff;
|
|
211
|
+
overflow: hidden;
|
|
212
|
+
}
|
|
213
|
+
.sidebar-head {
|
|
214
|
+
display: flex;
|
|
215
|
+
align-items: center;
|
|
216
|
+
justify-content: space-between;
|
|
217
|
+
gap: 8px;
|
|
218
|
+
padding: 0 12px;
|
|
219
|
+
border-bottom: 1px solid #e0ddd8;
|
|
220
|
+
}
|
|
221
|
+
.sidebar-title {
|
|
222
|
+
color: #555250;
|
|
223
|
+
font-size: 12px;
|
|
224
|
+
font-weight: 650;
|
|
225
|
+
text-transform: uppercase;
|
|
226
|
+
letter-spacing: 0.04em;
|
|
227
|
+
}
|
|
228
|
+
.sidebar-actions {
|
|
229
|
+
display: flex;
|
|
230
|
+
align-items: center;
|
|
231
|
+
gap: 6px;
|
|
232
|
+
}
|
|
233
|
+
.sidebar-actions .btn {
|
|
234
|
+
width: auto;
|
|
235
|
+
padding: 4px 7px;
|
|
236
|
+
font-size: 12px;
|
|
237
|
+
}
|
|
238
|
+
.session-list {
|
|
239
|
+
min-height: 0;
|
|
240
|
+
overflow: auto;
|
|
241
|
+
overscroll-behavior: contain;
|
|
242
|
+
padding: 8px;
|
|
243
|
+
}
|
|
244
|
+
.session-item {
|
|
245
|
+
width: 100%;
|
|
246
|
+
border: 1px solid transparent;
|
|
247
|
+
background: transparent;
|
|
248
|
+
color: inherit;
|
|
249
|
+
text-align: left;
|
|
250
|
+
border-radius: 7px;
|
|
251
|
+
padding: 9px;
|
|
252
|
+
cursor: pointer;
|
|
253
|
+
}
|
|
254
|
+
.session-item:hover {
|
|
255
|
+
background: #edecea;
|
|
256
|
+
}
|
|
257
|
+
.session-item.active {
|
|
258
|
+
background: #e8f0e4;
|
|
259
|
+
border-color: #9cc498;
|
|
260
|
+
}
|
|
261
|
+
.session-line {
|
|
262
|
+
display: flex;
|
|
263
|
+
align-items: center;
|
|
264
|
+
justify-content: space-between;
|
|
265
|
+
gap: 8px;
|
|
266
|
+
}
|
|
267
|
+
.session-name {
|
|
268
|
+
min-width: 0;
|
|
269
|
+
overflow: hidden;
|
|
270
|
+
white-space: nowrap;
|
|
271
|
+
text-overflow: ellipsis;
|
|
272
|
+
font-size: 13px;
|
|
273
|
+
font-weight: 600;
|
|
274
|
+
}
|
|
275
|
+
.session-meta {
|
|
276
|
+
margin-top: 4px;
|
|
277
|
+
color: #888480;
|
|
278
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
279
|
+
font-size: 11px;
|
|
280
|
+
overflow: hidden;
|
|
281
|
+
white-space: nowrap;
|
|
282
|
+
text-overflow: ellipsis;
|
|
283
|
+
}
|
|
284
|
+
.status-pill {
|
|
285
|
+
border: 1px solid #d0ccc5;
|
|
286
|
+
border-radius: 999px;
|
|
287
|
+
color: #555250;
|
|
288
|
+
font-size: 11px;
|
|
289
|
+
padding: 2px 7px;
|
|
290
|
+
line-height: 1.2;
|
|
291
|
+
}
|
|
292
|
+
.status-running {
|
|
293
|
+
color: #2a7a3a;
|
|
294
|
+
border-color: #8cc498;
|
|
295
|
+
background: #edf7ee;
|
|
296
|
+
}
|
|
297
|
+
.status-ended {
|
|
298
|
+
color: #8a7540;
|
|
299
|
+
border-color: #d4c498;
|
|
300
|
+
background: #faf5ea;
|
|
301
|
+
}
|
|
302
|
+
.status-failed {
|
|
303
|
+
color: #9a3a30;
|
|
304
|
+
border-color: #e0aaa4;
|
|
305
|
+
background: #fff5f3;
|
|
306
|
+
}
|
|
307
|
+
.empty {
|
|
308
|
+
color: #999690;
|
|
309
|
+
padding: 18px 10px;
|
|
310
|
+
font-size: 13px;
|
|
311
|
+
}
|
|
312
|
+
.session-history-note {
|
|
313
|
+
color: #888480;
|
|
314
|
+
padding: 8px 9px;
|
|
315
|
+
font-size: 12px;
|
|
316
|
+
line-height: 1.35;
|
|
317
|
+
}
|
|
318
|
+
.main {
|
|
319
|
+
min-width: 0;
|
|
320
|
+
min-height: 0;
|
|
321
|
+
height: 100%;
|
|
322
|
+
display: grid;
|
|
323
|
+
grid-template-rows: 40px minmax(0, 1fr);
|
|
324
|
+
background: #fafaf9;
|
|
325
|
+
overflow: hidden;
|
|
326
|
+
}
|
|
327
|
+
.session-toolbar {
|
|
328
|
+
min-width: 0;
|
|
329
|
+
display: flex;
|
|
330
|
+
align-items: center;
|
|
331
|
+
justify-content: space-between;
|
|
332
|
+
gap: 12px;
|
|
333
|
+
padding: 0 12px;
|
|
334
|
+
border-bottom: 1px solid #e0ddd8;
|
|
335
|
+
background: #f6f5f3;
|
|
336
|
+
}
|
|
337
|
+
.active-summary {
|
|
338
|
+
min-width: 0;
|
|
339
|
+
flex: 1 1 auto;
|
|
340
|
+
display: flex;
|
|
341
|
+
align-items: center;
|
|
342
|
+
gap: 10px;
|
|
343
|
+
color: #333130;
|
|
344
|
+
font-size: 13px;
|
|
345
|
+
}
|
|
346
|
+
.active-command {
|
|
347
|
+
min-width: 120px;
|
|
348
|
+
flex: 1 1 auto;
|
|
349
|
+
overflow: hidden;
|
|
350
|
+
white-space: nowrap;
|
|
351
|
+
text-overflow: ellipsis;
|
|
352
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
353
|
+
color: #666360;
|
|
354
|
+
}
|
|
355
|
+
.toolbar-actions {
|
|
356
|
+
flex: 0 0 auto;
|
|
357
|
+
display: flex;
|
|
358
|
+
align-items: center;
|
|
359
|
+
gap: 8px;
|
|
360
|
+
}
|
|
361
|
+
.toolbar-select {
|
|
362
|
+
height: 32px;
|
|
363
|
+
border: 1px solid #d0ccc5;
|
|
364
|
+
background: #ffffff;
|
|
365
|
+
color: #333130;
|
|
366
|
+
border-radius: 6px;
|
|
367
|
+
padding: 0 8px;
|
|
368
|
+
outline: none;
|
|
369
|
+
}
|
|
370
|
+
.terminal-session-select {
|
|
371
|
+
flex: 0 1 360px;
|
|
372
|
+
width: min(34vw, 360px);
|
|
373
|
+
min-width: 190px;
|
|
374
|
+
}
|
|
375
|
+
.terminal-page,
|
|
376
|
+
.terminal-wrap,
|
|
377
|
+
#terminal {
|
|
378
|
+
width: 100%;
|
|
379
|
+
}
|
|
380
|
+
.terminal-wrap {
|
|
381
|
+
min-width: 0;
|
|
382
|
+
min-height: 0;
|
|
383
|
+
display: grid;
|
|
384
|
+
grid-template-rows: minmax(0, 1fr);
|
|
385
|
+
padding: 8px 14px 8px 8px;
|
|
386
|
+
position: relative;
|
|
387
|
+
overflow: hidden;
|
|
388
|
+
background: #ffffff;
|
|
389
|
+
}
|
|
390
|
+
#terminal {
|
|
391
|
+
min-width: 0;
|
|
392
|
+
min-height: 0;
|
|
393
|
+
height: 100%;
|
|
394
|
+
width: 100%;
|
|
395
|
+
overflow: hidden;
|
|
396
|
+
background: #ffffff;
|
|
397
|
+
}
|
|
398
|
+
#terminal .xterm {
|
|
399
|
+
min-width: 0;
|
|
400
|
+
height: 100%;
|
|
401
|
+
width: 100%;
|
|
402
|
+
}
|
|
403
|
+
.chat-panel {
|
|
404
|
+
min-width: 0;
|
|
405
|
+
min-height: 0;
|
|
406
|
+
height: 100%;
|
|
407
|
+
display: grid;
|
|
408
|
+
grid-template-rows: 40px minmax(0, 1fr) auto;
|
|
409
|
+
border-left: 0;
|
|
410
|
+
background: #f4f3f1;
|
|
411
|
+
overflow: hidden;
|
|
412
|
+
}
|
|
413
|
+
.chat-head {
|
|
414
|
+
display: flex;
|
|
415
|
+
align-items: center;
|
|
416
|
+
justify-content: space-between;
|
|
417
|
+
gap: 10px;
|
|
418
|
+
padding: 0 12px;
|
|
419
|
+
border-bottom: 1px solid #e0ddd8;
|
|
420
|
+
}
|
|
421
|
+
.chat-title {
|
|
422
|
+
color: #555250;
|
|
423
|
+
font-size: 12px;
|
|
424
|
+
font-weight: 650;
|
|
425
|
+
text-transform: uppercase;
|
|
426
|
+
letter-spacing: 0.04em;
|
|
427
|
+
}
|
|
428
|
+
.chat-head-actions {
|
|
429
|
+
display: flex;
|
|
430
|
+
align-items: center;
|
|
431
|
+
gap: 8px;
|
|
432
|
+
min-width: 0;
|
|
433
|
+
}
|
|
434
|
+
.chat-meta {
|
|
435
|
+
min-width: 0;
|
|
436
|
+
overflow: hidden;
|
|
437
|
+
white-space: nowrap;
|
|
438
|
+
text-overflow: ellipsis;
|
|
439
|
+
color: #888480;
|
|
440
|
+
font-size: 12px;
|
|
441
|
+
}
|
|
442
|
+
.chat-clear {
|
|
443
|
+
width: auto;
|
|
444
|
+
padding: 4px 7px;
|
|
445
|
+
font-size: 12px;
|
|
446
|
+
}
|
|
447
|
+
.chat-messages {
|
|
448
|
+
min-height: 0;
|
|
449
|
+
overflow: auto;
|
|
450
|
+
display: flex;
|
|
451
|
+
flex-direction: column;
|
|
452
|
+
gap: 8px;
|
|
453
|
+
padding: 10px;
|
|
454
|
+
}
|
|
455
|
+
.chat-empty {
|
|
456
|
+
color: #999690;
|
|
457
|
+
padding: 8px 2px;
|
|
458
|
+
font-size: 13px;
|
|
459
|
+
}
|
|
460
|
+
.chat-message {
|
|
461
|
+
max-width: 100%;
|
|
462
|
+
border: 1px solid #e0ddd8;
|
|
463
|
+
border-radius: 7px;
|
|
464
|
+
background: #ffffff;
|
|
465
|
+
padding: 8px 9px;
|
|
466
|
+
}
|
|
467
|
+
.chat-message.user {
|
|
468
|
+
align-self: flex-end;
|
|
469
|
+
background: #e8f0e4;
|
|
470
|
+
border-color: #9cc498;
|
|
471
|
+
}
|
|
472
|
+
.chat-message.error {
|
|
473
|
+
border-color: #e0aaa4;
|
|
474
|
+
background: #fff5f3;
|
|
475
|
+
}
|
|
476
|
+
.chat-message-role {
|
|
477
|
+
margin-bottom: 4px;
|
|
478
|
+
color: #77736f;
|
|
479
|
+
font-size: 11px;
|
|
480
|
+
font-weight: 650;
|
|
481
|
+
text-transform: uppercase;
|
|
482
|
+
letter-spacing: 0.04em;
|
|
483
|
+
}
|
|
484
|
+
.chat-message-text {
|
|
485
|
+
color: #333130;
|
|
486
|
+
font-size: 13px;
|
|
487
|
+
line-height: 1.4;
|
|
488
|
+
white-space: pre-wrap;
|
|
489
|
+
word-break: break-word;
|
|
490
|
+
}
|
|
491
|
+
.chat-message-task {
|
|
492
|
+
margin-top: 8px;
|
|
493
|
+
padding-top: 7px;
|
|
494
|
+
border-top: 1px solid #ececec;
|
|
495
|
+
color: #66615c;
|
|
496
|
+
font-size: 12px;
|
|
497
|
+
line-height: 1.35;
|
|
498
|
+
}
|
|
499
|
+
.memory-proposal {
|
|
500
|
+
margin-top: 9px;
|
|
501
|
+
padding-top: 8px;
|
|
502
|
+
border-top: 1px solid #e0ddd8;
|
|
503
|
+
}
|
|
504
|
+
.memory-proposal-title {
|
|
505
|
+
color: #4b5a42;
|
|
506
|
+
font-size: 12px;
|
|
507
|
+
font-weight: 650;
|
|
508
|
+
}
|
|
509
|
+
.memory-proposal-text {
|
|
510
|
+
margin-top: 5px;
|
|
511
|
+
color: #333130;
|
|
512
|
+
font-size: 12px;
|
|
513
|
+
line-height: 1.4;
|
|
514
|
+
white-space: pre-wrap;
|
|
515
|
+
}
|
|
516
|
+
.memory-proposal-actions {
|
|
517
|
+
display: flex;
|
|
518
|
+
gap: 6px;
|
|
519
|
+
margin-top: 8px;
|
|
520
|
+
}
|
|
521
|
+
.memory-proposal-actions .btn {
|
|
522
|
+
width: auto;
|
|
523
|
+
padding: 5px 8px;
|
|
524
|
+
font-size: 12px;
|
|
525
|
+
}
|
|
526
|
+
.memory-proposal-status {
|
|
527
|
+
margin-top: 6px;
|
|
528
|
+
color: #77736f;
|
|
529
|
+
font-size: 12px;
|
|
530
|
+
}
|
|
531
|
+
.cm-panel {
|
|
532
|
+
min-width: 0;
|
|
533
|
+
min-height: 0;
|
|
534
|
+
height: 100%;
|
|
535
|
+
display: grid;
|
|
536
|
+
grid-template-rows: 40px minmax(0, 1fr);
|
|
537
|
+
border-top: 0;
|
|
538
|
+
background: #f7f6f4;
|
|
539
|
+
overflow: hidden;
|
|
540
|
+
}
|
|
541
|
+
.activity-panel {
|
|
542
|
+
min-width: 0;
|
|
543
|
+
min-height: 0;
|
|
544
|
+
height: 100%;
|
|
545
|
+
display: grid;
|
|
546
|
+
grid-template-rows: 40px 38px minmax(0, 1fr);
|
|
547
|
+
background: #f7f6f4;
|
|
548
|
+
overflow: hidden;
|
|
549
|
+
}
|
|
550
|
+
.activity-head {
|
|
551
|
+
display: flex;
|
|
552
|
+
align-items: center;
|
|
553
|
+
justify-content: space-between;
|
|
554
|
+
gap: 10px;
|
|
555
|
+
padding: 0 12px;
|
|
556
|
+
border-bottom: 1px solid #e0ddd8;
|
|
557
|
+
}
|
|
558
|
+
.activity-title {
|
|
559
|
+
color: #555250;
|
|
560
|
+
font-size: 12px;
|
|
561
|
+
font-weight: 650;
|
|
562
|
+
text-transform: uppercase;
|
|
563
|
+
letter-spacing: 0.04em;
|
|
564
|
+
}
|
|
565
|
+
.activity-controls {
|
|
566
|
+
min-width: 0;
|
|
567
|
+
display: flex;
|
|
568
|
+
align-items: center;
|
|
569
|
+
gap: 8px;
|
|
570
|
+
}
|
|
571
|
+
.activity-filter {
|
|
572
|
+
height: 28px;
|
|
573
|
+
min-width: 112px;
|
|
574
|
+
border: 1px solid #d9d5cf;
|
|
575
|
+
border-radius: 6px;
|
|
576
|
+
background: #fff;
|
|
577
|
+
color: #343230;
|
|
578
|
+
font-size: 12px;
|
|
579
|
+
padding: 0 8px;
|
|
580
|
+
}
|
|
581
|
+
.activity-search {
|
|
582
|
+
min-width: 160px;
|
|
583
|
+
}
|
|
584
|
+
.activity-refresh {
|
|
585
|
+
width: auto;
|
|
586
|
+
padding: 4px 7px;
|
|
587
|
+
font-size: 12px;
|
|
588
|
+
}
|
|
589
|
+
.activity-overview {
|
|
590
|
+
min-width: 0;
|
|
591
|
+
display: flex;
|
|
592
|
+
align-items: center;
|
|
593
|
+
gap: 8px;
|
|
594
|
+
overflow-x: auto;
|
|
595
|
+
padding: 5px 12px;
|
|
596
|
+
border-bottom: 1px solid #e0ddd8;
|
|
597
|
+
}
|
|
598
|
+
.activity-metric {
|
|
599
|
+
flex: 0 0 auto;
|
|
600
|
+
display: inline-flex;
|
|
601
|
+
align-items: baseline;
|
|
602
|
+
gap: 5px;
|
|
603
|
+
border: 1px solid #e0ddd8;
|
|
604
|
+
border-radius: 999px;
|
|
605
|
+
background: #fff;
|
|
606
|
+
padding: 4px 8px;
|
|
607
|
+
}
|
|
608
|
+
.activity-metric-label {
|
|
609
|
+
color: #77736f;
|
|
610
|
+
font-size: 11px;
|
|
611
|
+
font-weight: 650;
|
|
612
|
+
text-transform: uppercase;
|
|
613
|
+
letter-spacing: 0.04em;
|
|
614
|
+
}
|
|
615
|
+
.activity-metric-value {
|
|
616
|
+
color: #333130;
|
|
617
|
+
font-size: 12px;
|
|
618
|
+
font-weight: 700;
|
|
619
|
+
}
|
|
620
|
+
.activity-body {
|
|
621
|
+
min-width: 0;
|
|
622
|
+
min-height: 0;
|
|
623
|
+
display: grid;
|
|
624
|
+
grid-template-columns: minmax(280px, 0.9fr) minmax(360px, 1.1fr);
|
|
625
|
+
overflow: hidden;
|
|
626
|
+
}
|
|
627
|
+
.activity-items {
|
|
628
|
+
min-height: 0;
|
|
629
|
+
overflow: auto;
|
|
630
|
+
padding: 10px;
|
|
631
|
+
border-right: 1px solid #e0ddd8;
|
|
632
|
+
}
|
|
633
|
+
.activity-empty {
|
|
634
|
+
color: #999690;
|
|
635
|
+
padding: 8px 2px;
|
|
636
|
+
font-size: 13px;
|
|
637
|
+
}
|
|
638
|
+
.activity-group + .activity-group {
|
|
639
|
+
margin-top: 12px;
|
|
640
|
+
}
|
|
641
|
+
.activity-group-head {
|
|
642
|
+
display: flex;
|
|
643
|
+
align-items: center;
|
|
644
|
+
justify-content: space-between;
|
|
645
|
+
gap: 8px;
|
|
646
|
+
margin: 2px 2px 7px;
|
|
647
|
+
color: #77736f;
|
|
648
|
+
font-size: 11px;
|
|
649
|
+
font-weight: 650;
|
|
650
|
+
text-transform: uppercase;
|
|
651
|
+
letter-spacing: 0.04em;
|
|
652
|
+
}
|
|
653
|
+
.activity-group-count {
|
|
654
|
+
color: #999690;
|
|
655
|
+
font-weight: 600;
|
|
656
|
+
}
|
|
657
|
+
.activity-item {
|
|
658
|
+
border: 1px solid #e0ddd8;
|
|
659
|
+
border-radius: 7px;
|
|
660
|
+
background: #ffffff;
|
|
661
|
+
padding: 10px;
|
|
662
|
+
text-align: left;
|
|
663
|
+
width: 100%;
|
|
664
|
+
display: block;
|
|
665
|
+
cursor: pointer;
|
|
666
|
+
color: inherit;
|
|
667
|
+
font: inherit;
|
|
668
|
+
}
|
|
669
|
+
.activity-item + .activity-item {
|
|
670
|
+
margin-top: 8px;
|
|
671
|
+
}
|
|
672
|
+
.activity-item:hover,
|
|
673
|
+
.activity-item.selected {
|
|
674
|
+
border-color: #b8b1a8;
|
|
675
|
+
background: #fbfaf8;
|
|
676
|
+
}
|
|
677
|
+
.activity-item-head {
|
|
678
|
+
display: flex;
|
|
679
|
+
align-items: center;
|
|
680
|
+
justify-content: space-between;
|
|
681
|
+
gap: 8px;
|
|
682
|
+
margin-bottom: 6px;
|
|
683
|
+
}
|
|
684
|
+
.activity-item-title {
|
|
685
|
+
min-width: 0;
|
|
686
|
+
overflow: hidden;
|
|
687
|
+
white-space: nowrap;
|
|
688
|
+
text-overflow: ellipsis;
|
|
689
|
+
color: #333130;
|
|
690
|
+
font-size: 13px;
|
|
691
|
+
font-weight: 650;
|
|
692
|
+
}
|
|
693
|
+
.activity-item-meta {
|
|
694
|
+
color: #77736f;
|
|
695
|
+
font-size: 11px;
|
|
696
|
+
overflow: hidden;
|
|
697
|
+
white-space: nowrap;
|
|
698
|
+
text-overflow: ellipsis;
|
|
699
|
+
}
|
|
700
|
+
.activity-item-summary {
|
|
701
|
+
margin-top: 6px;
|
|
702
|
+
color: #555250;
|
|
703
|
+
font-size: 13px;
|
|
704
|
+
line-height: 1.4;
|
|
705
|
+
white-space: pre-wrap;
|
|
706
|
+
word-break: break-word;
|
|
707
|
+
}
|
|
708
|
+
.activity-detail {
|
|
709
|
+
min-width: 0;
|
|
710
|
+
min-height: 0;
|
|
711
|
+
overflow: auto;
|
|
712
|
+
padding: 14px;
|
|
713
|
+
}
|
|
714
|
+
.activity-detail-empty {
|
|
715
|
+
color: #999690;
|
|
716
|
+
font-size: 13px;
|
|
717
|
+
padding: 4px 2px;
|
|
718
|
+
}
|
|
719
|
+
.activity-detail-card {
|
|
720
|
+
max-width: 920px;
|
|
721
|
+
}
|
|
722
|
+
.activity-detail-head {
|
|
723
|
+
display: flex;
|
|
724
|
+
align-items: flex-start;
|
|
725
|
+
justify-content: space-between;
|
|
726
|
+
gap: 12px;
|
|
727
|
+
}
|
|
728
|
+
.activity-detail-title {
|
|
729
|
+
color: #292725;
|
|
730
|
+
font-size: 18px;
|
|
731
|
+
font-weight: 700;
|
|
732
|
+
line-height: 1.25;
|
|
733
|
+
overflow-wrap: anywhere;
|
|
734
|
+
}
|
|
735
|
+
.activity-detail-summary {
|
|
736
|
+
margin-top: 10px;
|
|
737
|
+
color: #47433f;
|
|
738
|
+
font-size: 14px;
|
|
739
|
+
line-height: 1.45;
|
|
740
|
+
white-space: pre-wrap;
|
|
741
|
+
overflow-wrap: anywhere;
|
|
742
|
+
}
|
|
743
|
+
.activity-detail-kicker {
|
|
744
|
+
color: #77736f;
|
|
745
|
+
font-size: 12px;
|
|
746
|
+
font-weight: 650;
|
|
747
|
+
text-transform: uppercase;
|
|
748
|
+
letter-spacing: 0.04em;
|
|
749
|
+
margin-bottom: 5px;
|
|
750
|
+
}
|
|
751
|
+
.activity-detail-grid {
|
|
752
|
+
display: grid;
|
|
753
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
754
|
+
gap: 8px;
|
|
755
|
+
margin-top: 14px;
|
|
756
|
+
}
|
|
757
|
+
.activity-detail-field {
|
|
758
|
+
min-width: 0;
|
|
759
|
+
border: 1px solid #e0ddd8;
|
|
760
|
+
border-radius: 7px;
|
|
761
|
+
background: #fff;
|
|
762
|
+
padding: 9px;
|
|
763
|
+
}
|
|
764
|
+
.activity-detail-label {
|
|
765
|
+
color: #77736f;
|
|
766
|
+
font-size: 11px;
|
|
767
|
+
font-weight: 650;
|
|
768
|
+
text-transform: uppercase;
|
|
769
|
+
letter-spacing: 0.04em;
|
|
770
|
+
}
|
|
771
|
+
.activity-detail-value {
|
|
772
|
+
margin-top: 5px;
|
|
773
|
+
color: #333130;
|
|
774
|
+
font-size: 13px;
|
|
775
|
+
line-height: 1.35;
|
|
776
|
+
overflow-wrap: anywhere;
|
|
777
|
+
}
|
|
778
|
+
.activity-detail-section {
|
|
779
|
+
margin-top: 14px;
|
|
780
|
+
}
|
|
781
|
+
.activity-detail-section + .activity-detail-section {
|
|
782
|
+
padding-top: 4px;
|
|
783
|
+
}
|
|
784
|
+
.activity-detail-section-title {
|
|
785
|
+
color: #555250;
|
|
786
|
+
font-size: 12px;
|
|
787
|
+
font-weight: 650;
|
|
788
|
+
text-transform: uppercase;
|
|
789
|
+
letter-spacing: 0.04em;
|
|
790
|
+
}
|
|
791
|
+
.activity-detail-text {
|
|
792
|
+
margin-top: 6px;
|
|
793
|
+
color: #333130;
|
|
794
|
+
font-size: 14px;
|
|
795
|
+
line-height: 1.45;
|
|
796
|
+
white-space: pre-wrap;
|
|
797
|
+
overflow-wrap: anywhere;
|
|
798
|
+
}
|
|
799
|
+
.activity-timeline {
|
|
800
|
+
display: grid;
|
|
801
|
+
gap: 8px;
|
|
802
|
+
margin-top: 8px;
|
|
803
|
+
}
|
|
804
|
+
.activity-timeline-item {
|
|
805
|
+
width: 100%;
|
|
806
|
+
min-width: 0;
|
|
807
|
+
border: 1px solid #e0ddd8;
|
|
808
|
+
border-radius: 7px;
|
|
809
|
+
background: #fff;
|
|
810
|
+
color: inherit;
|
|
811
|
+
cursor: pointer;
|
|
812
|
+
font: inherit;
|
|
813
|
+
padding: 9px;
|
|
814
|
+
text-align: left;
|
|
815
|
+
}
|
|
816
|
+
.activity-timeline-item:hover {
|
|
817
|
+
border-color: #b8b1a8;
|
|
818
|
+
background: #fbfaf8;
|
|
819
|
+
}
|
|
820
|
+
.activity-timeline-row {
|
|
821
|
+
display: flex;
|
|
822
|
+
align-items: center;
|
|
823
|
+
justify-content: space-between;
|
|
824
|
+
gap: 8px;
|
|
825
|
+
}
|
|
826
|
+
.activity-timeline-title {
|
|
827
|
+
min-width: 0;
|
|
828
|
+
color: #333130;
|
|
829
|
+
font-size: 13px;
|
|
830
|
+
font-weight: 650;
|
|
831
|
+
overflow: hidden;
|
|
832
|
+
text-overflow: ellipsis;
|
|
833
|
+
white-space: nowrap;
|
|
834
|
+
}
|
|
835
|
+
.activity-timeline-time {
|
|
836
|
+
flex: 0 0 auto;
|
|
837
|
+
color: #77736f;
|
|
838
|
+
font-size: 11px;
|
|
839
|
+
}
|
|
840
|
+
.activity-timeline-summary {
|
|
841
|
+
margin-top: 5px;
|
|
842
|
+
color: #555250;
|
|
843
|
+
font-size: 12px;
|
|
844
|
+
line-height: 1.35;
|
|
845
|
+
overflow-wrap: anywhere;
|
|
846
|
+
white-space: pre-wrap;
|
|
847
|
+
}
|
|
848
|
+
.activity-timeline-empty {
|
|
849
|
+
color: #999690;
|
|
850
|
+
font-size: 13px;
|
|
851
|
+
padding: 3px 0;
|
|
852
|
+
}
|
|
853
|
+
.activity-technical {
|
|
854
|
+
margin-top: 14px;
|
|
855
|
+
}
|
|
856
|
+
.activity-technical summary {
|
|
857
|
+
color: #555250;
|
|
858
|
+
cursor: pointer;
|
|
859
|
+
font-size: 12px;
|
|
860
|
+
font-weight: 650;
|
|
861
|
+
text-transform: uppercase;
|
|
862
|
+
letter-spacing: 0.04em;
|
|
863
|
+
}
|
|
864
|
+
.activity-detail-pre {
|
|
865
|
+
margin-top: 6px;
|
|
866
|
+
padding: 10px;
|
|
867
|
+
border: 1px solid #e0ddd8;
|
|
868
|
+
border-radius: 7px;
|
|
869
|
+
background: #fff;
|
|
870
|
+
color: #333130;
|
|
871
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
872
|
+
font-size: 12px;
|
|
873
|
+
line-height: 1.45;
|
|
874
|
+
white-space: pre-wrap;
|
|
875
|
+
overflow-wrap: anywhere;
|
|
876
|
+
}
|
|
877
|
+
.cm-head {
|
|
878
|
+
display: flex;
|
|
879
|
+
align-items: center;
|
|
880
|
+
justify-content: space-between;
|
|
881
|
+
padding: 0 12px;
|
|
882
|
+
border-bottom: 1px solid #e0ddd8;
|
|
883
|
+
}
|
|
884
|
+
.cm-title {
|
|
885
|
+
color: #555250;
|
|
886
|
+
font-size: 12px;
|
|
887
|
+
font-weight: 650;
|
|
888
|
+
text-transform: uppercase;
|
|
889
|
+
letter-spacing: 0.04em;
|
|
890
|
+
}
|
|
891
|
+
.cm-items {
|
|
892
|
+
min-height: 0;
|
|
893
|
+
overflow: auto;
|
|
894
|
+
padding: 8px;
|
|
895
|
+
}
|
|
896
|
+
.cm-empty {
|
|
897
|
+
color: #999690;
|
|
898
|
+
padding: 8px 2px;
|
|
899
|
+
font-size: 13px;
|
|
900
|
+
}
|
|
901
|
+
.cm-item {
|
|
902
|
+
border: 1px solid #e0ddd8;
|
|
903
|
+
border-radius: 7px;
|
|
904
|
+
background: #ffffff;
|
|
905
|
+
padding: 8px;
|
|
906
|
+
}
|
|
907
|
+
.cm-item + .cm-item {
|
|
908
|
+
margin-top: 8px;
|
|
909
|
+
}
|
|
910
|
+
.cm-item-head {
|
|
911
|
+
display: flex;
|
|
912
|
+
align-items: center;
|
|
913
|
+
justify-content: space-between;
|
|
914
|
+
gap: 8px;
|
|
915
|
+
margin-bottom: 6px;
|
|
916
|
+
}
|
|
917
|
+
.cm-item-kind {
|
|
918
|
+
color: #555250;
|
|
919
|
+
font-size: 11px;
|
|
920
|
+
font-weight: 650;
|
|
921
|
+
text-transform: uppercase;
|
|
922
|
+
letter-spacing: 0.04em;
|
|
923
|
+
}
|
|
924
|
+
.cm-item-time {
|
|
925
|
+
color: #999690;
|
|
926
|
+
font-size: 11px;
|
|
927
|
+
white-space: nowrap;
|
|
928
|
+
}
|
|
929
|
+
.chat-form {
|
|
930
|
+
display: grid;
|
|
931
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
932
|
+
align-items: end;
|
|
933
|
+
gap: 8px;
|
|
934
|
+
padding: 8px;
|
|
935
|
+
border-top: 1px solid #e0ddd8;
|
|
936
|
+
background: #ffffff;
|
|
937
|
+
}
|
|
938
|
+
.chat-form textarea {
|
|
939
|
+
width: 100%;
|
|
940
|
+
height: 128px;
|
|
941
|
+
min-height: 96px;
|
|
942
|
+
max-height: 220px;
|
|
943
|
+
resize: vertical;
|
|
944
|
+
border: 1px solid #d0ccc5;
|
|
945
|
+
background: #ffffff;
|
|
946
|
+
color: #1a1a1a;
|
|
947
|
+
border-radius: 6px;
|
|
948
|
+
padding: 8px 9px;
|
|
949
|
+
outline: none;
|
|
950
|
+
}
|
|
951
|
+
.chat-form textarea:focus {
|
|
952
|
+
border-color: #4a8f4a;
|
|
953
|
+
}
|
|
954
|
+
.chat-form .btn {
|
|
955
|
+
width: 68px;
|
|
956
|
+
}
|
|
957
|
+
.statusbar {
|
|
958
|
+
display: flex;
|
|
959
|
+
align-items: center;
|
|
960
|
+
justify-content: space-between;
|
|
961
|
+
gap: 12px;
|
|
962
|
+
padding: 0 12px;
|
|
963
|
+
border-top: 1px solid #e0ddd8;
|
|
964
|
+
background: #f6f5f3;
|
|
965
|
+
color: #888480;
|
|
966
|
+
font-size: 12px;
|
|
967
|
+
}
|
|
968
|
+
.status-error {
|
|
969
|
+
color: #c43830;
|
|
970
|
+
}
|
|
971
|
+
.search-bar {
|
|
972
|
+
display: none;
|
|
973
|
+
position: absolute;
|
|
974
|
+
top: 6px;
|
|
975
|
+
right: 14px;
|
|
976
|
+
z-index: 10;
|
|
977
|
+
gap: 4px;
|
|
978
|
+
align-items: center;
|
|
979
|
+
background: #ffffff;
|
|
980
|
+
border: 1px solid #d0ccc5;
|
|
981
|
+
border-radius: 6px;
|
|
982
|
+
padding: 4px 6px;
|
|
983
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
984
|
+
}
|
|
985
|
+
.search-bar.open {
|
|
986
|
+
display: flex;
|
|
987
|
+
}
|
|
988
|
+
.search-bar input {
|
|
989
|
+
width: 180px;
|
|
990
|
+
height: 26px;
|
|
991
|
+
border: 1px solid #d0ccc5;
|
|
992
|
+
background: #f8f7f6;
|
|
993
|
+
color: #1a1a1a;
|
|
994
|
+
border-radius: 4px;
|
|
995
|
+
padding: 0 7px;
|
|
996
|
+
font: 12px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
997
|
+
outline: none;
|
|
998
|
+
}
|
|
999
|
+
.search-bar input:focus {
|
|
1000
|
+
border-color: #4a8f4a;
|
|
1001
|
+
}
|
|
1002
|
+
.search-bar button {
|
|
1003
|
+
height: 26px;
|
|
1004
|
+
border: 1px solid #d0ccc5;
|
|
1005
|
+
background: #f0eeeb;
|
|
1006
|
+
color: #333130;
|
|
1007
|
+
border-radius: 4px;
|
|
1008
|
+
padding: 0 7px;
|
|
1009
|
+
cursor: pointer;
|
|
1010
|
+
font-size: 12px;
|
|
1011
|
+
}
|
|
1012
|
+
.search-bar button:hover {
|
|
1013
|
+
background: #e5e2de;
|
|
1014
|
+
}
|
|
1015
|
+
@media (max-width: 1200px) {
|
|
1016
|
+
.sessions-layout {
|
|
1017
|
+
grid-template-columns: minmax(300px, 380px) minmax(0, 1fr);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
@media (max-width: 980px) {
|
|
1021
|
+
.app {
|
|
1022
|
+
grid-template-rows: auto minmax(0, 1fr) 30px;
|
|
1023
|
+
}
|
|
1024
|
+
.topbar {
|
|
1025
|
+
grid-template-columns: 1fr;
|
|
1026
|
+
align-items: stretch;
|
|
1027
|
+
padding: 10px;
|
|
1028
|
+
}
|
|
1029
|
+
.brand {
|
|
1030
|
+
min-width: 0;
|
|
1031
|
+
}
|
|
1032
|
+
.page-tabs {
|
|
1033
|
+
justify-content: flex-start;
|
|
1034
|
+
}
|
|
1035
|
+
.start-form {
|
|
1036
|
+
grid-template-columns: 1fr 1fr;
|
|
1037
|
+
}
|
|
1038
|
+
.sessions-layout {
|
|
1039
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1040
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
1041
|
+
}
|
|
1042
|
+
.start-panel {
|
|
1043
|
+
border-right: 0;
|
|
1044
|
+
border-bottom: 1px solid #e0ddd8;
|
|
1045
|
+
}
|
|
1046
|
+
.start-panel-body {
|
|
1047
|
+
max-height: 280px;
|
|
1048
|
+
}
|
|
1049
|
+
.chat-form {
|
|
1050
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1051
|
+
}
|
|
1052
|
+
.chat-form .btn {
|
|
1053
|
+
width: 100%;
|
|
1054
|
+
}
|
|
1055
|
+
.main {
|
|
1056
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
1057
|
+
}
|
|
1058
|
+
.session-toolbar {
|
|
1059
|
+
align-items: stretch;
|
|
1060
|
+
flex-direction: column;
|
|
1061
|
+
height: auto;
|
|
1062
|
+
padding: 8px;
|
|
1063
|
+
}
|
|
1064
|
+
.active-summary,
|
|
1065
|
+
.toolbar-actions {
|
|
1066
|
+
width: 100%;
|
|
1067
|
+
}
|
|
1068
|
+
.terminal-session-select {
|
|
1069
|
+
width: 100%;
|
|
1070
|
+
min-width: 0;
|
|
1071
|
+
}
|
|
1072
|
+
.activity-head {
|
|
1073
|
+
height: auto;
|
|
1074
|
+
align-items: stretch;
|
|
1075
|
+
flex-direction: column;
|
|
1076
|
+
padding: 8px;
|
|
1077
|
+
}
|
|
1078
|
+
.activity-panel {
|
|
1079
|
+
grid-template-rows: auto auto minmax(0, 1fr);
|
|
1080
|
+
}
|
|
1081
|
+
.activity-controls {
|
|
1082
|
+
width: 100%;
|
|
1083
|
+
flex-wrap: wrap;
|
|
1084
|
+
}
|
|
1085
|
+
.activity-filter {
|
|
1086
|
+
flex: 1 1 130px;
|
|
1087
|
+
min-width: 0;
|
|
1088
|
+
}
|
|
1089
|
+
.activity-body {
|
|
1090
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1091
|
+
grid-template-rows: minmax(180px, 0.55fr) minmax(220px, 0.45fr);
|
|
1092
|
+
}
|
|
1093
|
+
.activity-items {
|
|
1094
|
+
border-right: 0;
|
|
1095
|
+
border-bottom: 1px solid #e0ddd8;
|
|
1096
|
+
}
|
|
1097
|
+
.activity-detail-grid {
|
|
1098
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
</style>
|
|
1102
|
+
</head>
|
|
1103
|
+
<body>
|
|
1104
|
+
<div class="app">
|
|
1105
|
+
<header class="topbar">
|
|
1106
|
+
<div class="brand">
|
|
1107
|
+
<div class="brand-title">Akemon Workbench</div>
|
|
1108
|
+
<div class="brand-subtitle">local</div>
|
|
1109
|
+
</div>
|
|
1110
|
+
<nav class="page-tabs" id="page-tabs" aria-label="Workbench pages">
|
|
1111
|
+
<button class="page-tab active" type="button" data-page-tab="terminal">Terminal</button>
|
|
1112
|
+
<button class="page-tab" type="button" data-page-tab="sessions">Sessions</button>
|
|
1113
|
+
<button class="page-tab" type="button" data-page-tab="activity">Activity</button>
|
|
1114
|
+
<button class="page-tab" type="button" data-page-tab="akemon">Akemon</button>
|
|
1115
|
+
<button class="page-tab" type="button" data-page-tab="cm">CM</button>
|
|
1116
|
+
</nav>
|
|
1117
|
+
<div class="auth-strip" id="auth-strip">
|
|
1118
|
+
<input class="token-input" id="token-input" placeholder="owner token">
|
|
1119
|
+
<button class="btn" id="token-save" type="button">Use</button>
|
|
1120
|
+
<span class="auth-message" id="auth-message"></span>
|
|
1121
|
+
</div>
|
|
1122
|
+
</header>
|
|
1123
|
+
|
|
1124
|
+
<div class="layout">
|
|
1125
|
+
<section class="page-view active terminal-page" data-page-panel="terminal">
|
|
1126
|
+
<main class="main">
|
|
1127
|
+
<div class="session-toolbar">
|
|
1128
|
+
<div class="active-summary">
|
|
1129
|
+
<select class="toolbar-select terminal-session-select" id="terminal-session-select" title="Session">
|
|
1130
|
+
<option value="">No sessions</option>
|
|
1131
|
+
</select>
|
|
1132
|
+
<span class="status-pill" id="active-status">idle</span>
|
|
1133
|
+
<span class="active-command" id="active-command">No active session</span>
|
|
1134
|
+
</div>
|
|
1135
|
+
<div class="toolbar-actions">
|
|
1136
|
+
<select class="toolbar-select" id="active-input-mode" title="Input mode">
|
|
1137
|
+
<option value="line">line</option>
|
|
1138
|
+
<option value="tui">tui</option>
|
|
1139
|
+
</select>
|
|
1140
|
+
<button class="btn" id="resize" type="button">Resize</button>
|
|
1141
|
+
<button class="btn btn-danger" id="stop" type="button">Stop</button>
|
|
1142
|
+
</div>
|
|
1143
|
+
</div>
|
|
1144
|
+
<div class="terminal-wrap">
|
|
1145
|
+
<div class="search-bar" id="search-bar">
|
|
1146
|
+
<input id="search-input" placeholder="Search...">
|
|
1147
|
+
<button id="search-prev" type="button">▲</button>
|
|
1148
|
+
<button id="search-next" type="button">▼</button>
|
|
1149
|
+
<button id="search-close" type="button">✕</button>
|
|
1150
|
+
</div>
|
|
1151
|
+
<div id="terminal"></div>
|
|
1152
|
+
</div>
|
|
1153
|
+
</main>
|
|
1154
|
+
</section>
|
|
1155
|
+
|
|
1156
|
+
<section class="page-view sessions-page" data-page-panel="sessions">
|
|
1157
|
+
<div class="sessions-layout">
|
|
1158
|
+
<section class="start-panel">
|
|
1159
|
+
<div class="sidebar-head">
|
|
1160
|
+
<div class="sidebar-title">New Session</div>
|
|
1161
|
+
</div>
|
|
1162
|
+
<div class="start-panel-body">
|
|
1163
|
+
<form class="start-form" id="start-form">
|
|
1164
|
+
<div class="field">
|
|
1165
|
+
<select id="tool">
|
|
1166
|
+
<option value="codex">codex</option>
|
|
1167
|
+
<option value="claude">claude</option>
|
|
1168
|
+
<option value="cursor">cursor</option>
|
|
1169
|
+
<option value="shell">shell</option>
|
|
1170
|
+
<option value="custom">custom command</option>
|
|
1171
|
+
</select>
|
|
1172
|
+
</div>
|
|
1173
|
+
<div class="field">
|
|
1174
|
+
<select id="input-mode">
|
|
1175
|
+
<option value="auto">auto input</option>
|
|
1176
|
+
<option value="line">line</option>
|
|
1177
|
+
<option value="tui">tui</option>
|
|
1178
|
+
</select>
|
|
1179
|
+
</div>
|
|
1180
|
+
<div class="field field-wide"><input id="command" placeholder="command"></div>
|
|
1181
|
+
<div class="field field-wide"><input id="args" placeholder="args"></div>
|
|
1182
|
+
<div class="field field-wide"><input id="workdir" placeholder="workdir"></div>
|
|
1183
|
+
<div class="field field-wide"><input id="label" placeholder="label"></div>
|
|
1184
|
+
<label class="checkbox-field"><input type="checkbox" id="allow-outside"> outside</label>
|
|
1185
|
+
<button class="btn btn-primary" type="submit">Start</button>
|
|
1186
|
+
</form>
|
|
1187
|
+
</div>
|
|
1188
|
+
</section>
|
|
1189
|
+
<section class="sidebar">
|
|
1190
|
+
<div class="sidebar-head">
|
|
1191
|
+
<div class="sidebar-title">Sessions</div>
|
|
1192
|
+
<div class="sidebar-actions">
|
|
1193
|
+
<button class="btn" id="session-history-toggle" type="button">History</button>
|
|
1194
|
+
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
1195
|
+
</div>
|
|
1196
|
+
</div>
|
|
1197
|
+
<div class="session-list" id="session-list">
|
|
1198
|
+
<div class="empty">No sessions.</div>
|
|
1199
|
+
</div>
|
|
1200
|
+
</section>
|
|
1201
|
+
</div>
|
|
1202
|
+
</section>
|
|
1203
|
+
|
|
1204
|
+
<section class="page-view activity-page" data-page-panel="activity">
|
|
1205
|
+
<section class="activity-panel">
|
|
1206
|
+
<div class="activity-head">
|
|
1207
|
+
<div class="activity-title">Activity</div>
|
|
1208
|
+
<div class="activity-controls">
|
|
1209
|
+
<select class="activity-filter" id="activity-status-filter" aria-label="Activity status">
|
|
1210
|
+
<option value="">All status</option>
|
|
1211
|
+
<option value="pending">Pending</option>
|
|
1212
|
+
<option value="running">Running</option>
|
|
1213
|
+
<option value="waiting">Waiting</option>
|
|
1214
|
+
<option value="succeeded">Succeeded</option>
|
|
1215
|
+
<option value="failed">Failed</option>
|
|
1216
|
+
<option value="info">Info</option>
|
|
1217
|
+
</select>
|
|
1218
|
+
<select class="activity-filter" id="activity-route-filter" aria-label="Activity route">
|
|
1219
|
+
<option value="">All routes</option>
|
|
1220
|
+
<option value="core_cm">Core CM</option>
|
|
1221
|
+
<option value="interactive-session">Interactive</option>
|
|
1222
|
+
<option value="software-agent">Software Agent</option>
|
|
1223
|
+
<option value="relay-order">Relay Order</option>
|
|
1224
|
+
<option value="user-task">User Task</option>
|
|
1225
|
+
<option value="relay-task">Relay Task</option>
|
|
1226
|
+
<option value="memory_cm">Memory CM</option>
|
|
1227
|
+
</select>
|
|
1228
|
+
<input class="activity-filter activity-search" id="activity-search" aria-label="Search activity" placeholder="Search">
|
|
1229
|
+
<button class="btn activity-refresh" id="activity-refresh" type="button">Refresh</button>
|
|
1230
|
+
</div>
|
|
1231
|
+
</div>
|
|
1232
|
+
<div class="activity-overview" id="activity-overview"></div>
|
|
1233
|
+
<div class="activity-body">
|
|
1234
|
+
<div class="activity-items" id="activity-items">
|
|
1235
|
+
<div class="activity-empty">No activity yet.</div>
|
|
1236
|
+
</div>
|
|
1237
|
+
<div class="activity-detail" id="activity-detail">
|
|
1238
|
+
<div class="activity-detail-empty">No task selected.</div>
|
|
1239
|
+
</div>
|
|
1240
|
+
</div>
|
|
1241
|
+
</section>
|
|
1242
|
+
</section>
|
|
1243
|
+
|
|
1244
|
+
<section class="page-view akemon-page" data-page-panel="akemon">
|
|
1245
|
+
<aside class="chat-panel">
|
|
1246
|
+
<div class="chat-head">
|
|
1247
|
+
<div class="chat-title">Akemon</div>
|
|
1248
|
+
<div class="chat-head-actions">
|
|
1249
|
+
<div class="chat-meta" id="chat-meta"></div>
|
|
1250
|
+
<button class="btn chat-clear" id="chat-clear" type="button">Clear</button>
|
|
1251
|
+
</div>
|
|
1252
|
+
</div>
|
|
1253
|
+
<div class="chat-messages" id="chat-messages">
|
|
1254
|
+
<div class="chat-empty">No messages.</div>
|
|
1255
|
+
</div>
|
|
1256
|
+
<form class="chat-form" id="chat-form">
|
|
1257
|
+
<textarea id="chat-input" placeholder="Message Akemon"></textarea>
|
|
1258
|
+
<button class="btn btn-primary" id="chat-send" type="submit">Send</button>
|
|
1259
|
+
</form>
|
|
1260
|
+
</aside>
|
|
1261
|
+
</section>
|
|
1262
|
+
|
|
1263
|
+
<section class="page-view cm-page" data-page-panel="cm">
|
|
1264
|
+
<section class="cm-panel">
|
|
1265
|
+
<div class="cm-head">
|
|
1266
|
+
<div class="cm-title">CM</div>
|
|
1267
|
+
</div>
|
|
1268
|
+
<div class="cm-items" id="cm-items">
|
|
1269
|
+
<div class="cm-empty">No CM items yet.</div>
|
|
1270
|
+
</div>
|
|
1271
|
+
</section>
|
|
1272
|
+
</section>
|
|
1273
|
+
</div>
|
|
1274
|
+
|
|
1275
|
+
<footer class="statusbar">
|
|
1276
|
+
<span id="status-text">Disconnected</span>
|
|
1277
|
+
<span id="size-text">0x0</span>
|
|
1278
|
+
<span id="debug-text"></span>
|
|
1279
|
+
</footer>
|
|
1280
|
+
</div>
|
|
1281
|
+
|
|
1282
|
+
<script src="/vendor/xterm/xterm.js"></script>
|
|
1283
|
+
<script src="/vendor/xterm/addon-fit.js"></script>
|
|
1284
|
+
<script src="/vendor/xterm/addon-web-links.js"></script>
|
|
1285
|
+
<script src="/vendor/xterm/addon-search.js"></script>
|
|
1286
|
+
<script>
|
|
1287
|
+
const TOKEN_STORAGE_KEY = "akemon.workbench.ownerToken";
|
|
1288
|
+
const PAGE_STORAGE_KEY = "akemon.workbench.activePage";
|
|
1289
|
+
const CHAT_CONVERSATION_KEY = "akemon.workbench.chatConversation";
|
|
1290
|
+
const CHAT_HISTORY_PREFIX = "akemon.workbench.chatHistory.";
|
|
1291
|
+
const CHAT_HISTORY_LIMIT = 50;
|
|
1292
|
+
const ACTIVE_CHAT_TASK_PREFIX = "akemon.workbench.activeChatTask.";
|
|
1293
|
+
const CM_ITEMS_PREFIX = "akemon.workbench.cmItems.";
|
|
1294
|
+
const CM_ITEMS_LIMIT = 30;
|
|
1295
|
+
const FONT_SIZE = 13;
|
|
1296
|
+
const LINE_HEIGHT = 1.25;
|
|
1297
|
+
const CHAT_REQUEST_TIMEOUT_MS = 8 * 60 * 1000 + 15 * 1000;
|
|
1298
|
+
const CHAT_ACTIVE_TASK_RECOVERY_MS = 30 * 60 * 1000;
|
|
1299
|
+
const TERMINAL_RESPONSE_BUFFER_LIMIT = 160;
|
|
1300
|
+
|
|
1301
|
+
const state = {
|
|
1302
|
+
token: "",
|
|
1303
|
+
sessions: [],
|
|
1304
|
+
activeId: "",
|
|
1305
|
+
streamAbort: null,
|
|
1306
|
+
streamRetryTimer: 0,
|
|
1307
|
+
term: null,
|
|
1308
|
+
fitAddon: null,
|
|
1309
|
+
searchAddon: null,
|
|
1310
|
+
lastResize: { cols: 100, rows: 30 },
|
|
1311
|
+
sessionItems: new Map(),
|
|
1312
|
+
lastCursorControl: "",
|
|
1313
|
+
terminalResponseBuffer: "",
|
|
1314
|
+
wheelScrollRemainder: 0,
|
|
1315
|
+
agentName: "",
|
|
1316
|
+
chatConversationId: "",
|
|
1317
|
+
chatHistory: [],
|
|
1318
|
+
cmItems: [],
|
|
1319
|
+
activityTasks: [],
|
|
1320
|
+
activitySelectedTaskId: "",
|
|
1321
|
+
activitySelectedTask: null,
|
|
1322
|
+
activityTimelineTasks: [],
|
|
1323
|
+
activityTimelineParentId: "",
|
|
1324
|
+
activityTimelineRequestId: 0,
|
|
1325
|
+
chatBusy: false,
|
|
1326
|
+
activeChatTaskId: "",
|
|
1327
|
+
activeChatRequestId: "",
|
|
1328
|
+
completedChatTaskIds: new Set(),
|
|
1329
|
+
chatComposing: false,
|
|
1330
|
+
chatCompositionEndedAt: 0,
|
|
1331
|
+
pageUnloading: false,
|
|
1332
|
+
showSessionHistory: false,
|
|
1333
|
+
activePage: "terminal",
|
|
1334
|
+
resizeFrame: 0,
|
|
1335
|
+
activationRequestId: 0,
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
const el = {
|
|
1339
|
+
pageTabs: Array.from(document.querySelectorAll("[data-page-tab]")),
|
|
1340
|
+
pagePanels: Array.from(document.querySelectorAll("[data-page-panel]")),
|
|
1341
|
+
authStrip: document.getElementById("auth-strip"),
|
|
1342
|
+
tokenInput: document.getElementById("token-input"),
|
|
1343
|
+
tokenSave: document.getElementById("token-save"),
|
|
1344
|
+
authMessage: document.getElementById("auth-message"),
|
|
1345
|
+
startForm: document.getElementById("start-form"),
|
|
1346
|
+
tool: document.getElementById("tool"),
|
|
1347
|
+
inputMode: document.getElementById("input-mode"),
|
|
1348
|
+
command: document.getElementById("command"),
|
|
1349
|
+
args: document.getElementById("args"),
|
|
1350
|
+
workdir: document.getElementById("workdir"),
|
|
1351
|
+
label: document.getElementById("label"),
|
|
1352
|
+
allowOutside: document.getElementById("allow-outside"),
|
|
1353
|
+
list: document.getElementById("session-list"),
|
|
1354
|
+
terminal: document.getElementById("terminal"),
|
|
1355
|
+
statusText: document.getElementById("status-text"),
|
|
1356
|
+
sizeText: document.getElementById("size-text"),
|
|
1357
|
+
debugText: document.getElementById("debug-text"),
|
|
1358
|
+
terminalSessionSelect: document.getElementById("terminal-session-select"),
|
|
1359
|
+
activeStatus: document.getElementById("active-status"),
|
|
1360
|
+
activeCommand: document.getElementById("active-command"),
|
|
1361
|
+
sessionHistoryToggle: document.getElementById("session-history-toggle"),
|
|
1362
|
+
refresh: document.getElementById("refresh"),
|
|
1363
|
+
resize: document.getElementById("resize"),
|
|
1364
|
+
stop: document.getElementById("stop"),
|
|
1365
|
+
activeInputMode: document.getElementById("active-input-mode"),
|
|
1366
|
+
searchBar: document.getElementById("search-bar"),
|
|
1367
|
+
searchInput: document.getElementById("search-input"),
|
|
1368
|
+
searchPrev: document.getElementById("search-prev"),
|
|
1369
|
+
searchNext: document.getElementById("search-next"),
|
|
1370
|
+
searchClose: document.getElementById("search-close"),
|
|
1371
|
+
activityItems: document.getElementById("activity-items"),
|
|
1372
|
+
activityDetail: document.getElementById("activity-detail"),
|
|
1373
|
+
activityStatusFilter: document.getElementById("activity-status-filter"),
|
|
1374
|
+
activityRouteFilter: document.getElementById("activity-route-filter"),
|
|
1375
|
+
activitySearch: document.getElementById("activity-search"),
|
|
1376
|
+
activityOverview: document.getElementById("activity-overview"),
|
|
1377
|
+
activityRefresh: document.getElementById("activity-refresh"),
|
|
1378
|
+
cmItems: document.getElementById("cm-items"),
|
|
1379
|
+
chatMeta: document.getElementById("chat-meta"),
|
|
1380
|
+
chatMessages: document.getElementById("chat-messages"),
|
|
1381
|
+
chatForm: document.getElementById("chat-form"),
|
|
1382
|
+
chatInput: document.getElementById("chat-input"),
|
|
1383
|
+
chatSend: document.getElementById("chat-send"),
|
|
1384
|
+
chatClear: document.getElementById("chat-clear"),
|
|
1385
|
+
};
|
|
1386
|
+
|
|
1387
|
+
function initToken() {
|
|
1388
|
+
const hash = new URLSearchParams(location.hash.slice(1));
|
|
1389
|
+
const tokenFromHash = hash.get("token");
|
|
1390
|
+
if (tokenFromHash) {
|
|
1391
|
+
localStorage.setItem(TOKEN_STORAGE_KEY, tokenFromHash);
|
|
1392
|
+
history.replaceState(null, "", location.pathname + location.search);
|
|
1393
|
+
}
|
|
1394
|
+
state.token = localStorage.getItem(TOKEN_STORAGE_KEY) || "";
|
|
1395
|
+
setAuthVisible(!state.token, state.token ? "" : "Owner token required");
|
|
1396
|
+
if (!state.token) setStatus("Owner token required", true);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function setStatus(text, error) {
|
|
1400
|
+
el.statusText.textContent = text;
|
|
1401
|
+
el.statusText.classList.toggle("status-error", error === true);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function setAuthVisible(show, message) {
|
|
1405
|
+
el.authStrip.classList.toggle("show", show);
|
|
1406
|
+
el.authMessage.textContent = message || "";
|
|
1407
|
+
if (show) el.tokenInput.focus();
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function setActivePage(page) {
|
|
1411
|
+
const nextPage = ["terminal", "sessions", "activity", "akemon", "cm"].includes(page) ? page : "terminal";
|
|
1412
|
+
state.activePage = nextPage;
|
|
1413
|
+
el.pageTabs.forEach((tab) => {
|
|
1414
|
+
const active = tab.dataset.pageTab === nextPage;
|
|
1415
|
+
tab.classList.toggle("active", active);
|
|
1416
|
+
tab.setAttribute("aria-selected", active ? "true" : "false");
|
|
1417
|
+
});
|
|
1418
|
+
el.pagePanels.forEach((panel) => {
|
|
1419
|
+
panel.classList.toggle("active", panel.dataset.pagePanel === nextPage);
|
|
1420
|
+
});
|
|
1421
|
+
try {
|
|
1422
|
+
localStorage.setItem(PAGE_STORAGE_KEY, nextPage);
|
|
1423
|
+
} catch {
|
|
1424
|
+
// Page choice is best-effort UI state.
|
|
1425
|
+
}
|
|
1426
|
+
if (nextPage === "terminal") {
|
|
1427
|
+
requestTerminalResize(true, true);
|
|
1428
|
+
setTimeout(() => requestTerminalResize(true, true), 60);
|
|
1429
|
+
restoreTerminalFocus(false);
|
|
1430
|
+
} else {
|
|
1431
|
+
toggleSearch(false);
|
|
1432
|
+
}
|
|
1433
|
+
if (nextPage === "activity") refreshActivity({ quiet: true }).catch((error) => setStatus(error.message, true));
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
function initPageTabs() {
|
|
1437
|
+
el.pageTabs.forEach((tab) => {
|
|
1438
|
+
tab.addEventListener("click", () => setActivePage(tab.dataset.pageTab));
|
|
1439
|
+
});
|
|
1440
|
+
let page = "terminal";
|
|
1441
|
+
try {
|
|
1442
|
+
page = localStorage.getItem(PAGE_STORAGE_KEY) || page;
|
|
1443
|
+
} catch {
|
|
1444
|
+
// Page choice is best-effort UI state.
|
|
1445
|
+
}
|
|
1446
|
+
setActivePage(page);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
function clearOwnerToken(message) {
|
|
1450
|
+
localStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
1451
|
+
state.token = "";
|
|
1452
|
+
if (state.streamAbort) state.streamAbort.abort();
|
|
1453
|
+
clearStreamRetry();
|
|
1454
|
+
state.sessions = [];
|
|
1455
|
+
state.activeId = "";
|
|
1456
|
+
renderSessions();
|
|
1457
|
+
updateActiveHeader();
|
|
1458
|
+
setAuthVisible(true, message || "Owner token required");
|
|
1459
|
+
setStatus(message || "Owner token required", true);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function requireOwnerToken() {
|
|
1463
|
+
if (state.token) return true;
|
|
1464
|
+
setAuthVisible(true, "Owner token required");
|
|
1465
|
+
setStatus("Owner token required", true);
|
|
1466
|
+
return false;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function authHeaders(extra) {
|
|
1470
|
+
const headers = Object.assign({}, extra || {});
|
|
1471
|
+
if (state.token) headers.Authorization = "Bearer " + state.token;
|
|
1472
|
+
return headers;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
async function api(path, options) {
|
|
1476
|
+
const init = options || {};
|
|
1477
|
+
const headers = authHeaders(init.headers);
|
|
1478
|
+
if (init.body !== undefined && !headers["Content-Type"]) {
|
|
1479
|
+
headers["Content-Type"] = "application/json";
|
|
1480
|
+
}
|
|
1481
|
+
const res = await fetch(path, Object.assign({}, init, { headers }));
|
|
1482
|
+
const text = await res.text();
|
|
1483
|
+
let data = {};
|
|
1484
|
+
try { data = text ? JSON.parse(text) : {}; }
|
|
1485
|
+
catch { data = { output: text }; }
|
|
1486
|
+
if (!res.ok) {
|
|
1487
|
+
if (res.status === 401 || res.status === 403) {
|
|
1488
|
+
const message = "Owner token rejected";
|
|
1489
|
+
clearOwnerToken(message);
|
|
1490
|
+
throw new Error(message);
|
|
1491
|
+
}
|
|
1492
|
+
throw new Error(data.error || text || "HTTP " + res.status);
|
|
1493
|
+
}
|
|
1494
|
+
return data;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function randomId(prefix) {
|
|
1498
|
+
if (window.crypto && typeof window.crypto.randomUUID === "function") return window.crypto.randomUUID();
|
|
1499
|
+
return (prefix || "id") + "_" + Date.now() + "_" + Math.random().toString(16).slice(2);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function chatConversationId() {
|
|
1503
|
+
let id = localStorage.getItem(CHAT_CONVERSATION_KEY) || "";
|
|
1504
|
+
if (!id) {
|
|
1505
|
+
id = "workbench_" + Date.now() + "_" + Math.random().toString(16).slice(2, 10);
|
|
1506
|
+
localStorage.setItem(CHAT_CONVERSATION_KEY, id);
|
|
1507
|
+
}
|
|
1508
|
+
state.chatConversationId = id;
|
|
1509
|
+
return id;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function chatHistoryKey() {
|
|
1513
|
+
return CHAT_HISTORY_PREFIX + chatConversationId();
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
function activeChatTaskKey() {
|
|
1517
|
+
return ACTIVE_CHAT_TASK_PREFIX + chatConversationId();
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
function cmItemsKey() {
|
|
1521
|
+
return CM_ITEMS_PREFIX + chatConversationId();
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
function normalizeCmItem(record) {
|
|
1525
|
+
if (!record || typeof record !== "object") return null;
|
|
1526
|
+
const type = typeof record.type === "string" ? record.type : "notice";
|
|
1527
|
+
const title = typeof record.title === "string" ? record.title : "CM item";
|
|
1528
|
+
const text = typeof record.text === "string" ? record.text : "";
|
|
1529
|
+
const memoryProposal = record.memoryProposal && typeof record.memoryProposal === "object"
|
|
1530
|
+
? record.memoryProposal
|
|
1531
|
+
: null;
|
|
1532
|
+
if (!text && !memoryProposal) return null;
|
|
1533
|
+
return {
|
|
1534
|
+
id: typeof record.id === "string" && record.id ? record.id : randomId("cm"),
|
|
1535
|
+
type,
|
|
1536
|
+
title,
|
|
1537
|
+
text,
|
|
1538
|
+
status: typeof record.status === "string" ? record.status : "pending",
|
|
1539
|
+
ts: typeof record.ts === "string" ? record.ts : new Date().toISOString(),
|
|
1540
|
+
eventIds: record.eventIds && typeof record.eventIds === "object" ? record.eventIds : null,
|
|
1541
|
+
memoryProposal,
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
function loadCmItems() {
|
|
1546
|
+
try {
|
|
1547
|
+
const raw = localStorage.getItem(cmItemsKey());
|
|
1548
|
+
const parsed = raw ? JSON.parse(raw) : [];
|
|
1549
|
+
state.cmItems = Array.isArray(parsed)
|
|
1550
|
+
? parsed.map(normalizeCmItem).filter(Boolean).slice(-CM_ITEMS_LIMIT)
|
|
1551
|
+
: [];
|
|
1552
|
+
} catch {
|
|
1553
|
+
state.cmItems = [];
|
|
1554
|
+
}
|
|
1555
|
+
renderCmItems();
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
function saveCmItems() {
|
|
1559
|
+
try {
|
|
1560
|
+
const records = state.cmItems.slice(-CM_ITEMS_LIMIT);
|
|
1561
|
+
state.cmItems = records;
|
|
1562
|
+
localStorage.setItem(cmItemsKey(), JSON.stringify(records));
|
|
1563
|
+
} catch {
|
|
1564
|
+
// Local CM items are best-effort UI state.
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function renderCmItems() {
|
|
1569
|
+
el.cmItems.textContent = "";
|
|
1570
|
+
if (!state.cmItems.length) {
|
|
1571
|
+
const empty = document.createElement("div");
|
|
1572
|
+
empty.className = "cm-empty";
|
|
1573
|
+
empty.textContent = "No CM items yet.";
|
|
1574
|
+
el.cmItems.appendChild(empty);
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
state.cmItems.forEach((item) => {
|
|
1578
|
+
el.cmItems.appendChild(renderCmItem(item));
|
|
1579
|
+
});
|
|
1580
|
+
el.cmItems.scrollTop = el.cmItems.scrollHeight;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
function appendCmItem(item) {
|
|
1584
|
+
const normalized = normalizeCmItem(Object.assign({
|
|
1585
|
+
id: randomId("cm"),
|
|
1586
|
+
ts: new Date().toISOString(),
|
|
1587
|
+
status: "pending",
|
|
1588
|
+
}, item));
|
|
1589
|
+
if (!normalized) return;
|
|
1590
|
+
state.cmItems.push(normalized);
|
|
1591
|
+
saveCmItems();
|
|
1592
|
+
renderCmItems();
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
async function refreshActivity(options) {
|
|
1596
|
+
if (!state.token) {
|
|
1597
|
+
if (!options || options.quiet !== true) requireOwnerToken();
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
const data = await api(activityListPath(), { method: "GET" });
|
|
1601
|
+
state.activityTasks = Array.isArray(data.tasks) ? data.tasks : [];
|
|
1602
|
+
reconcileActiveChatTaskFromTasks(state.activityTasks);
|
|
1603
|
+
reconcileActivitySelection();
|
|
1604
|
+
renderActivity();
|
|
1605
|
+
await renderSelectedActivityDetail({ quiet: options?.quiet === true });
|
|
1606
|
+
if (!options || options.quiet !== true) setStatus("Activity refreshed");
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
function activityListPath() {
|
|
1610
|
+
const params = new URLSearchParams();
|
|
1611
|
+
params.set("limit", "50");
|
|
1612
|
+
if (el.activityRouteFilter.value !== "interactive-session") params.set("visibility", "primary");
|
|
1613
|
+
if (el.activityStatusFilter.value) params.set("status", el.activityStatusFilter.value);
|
|
1614
|
+
if (el.activityRouteFilter.value) params.set("route", el.activityRouteFilter.value);
|
|
1615
|
+
return "/self/tasks?" + params.toString();
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function renderActivity() {
|
|
1619
|
+
el.activityItems.textContent = "";
|
|
1620
|
+
renderActivityOverview();
|
|
1621
|
+
const visibleTasks = activityVisibleTasks();
|
|
1622
|
+
if (!visibleTasks.length) {
|
|
1623
|
+
const empty = document.createElement("div");
|
|
1624
|
+
empty.className = "activity-empty";
|
|
1625
|
+
empty.textContent = activityEmptyText();
|
|
1626
|
+
el.activityItems.appendChild(empty);
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
activityTaskGroups(visibleTasks).forEach((group) => {
|
|
1630
|
+
const section = document.createElement("div");
|
|
1631
|
+
section.className = "activity-group";
|
|
1632
|
+
const head = document.createElement("div");
|
|
1633
|
+
head.className = "activity-group-head";
|
|
1634
|
+
const label = document.createElement("span");
|
|
1635
|
+
label.textContent = group.label;
|
|
1636
|
+
const count = document.createElement("span");
|
|
1637
|
+
count.className = "activity-group-count";
|
|
1638
|
+
count.textContent = String(group.tasks.length);
|
|
1639
|
+
head.appendChild(label);
|
|
1640
|
+
head.appendChild(count);
|
|
1641
|
+
section.appendChild(head);
|
|
1642
|
+
group.tasks.forEach((task) => {
|
|
1643
|
+
section.appendChild(renderActivityTask(task));
|
|
1644
|
+
});
|
|
1645
|
+
el.activityItems.appendChild(section);
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
function activityVisibleTasks() {
|
|
1650
|
+
return activityBaseTasks().filter((task) => activityMatchesSearch(task));
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
function activityBaseTasks() {
|
|
1654
|
+
return state.activityTasks.filter((task) => !activityShouldHideTask(task));
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
function activitySearchTerm() {
|
|
1658
|
+
return (el.activitySearch.value || "").trim().toLowerCase();
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
function activityMatchesSearch(task) {
|
|
1662
|
+
const term = activitySearchTerm();
|
|
1663
|
+
if (!term) return true;
|
|
1664
|
+
const view = taskSurfaceView(task);
|
|
1665
|
+
const text = [
|
|
1666
|
+
task.taskId,
|
|
1667
|
+
view?.title,
|
|
1668
|
+
view?.summary,
|
|
1669
|
+
view?.nextAction,
|
|
1670
|
+
view?.ownerStatus,
|
|
1671
|
+
view?.stage?.label,
|
|
1672
|
+
task.title,
|
|
1673
|
+
task.objective,
|
|
1674
|
+
task.summary,
|
|
1675
|
+
task.ownerStatus,
|
|
1676
|
+
task.nextAction,
|
|
1677
|
+
activityStageLabel(task.stage),
|
|
1678
|
+
activityStatusLabel(task.status, task),
|
|
1679
|
+
activityRouteLabel(task),
|
|
1680
|
+
activityRelatedLabel(task),
|
|
1681
|
+
activityListSummary(task),
|
|
1682
|
+
].filter(Boolean).join(" ").toLowerCase();
|
|
1683
|
+
return text.includes(term);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
function reconcileActivitySelection() {
|
|
1687
|
+
const visibleTasks = activityVisibleTasks();
|
|
1688
|
+
const selected = visibleTasks.find((task) => task.taskId === state.activitySelectedTaskId);
|
|
1689
|
+
if (!selected) {
|
|
1690
|
+
state.activitySelectedTaskId = visibleTasks[0]?.taskId || "";
|
|
1691
|
+
state.activitySelectedTask = visibleTasks[0] || null;
|
|
1692
|
+
} else {
|
|
1693
|
+
state.activitySelectedTask = selected;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
function renderActivityOverview() {
|
|
1698
|
+
el.activityOverview.textContent = "";
|
|
1699
|
+
const counts = activityStatusCounts(activityBaseTasks());
|
|
1700
|
+
[
|
|
1701
|
+
["Open", counts.open],
|
|
1702
|
+
["Running", counts.running],
|
|
1703
|
+
["Waiting", counts.waiting],
|
|
1704
|
+
["Failed", counts.failed],
|
|
1705
|
+
["Done", counts.done],
|
|
1706
|
+
].forEach(([labelText, valueText]) => {
|
|
1707
|
+
const metric = document.createElement("div");
|
|
1708
|
+
metric.className = "activity-metric";
|
|
1709
|
+
const label = document.createElement("span");
|
|
1710
|
+
label.className = "activity-metric-label";
|
|
1711
|
+
label.textContent = labelText;
|
|
1712
|
+
const value = document.createElement("span");
|
|
1713
|
+
value.className = "activity-metric-value";
|
|
1714
|
+
value.textContent = String(valueText);
|
|
1715
|
+
metric.appendChild(label);
|
|
1716
|
+
metric.appendChild(value);
|
|
1717
|
+
el.activityOverview.appendChild(metric);
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
function activityStatusCounts(tasks) {
|
|
1722
|
+
const counts = { open: 0, running: 0, waiting: 0, failed: 0, done: 0 };
|
|
1723
|
+
tasks.forEach((task) => {
|
|
1724
|
+
if (task.status === "running" || task.status === "pending") {
|
|
1725
|
+
counts.running += 1;
|
|
1726
|
+
counts.open += 1;
|
|
1727
|
+
} else if (task.status === "waiting") {
|
|
1728
|
+
counts.waiting += 1;
|
|
1729
|
+
counts.open += 1;
|
|
1730
|
+
} else if (task.status === "failed") {
|
|
1731
|
+
counts.failed += 1;
|
|
1732
|
+
counts.open += 1;
|
|
1733
|
+
} else if (task.status === "succeeded" || task.status === "info") {
|
|
1734
|
+
counts.done += 1;
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
return counts;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
function activityTaskGroups(tasks) {
|
|
1741
|
+
const groups = [
|
|
1742
|
+
{ key: "attention", label: "Needs attention", tasks: [] },
|
|
1743
|
+
{ key: "active", label: "Active", tasks: [] },
|
|
1744
|
+
{ key: "done", label: "Done", tasks: [] },
|
|
1745
|
+
{ key: "other", label: "Other", tasks: [] },
|
|
1746
|
+
];
|
|
1747
|
+
const byKey = Object.fromEntries(groups.map((group) => [group.key, group]));
|
|
1748
|
+
tasks.forEach((task) => {
|
|
1749
|
+
byKey[activityTaskGroupKey(task)].tasks.push(task);
|
|
1750
|
+
});
|
|
1751
|
+
return groups.filter((group) => group.tasks.length);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
function activityTaskGroupKey(task) {
|
|
1755
|
+
if (task.status === "failed" || task.status === "waiting") return "attention";
|
|
1756
|
+
if (task.status === "running" || task.status === "pending") return "active";
|
|
1757
|
+
if (task.status === "succeeded" || task.status === "info") return "done";
|
|
1758
|
+
return "other";
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
function activityEmptyText() {
|
|
1762
|
+
if (!state.activityTasks.length) return "No activity yet.";
|
|
1763
|
+
if (activitySearchTerm()) return "No matching activity.";
|
|
1764
|
+
if (el.activityStatusFilter.value || el.activityRouteFilter.value) return "No activity in this view.";
|
|
1765
|
+
return "No activity yet.";
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
function activityShouldHideTask(task) {
|
|
1769
|
+
if (el.activityRouteFilter.value || el.activityStatusFilter.value) return false;
|
|
1770
|
+
return task?.visibility === "technical" || activityIsNoisyTerminalControl(task);
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
function activityIsNoisyTerminalControl(task) {
|
|
1774
|
+
return (task?.route === "interactive-session" || task?.source === "workbench_http" || readActivityDataText(task, "source") === "workbench_http")
|
|
1775
|
+
&& task?.status === "succeeded"
|
|
1776
|
+
&& readActivityDataText(task, "capability") === "resize_session";
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
function renderActivityTask(task) {
|
|
1780
|
+
const box = document.createElement("button");
|
|
1781
|
+
box.type = "button";
|
|
1782
|
+
box.className = "activity-item";
|
|
1783
|
+
if (task.taskId && task.taskId === state.activitySelectedTaskId) box.classList.add("selected");
|
|
1784
|
+
box.dataset.taskId = task.taskId || "";
|
|
1785
|
+
box.addEventListener("click", () => selectActivityTask(task.taskId));
|
|
1786
|
+
|
|
1787
|
+
const head = document.createElement("div");
|
|
1788
|
+
head.className = "activity-item-head";
|
|
1789
|
+
const title = document.createElement("div");
|
|
1790
|
+
title.className = "activity-item-title";
|
|
1791
|
+
title.textContent = activityTaskTitle(task);
|
|
1792
|
+
const status = document.createElement("span");
|
|
1793
|
+
status.className = activityStatusClass(task.status);
|
|
1794
|
+
status.textContent = activityStatusLabel(task.status, task);
|
|
1795
|
+
head.appendChild(title);
|
|
1796
|
+
head.appendChild(status);
|
|
1797
|
+
box.appendChild(head);
|
|
1798
|
+
|
|
1799
|
+
const meta = document.createElement("div");
|
|
1800
|
+
meta.className = "activity-item-meta";
|
|
1801
|
+
meta.textContent = [
|
|
1802
|
+
activityRouteLabel(task),
|
|
1803
|
+
activityRelatedLabel(task),
|
|
1804
|
+
formatShortTime(task.updatedAt || task.createdAt),
|
|
1805
|
+
].filter(Boolean).join(" · ");
|
|
1806
|
+
box.appendChild(meta);
|
|
1807
|
+
|
|
1808
|
+
const summaryText = activityListSummary(task);
|
|
1809
|
+
if (summaryText) {
|
|
1810
|
+
const summary = document.createElement("div");
|
|
1811
|
+
summary.className = "activity-item-summary";
|
|
1812
|
+
summary.textContent = summaryText;
|
|
1813
|
+
box.appendChild(summary);
|
|
1814
|
+
}
|
|
1815
|
+
return box;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
async function selectActivityTask(taskId, fallbackTask) {
|
|
1819
|
+
if (!taskId) return;
|
|
1820
|
+
state.activitySelectedTaskId = taskId;
|
|
1821
|
+
state.activitySelectedTask = state.activityTasks.find((task) => task.taskId === taskId) || fallbackTask || null;
|
|
1822
|
+
renderActivity();
|
|
1823
|
+
renderActivityDetail(state.activitySelectedTask, { loading: true });
|
|
1824
|
+
try {
|
|
1825
|
+
const data = await api("/self/tasks/" + encodeURIComponent(taskId), { method: "GET" });
|
|
1826
|
+
if (state.activitySelectedTaskId !== taskId) return;
|
|
1827
|
+
state.activitySelectedTask = data.task || state.activitySelectedTask;
|
|
1828
|
+
await renderSelectedActivityDetail();
|
|
1829
|
+
} catch (error) {
|
|
1830
|
+
if (state.activitySelectedTaskId !== taskId) return;
|
|
1831
|
+
setStatus(error.message, true);
|
|
1832
|
+
renderActivityDetail(state.activitySelectedTask);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
async function renderSelectedActivityDetail(options) {
|
|
1837
|
+
const task = state.activitySelectedTask;
|
|
1838
|
+
state.activityTimelineTasks = [];
|
|
1839
|
+
state.activityTimelineParentId = activityTimelineParentId(task);
|
|
1840
|
+
if (!task) {
|
|
1841
|
+
renderActivityDetail(null);
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
const parentTaskId = state.activityTimelineParentId;
|
|
1845
|
+
if (!parentTaskId || !state.token) {
|
|
1846
|
+
renderActivityDetail(task);
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
renderActivityDetail(task, { timelineLoading: true });
|
|
1850
|
+
try {
|
|
1851
|
+
const timelineTasks = await fetchActivityTimelineTasks(parentTaskId, task.taskId);
|
|
1852
|
+
if (state.activitySelectedTaskId !== task.taskId || state.activityTimelineParentId !== parentTaskId) return;
|
|
1853
|
+
state.activityTimelineTasks = timelineTasks;
|
|
1854
|
+
renderActivityDetail(state.activitySelectedTask);
|
|
1855
|
+
} catch (error) {
|
|
1856
|
+
if (options?.quiet === true) {
|
|
1857
|
+
renderActivityDetail(state.activitySelectedTask);
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
setStatus(error.message, true);
|
|
1861
|
+
renderActivityDetail(state.activitySelectedTask);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
async function fetchActivityTimelineTasks(parentTaskId, selectedTaskId) {
|
|
1866
|
+
const requestId = ++state.activityTimelineRequestId;
|
|
1867
|
+
const params = new URLSearchParams();
|
|
1868
|
+
params.set("visibility", "technical");
|
|
1869
|
+
params.set("parentTaskId", parentTaskId);
|
|
1870
|
+
params.set("limit", "30");
|
|
1871
|
+
const data = await api("/self/tasks?" + params.toString(), { method: "GET" });
|
|
1872
|
+
if (requestId !== state.activityTimelineRequestId) return [];
|
|
1873
|
+
const tasks = Array.isArray(data.tasks) ? data.tasks : [];
|
|
1874
|
+
return tasks.filter((task) => task.taskId !== selectedTaskId);
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
function activityStatusClass(status) {
|
|
1878
|
+
if (status === "running" || status === "waiting" || status === "pending") return "status-pill status-running";
|
|
1879
|
+
if (status === "failed") return "status-pill status-failed";
|
|
1880
|
+
return "status-pill status-ended";
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
function activityStatusLabel(status, task) {
|
|
1884
|
+
const view = taskSurfaceView(task);
|
|
1885
|
+
if (view?.stage?.label) return view.stage.label;
|
|
1886
|
+
const stage = activityStageLabel(task?.stage);
|
|
1887
|
+
if (stage) return stage;
|
|
1888
|
+
if (task?.ownerStatus) return task.ownerStatus;
|
|
1889
|
+
if (status === "pending") return "Queued";
|
|
1890
|
+
if (status === "running") return "Running";
|
|
1891
|
+
if (status === "waiting") {
|
|
1892
|
+
const retryAt = readActivityDataText(task, "nextRetryAt");
|
|
1893
|
+
return retryAt ? "Retry waiting" : "Waiting";
|
|
1894
|
+
}
|
|
1895
|
+
if (status === "succeeded") return "Done";
|
|
1896
|
+
if (status === "failed") return "Failed";
|
|
1897
|
+
if (status === "info") return "Info";
|
|
1898
|
+
return "Queued";
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
function activityStageLabel(stage) {
|
|
1902
|
+
const labels = {
|
|
1903
|
+
queued: "Queued",
|
|
1904
|
+
framing: "Framing",
|
|
1905
|
+
dispatching: "Dispatching",
|
|
1906
|
+
executing: "Executing",
|
|
1907
|
+
observing: "Observing",
|
|
1908
|
+
reviewing: "Reviewing",
|
|
1909
|
+
done: "Done",
|
|
1910
|
+
blocked: "Blocked",
|
|
1911
|
+
};
|
|
1912
|
+
return labels[stage] || "";
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
function activityRouteLabel(task) {
|
|
1916
|
+
const route = task?.route || task?.source || "";
|
|
1917
|
+
const labels = {
|
|
1918
|
+
core_cm: "Akemon",
|
|
1919
|
+
"interactive-session": "Terminal",
|
|
1920
|
+
"software-agent": "Software agent",
|
|
1921
|
+
"relay-order": "Order",
|
|
1922
|
+
"user-task": "Scheduled task",
|
|
1923
|
+
"relay-task": "Relay task",
|
|
1924
|
+
memory_cm: "Memory",
|
|
1925
|
+
relay_order: "Order",
|
|
1926
|
+
owner_recurring_task: "Scheduled task",
|
|
1927
|
+
relay_platform_task: "Relay task",
|
|
1928
|
+
local_chat: "Akemon",
|
|
1929
|
+
local_chat_command: "Command",
|
|
1930
|
+
workbench_http: "Terminal",
|
|
1931
|
+
};
|
|
1932
|
+
return labels[route] || prettifyActivityToken(route || "task");
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
function activityTaskTitle(task) {
|
|
1936
|
+
const view = taskSurfaceView(task);
|
|
1937
|
+
if (view?.title) return view.title;
|
|
1938
|
+
if (task?.title) return task.title;
|
|
1939
|
+
const terminalTitle = interactiveSessionActionTitle(task);
|
|
1940
|
+
if (terminalTitle) return terminalTitle;
|
|
1941
|
+
const related = activityRelatedLabel(task);
|
|
1942
|
+
const route = activityRouteLabel(task);
|
|
1943
|
+
if (related) return `${route}: ${related}`;
|
|
1944
|
+
return task.objective || task.summary || route || "Task";
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
function activityRelatedLabel(task) {
|
|
1948
|
+
const product = readActivityDataText(task, "productName");
|
|
1949
|
+
const order = readActivityDataText(task, "orderId");
|
|
1950
|
+
if (product && order) return `${product} (${shortActivityId(order)})`;
|
|
1951
|
+
if (product) return product;
|
|
1952
|
+
if (order) return `Order ${shortActivityId(order)}`;
|
|
1953
|
+
|
|
1954
|
+
const session = readActivityDataText(task, "sessionId");
|
|
1955
|
+
if (session) return `Session ${shortActivityId(session)}`;
|
|
1956
|
+
|
|
1957
|
+
const softwareId = readActivityDataText(task, "softwareAgentId");
|
|
1958
|
+
if (softwareId) return prettifyActivityToken(softwareId);
|
|
1959
|
+
|
|
1960
|
+
const taskKey = readActivityDataText(task, "taskKey");
|
|
1961
|
+
if (taskKey) return taskKey;
|
|
1962
|
+
|
|
1963
|
+
const relayTaskType = readActivityDataText(task, "relayTaskType");
|
|
1964
|
+
if (relayTaskType) return prettifyActivityToken(relayTaskType);
|
|
1965
|
+
|
|
1966
|
+
return "";
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
function activityListSummary(task) {
|
|
1970
|
+
const view = taskSurfaceView(task);
|
|
1971
|
+
const terminalSummary = interactiveSessionActionSummary(task);
|
|
1972
|
+
if (terminalSummary) return terminalSummary;
|
|
1973
|
+
if (view?.summary) return view.summary;
|
|
1974
|
+
if (view?.nextAction && task.status !== "succeeded") return view.nextAction;
|
|
1975
|
+
if (task.nextAction && task.status !== "succeeded") return task.nextAction;
|
|
1976
|
+
const error = readActivityDataText(task, "error");
|
|
1977
|
+
if (error) return `Error: ${error}`;
|
|
1978
|
+
const retryAt = readActivityDataText(task, "nextRetryAt");
|
|
1979
|
+
if (retryAt) return `Next retry: ${formatDateTime(retryAt)}`;
|
|
1980
|
+
const output = readActivityDataText(task, "outputSummary");
|
|
1981
|
+
if (output) return output;
|
|
1982
|
+
return task.summary || task.objective || "";
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
function renderActivityDetail(task, options) {
|
|
1986
|
+
el.activityDetail.textContent = "";
|
|
1987
|
+
if (!task) {
|
|
1988
|
+
const empty = document.createElement("div");
|
|
1989
|
+
empty.className = "activity-detail-empty";
|
|
1990
|
+
empty.textContent = "No task selected.";
|
|
1991
|
+
el.activityDetail.appendChild(empty);
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
const card = document.createElement("div");
|
|
1996
|
+
card.className = "activity-detail-card";
|
|
1997
|
+
|
|
1998
|
+
const head = document.createElement("div");
|
|
1999
|
+
head.className = "activity-detail-head";
|
|
2000
|
+
const titleBlock = document.createElement("div");
|
|
2001
|
+
const kicker = document.createElement("div");
|
|
2002
|
+
kicker.className = "activity-detail-kicker";
|
|
2003
|
+
kicker.textContent = activityRouteLabel(task);
|
|
2004
|
+
const title = document.createElement("div");
|
|
2005
|
+
title.className = "activity-detail-title";
|
|
2006
|
+
title.textContent = task.title || interactiveSessionActionTitle(task) || task.objective || activityTaskTitle(task);
|
|
2007
|
+
titleBlock.appendChild(kicker);
|
|
2008
|
+
titleBlock.appendChild(title);
|
|
2009
|
+
const status = document.createElement("span");
|
|
2010
|
+
status.className = activityStatusClass(task.status);
|
|
2011
|
+
status.textContent = options?.loading ? "Loading" : activityStatusLabel(task.status, task);
|
|
2012
|
+
head.appendChild(titleBlock);
|
|
2013
|
+
head.appendChild(status);
|
|
2014
|
+
card.appendChild(head);
|
|
2015
|
+
|
|
2016
|
+
const stateText = activityStateText(task);
|
|
2017
|
+
if (stateText) {
|
|
2018
|
+
const summary = document.createElement("div");
|
|
2019
|
+
summary.className = "activity-detail-summary";
|
|
2020
|
+
summary.textContent = stateText;
|
|
2021
|
+
card.appendChild(summary);
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
card.appendChild(renderActivityDetailGroup("Current state", activityCurrentFields(task)));
|
|
2025
|
+
const relatedFields = activityRelatedFields(task);
|
|
2026
|
+
if (relatedFields.length) card.appendChild(renderActivityDetailGroup("Related", relatedFields));
|
|
2027
|
+
|
|
2028
|
+
const dispatchText = activityDispatchText(task);
|
|
2029
|
+
if (dispatchText) card.appendChild(renderActivityTextSection("Dispatch records", dispatchText));
|
|
2030
|
+
|
|
2031
|
+
const reviewText = activityReviewText(task);
|
|
2032
|
+
if (reviewText) card.appendChild(renderActivityTextSection("Review records", reviewText));
|
|
2033
|
+
|
|
2034
|
+
const followUpText = activityFollowUpText(task);
|
|
2035
|
+
if (followUpText) card.appendChild(renderActivityTextSection("Queued follow-ups", followUpText));
|
|
2036
|
+
|
|
2037
|
+
const resultText = activityResultText(task);
|
|
2038
|
+
if (resultText) card.appendChild(renderActivityTextSection("Result", resultText));
|
|
2039
|
+
const retryAt = readActivityDataText(task, "nextRetryAt");
|
|
2040
|
+
if (retryAt) card.appendChild(renderActivityTextSection("Next retry", formatDateTime(retryAt)));
|
|
2041
|
+
const timelineTasks = activityTimelineTasksFor(task);
|
|
2042
|
+
if (timelineTasks.length || options?.timelineLoading) {
|
|
2043
|
+
card.appendChild(renderActivityTimeline(timelineTasks, options?.timelineLoading));
|
|
2044
|
+
}
|
|
2045
|
+
card.appendChild(renderActivityTechnicalDetails(task));
|
|
2046
|
+
|
|
2047
|
+
el.activityDetail.appendChild(card);
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
function activityTimelineParentId(task) {
|
|
2051
|
+
if (!task) return "";
|
|
2052
|
+
if (task.parentTaskId) return task.parentTaskId;
|
|
2053
|
+
const sessionId = readActivityDataText(task, "sessionId");
|
|
2054
|
+
if ((task.route === "interactive-session" || task.source === "workbench_http") && sessionId) {
|
|
2055
|
+
return `interactive-session:${sessionId}`;
|
|
2056
|
+
}
|
|
2057
|
+
return task.taskId || "";
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
function activityTimelineTasksFor(task) {
|
|
2061
|
+
const parentTaskId = activityTimelineParentId(task);
|
|
2062
|
+
if (!parentTaskId || state.activityTimelineParentId !== parentTaskId) return [];
|
|
2063
|
+
return state.activityTimelineTasks;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
function activityStateText(task) {
|
|
2067
|
+
const view = taskSurfaceView(task);
|
|
2068
|
+
const terminalSummary = interactiveSessionActionSummary(task);
|
|
2069
|
+
if (terminalSummary) return terminalSummary;
|
|
2070
|
+
if (view?.stage?.description) return view.stage.description;
|
|
2071
|
+
const stageText = activityStageText(task);
|
|
2072
|
+
if (stageText) return stageText;
|
|
2073
|
+
if (task.status === "waiting") {
|
|
2074
|
+
const retryAt = readActivityDataText(task, "nextRetryAt");
|
|
2075
|
+
if (retryAt) return `Waiting until ${formatDateTime(retryAt)} before retrying.`;
|
|
2076
|
+
return "Waiting for another system or retry window.";
|
|
2077
|
+
}
|
|
2078
|
+
if (task.status === "running") return "Currently being worked on.";
|
|
2079
|
+
if (task.status === "pending") return "Queued and not started yet.";
|
|
2080
|
+
if (task.status === "succeeded") return "Finished successfully.";
|
|
2081
|
+
if (task.status === "failed") return "Stopped with an error.";
|
|
2082
|
+
return task.summary || "";
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
function activityStageText(task) {
|
|
2086
|
+
if (!task?.stage) return "";
|
|
2087
|
+
const summary = task.summary || task.nextAction || "";
|
|
2088
|
+
const texts = {
|
|
2089
|
+
queued: "Queued and waiting to start.",
|
|
2090
|
+
framing: "Akemon is framing the owner request.",
|
|
2091
|
+
dispatching: "Akemon is dispatching work to a capability.",
|
|
2092
|
+
executing: "Execution is in progress.",
|
|
2093
|
+
observing: "Akemon is observing the execution result.",
|
|
2094
|
+
reviewing: "Akemon is reviewing the result for the owner report.",
|
|
2095
|
+
done: "Finished.",
|
|
2096
|
+
blocked: "Blocked.",
|
|
2097
|
+
};
|
|
2098
|
+
const text = texts[task.stage] || "";
|
|
2099
|
+
if (!text) return "";
|
|
2100
|
+
return summary && summary !== text ? `${text} ${summary}` : text;
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
function interactiveSessionActionTitle(task) {
|
|
2104
|
+
if (task?.route !== "interactive-session") return "";
|
|
2105
|
+
const label = interactiveSessionCapabilityLabel(readActivityDataText(task, "capability"));
|
|
2106
|
+
if (!label) return "";
|
|
2107
|
+
const related = activityRelatedLabel(task);
|
|
2108
|
+
return related ? `${label}: ${related}` : label;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
function interactiveSessionActionSummary(task) {
|
|
2112
|
+
if (task?.route !== "interactive-session") return "";
|
|
2113
|
+
const capability = readActivityDataText(task, "capability");
|
|
2114
|
+
const session = readActivityDataText(task, "sessionId");
|
|
2115
|
+
const target = session ? ` for ${shortActivityId(session)}` : "";
|
|
2116
|
+
if (task.status === "failed") {
|
|
2117
|
+
const error = readActivityDataText(task, "error");
|
|
2118
|
+
return error ? `${interactiveSessionCapabilityLabel(capability) || "Terminal action"} failed: ${error}` : `${interactiveSessionCapabilityLabel(capability) || "Terminal action"} failed.`;
|
|
2119
|
+
}
|
|
2120
|
+
if (capability === "resize_session") return `Terminal size updated${target}.`;
|
|
2121
|
+
if (capability === "start_session") return `Terminal session started${target}.`;
|
|
2122
|
+
if (capability === "send_input") return `Input sent to terminal${target}.`;
|
|
2123
|
+
if (capability === "stop_session") return `Terminal session stopped${target}.`;
|
|
2124
|
+
if (capability === "inspect_session") return `Terminal session inspected${target}.`;
|
|
2125
|
+
if (capability === "set_input_mode") return `Terminal input mode changed${target}.`;
|
|
2126
|
+
if (capability === "list_sessions") return "Terminal sessions listed.";
|
|
2127
|
+
return "";
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
function interactiveSessionCapabilityLabel(capability) {
|
|
2131
|
+
const labels = {
|
|
2132
|
+
resize_session: "Resize terminal",
|
|
2133
|
+
start_session: "Start terminal",
|
|
2134
|
+
send_input: "Send input",
|
|
2135
|
+
stop_session: "Stop terminal",
|
|
2136
|
+
inspect_session: "Inspect terminal",
|
|
2137
|
+
set_input_mode: "Input mode",
|
|
2138
|
+
list_sessions: "List sessions",
|
|
2139
|
+
};
|
|
2140
|
+
return labels[capability] || "";
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
function activityCurrentFields(task) {
|
|
2144
|
+
const view = taskSurfaceView(task);
|
|
2145
|
+
const counts = view?.counts || {};
|
|
2146
|
+
return [
|
|
2147
|
+
{ label: "State", value: activityStatusLabel(task.status, task) },
|
|
2148
|
+
{ label: "Stage", value: view?.stage?.label || activityStageLabel(task.stage) },
|
|
2149
|
+
{ label: "Kind", value: activityRouteLabel(task) },
|
|
2150
|
+
{ label: "Visibility", value: task.visibility === "technical" ? "Technical event" : "Primary task" },
|
|
2151
|
+
{ label: "Dispatches", value: counts.dispatches ? String(counts.dispatches) : (activityDispatchRecords(task).length ? String(activityDispatchRecords(task).length) : "") },
|
|
2152
|
+
{ label: "Reviews", value: counts.reviews ? String(counts.reviews) : (activityReviewRecords(task).length ? String(activityReviewRecords(task).length) : "") },
|
|
2153
|
+
{ label: "Follow-ups", value: counts.followUps ? String(counts.followUps) : (activityFollowUps(task).length ? String(activityFollowUps(task).length) : "") },
|
|
2154
|
+
{ label: "Next", value: view?.nextAction || task.nextAction },
|
|
2155
|
+
{ label: "Updated", value: formatDateTime(task.updatedAt) },
|
|
2156
|
+
{ label: "Started", value: formatDateTime(task.createdAt) },
|
|
2157
|
+
{ label: "Finished", value: formatDateTime(task.completedAt) },
|
|
2158
|
+
].filter((field) => field.value);
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
function activityRelatedFields(task) {
|
|
2162
|
+
return [
|
|
2163
|
+
{ label: "Object", value: activityRelatedLabel(task) },
|
|
2164
|
+
{ label: "Order", value: readActivityDataText(task, "orderId") },
|
|
2165
|
+
{ label: "Product", value: readActivityDataText(task, "productName") || readActivityDataText(task, "productId") },
|
|
2166
|
+
{ label: "Session", value: readActivityDataText(task, "sessionId") },
|
|
2167
|
+
{ label: "Conversation", value: task.conversationId },
|
|
2168
|
+
{ label: "Parent", value: task.parentTaskId },
|
|
2169
|
+
{ label: "Software Ledger", value: readActivityDataText(task, "taskRecordPath") },
|
|
2170
|
+
].filter((field) => field.value);
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
function activityResultText(task) {
|
|
2174
|
+
const view = taskSurfaceView(task);
|
|
2175
|
+
if (view?.report?.text) return view.report.text;
|
|
2176
|
+
const error = readActivityDataText(task, "error");
|
|
2177
|
+
if (error) return `Error: ${error}`;
|
|
2178
|
+
const output = readActivityDataText(task, "outputSummary");
|
|
2179
|
+
if (output) return output;
|
|
2180
|
+
if (task.status === "failed") return task.summary || "No error detail recorded.";
|
|
2181
|
+
if (task.status === "succeeded") return task.summary || "No result detail recorded.";
|
|
2182
|
+
return "";
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
function activityDispatchRecords(task) {
|
|
2186
|
+
const view = taskSurfaceView(task);
|
|
2187
|
+
if (Array.isArray(view?.dispatches)) {
|
|
2188
|
+
return view.dispatches.map((record) => ({
|
|
2189
|
+
target: typeof record.targetPeripheral === "string" ? record.targetPeripheral : "",
|
|
2190
|
+
capability: typeof record.capability === "string" ? record.capability : "",
|
|
2191
|
+
brief: typeof record.brief === "string" ? record.brief : "",
|
|
2192
|
+
expectedDeliverable: typeof record.expectedDeliverable === "string" ? record.expectedDeliverable : "",
|
|
2193
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : "",
|
|
2194
|
+
})).filter((record) => record.target || record.capability);
|
|
2195
|
+
}
|
|
2196
|
+
const records = Array.isArray(task?.data?.dispatchRecords) ? task.data.dispatchRecords : [];
|
|
2197
|
+
return records.map((record) => {
|
|
2198
|
+
if (!record || typeof record !== "object") return null;
|
|
2199
|
+
const target = typeof record.targetPeripheral === "string" ? record.targetPeripheral : "";
|
|
2200
|
+
const capability = typeof record.capability === "string" ? record.capability : "";
|
|
2201
|
+
if (!target && !capability) return null;
|
|
2202
|
+
return {
|
|
2203
|
+
target,
|
|
2204
|
+
capability,
|
|
2205
|
+
brief: typeof record.brief === "string" ? record.brief : "",
|
|
2206
|
+
expectedDeliverable: typeof record.expectedDeliverable === "string" ? record.expectedDeliverable : "",
|
|
2207
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : "",
|
|
2208
|
+
};
|
|
2209
|
+
}).filter(Boolean);
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
function activityDispatchText(task) {
|
|
2213
|
+
const records = activityDispatchRecords(task);
|
|
2214
|
+
if (!records.length) return "";
|
|
2215
|
+
return records.slice(-5).map((record) => {
|
|
2216
|
+
const time = record.createdAt ? formatShortTime(record.createdAt) : "";
|
|
2217
|
+
const target = [record.target, record.capability].filter(Boolean).join(".");
|
|
2218
|
+
const head = [target, time].filter(Boolean).join(" ");
|
|
2219
|
+
const details = [record.brief, record.expectedDeliverable].filter(Boolean).join("\n");
|
|
2220
|
+
return details ? `${head}\n${details}` : head;
|
|
2221
|
+
}).join("\n\n");
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
function activityReviewRecords(task) {
|
|
2225
|
+
const view = taskSurfaceView(task);
|
|
2226
|
+
if (Array.isArray(view?.reviews)) {
|
|
2227
|
+
return view.reviews.map((record) => ({
|
|
2228
|
+
resultQuality: typeof record.resultQuality === "string" ? record.resultQuality : "",
|
|
2229
|
+
completionDecision: typeof record.completionDecision === "string" ? record.completionDecision : "",
|
|
2230
|
+
followUpNeeded: record.followUpNeeded === true,
|
|
2231
|
+
reportText: typeof record.reportText === "string" ? record.reportText : "",
|
|
2232
|
+
summary: typeof record.summary === "string" ? record.summary : "",
|
|
2233
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : "",
|
|
2234
|
+
memoryProposalRefs: Array.isArray(record.memoryProposalRefs) ? record.memoryProposalRefs : [],
|
|
2235
|
+
})).filter((record) => record.resultQuality || record.completionDecision || record.reportText);
|
|
2236
|
+
}
|
|
2237
|
+
const records = Array.isArray(task?.data?.reviewRecords) ? task.data.reviewRecords : [];
|
|
2238
|
+
return records.map((record) => {
|
|
2239
|
+
if (!record || typeof record !== "object") return null;
|
|
2240
|
+
const resultQuality = typeof record.resultQuality === "string" ? record.resultQuality : "";
|
|
2241
|
+
const completionDecision = typeof record.completionDecision === "string" ? record.completionDecision : "";
|
|
2242
|
+
const reportText = typeof record.reportText === "string" ? record.reportText : "";
|
|
2243
|
+
if (!resultQuality && !completionDecision && !reportText) return null;
|
|
2244
|
+
return {
|
|
2245
|
+
resultQuality,
|
|
2246
|
+
completionDecision,
|
|
2247
|
+
followUpNeeded: record.followUpNeeded === true,
|
|
2248
|
+
reportText,
|
|
2249
|
+
summary: typeof record.summary === "string" ? record.summary : "",
|
|
2250
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : "",
|
|
2251
|
+
memoryProposalRefs: Array.isArray(record.memoryProposalRefs) ? record.memoryProposalRefs : [],
|
|
2252
|
+
};
|
|
2253
|
+
}).filter(Boolean);
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
function activityReviewText(task) {
|
|
2257
|
+
const records = activityReviewRecords(task);
|
|
2258
|
+
if (!records.length) return "";
|
|
2259
|
+
return records.slice(-5).map((record) => {
|
|
2260
|
+
const time = record.createdAt ? formatShortTime(record.createdAt) : "";
|
|
2261
|
+
const decision = [
|
|
2262
|
+
record.resultQuality ? prettifyActivityToken(record.resultQuality) : "",
|
|
2263
|
+
record.completionDecision ? prettifyActivityToken(record.completionDecision) : "",
|
|
2264
|
+
record.followUpNeeded ? "Follow-up needed" : "",
|
|
2265
|
+
].filter(Boolean).join(" · ");
|
|
2266
|
+
const head = [decision, time].filter(Boolean).join(" ");
|
|
2267
|
+
const memoryRefs = record.memoryProposalRefs.length
|
|
2268
|
+
? `Memory proposals: ${record.memoryProposalRefs.map((item) => (
|
|
2269
|
+
item && typeof item === "object" && typeof item.memoryProposalId === "string" ? item.memoryProposalId : ""
|
|
2270
|
+
)).filter(Boolean).join(", ")}`
|
|
2271
|
+
: "";
|
|
2272
|
+
return [head, record.summary, memoryRefs, record.reportText].filter(Boolean).join("\n");
|
|
2273
|
+
}).join("\n\n");
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
function activityFollowUps(task) {
|
|
2277
|
+
const view = taskSurfaceView(task);
|
|
2278
|
+
if (Array.isArray(view?.followUps)) {
|
|
2279
|
+
return view.followUps.map((item) => ({
|
|
2280
|
+
kind: typeof item.kind === "string" ? item.kind : "note",
|
|
2281
|
+
text: typeof item.text === "string" ? item.text.trim() : "",
|
|
2282
|
+
createdAt: typeof item.createdAt === "string" ? item.createdAt : "",
|
|
2283
|
+
})).filter((item) => item.text);
|
|
2284
|
+
}
|
|
2285
|
+
const items = Array.isArray(task?.data?.followUps) ? task.data.followUps : [];
|
|
2286
|
+
return items.map((item) => {
|
|
2287
|
+
if (!item || typeof item !== "object") return null;
|
|
2288
|
+
const text = typeof item.text === "string" ? item.text.trim() : "";
|
|
2289
|
+
if (!text) return null;
|
|
2290
|
+
const kind = typeof item.kind === "string" ? item.kind : "note";
|
|
2291
|
+
const createdAt = typeof item.createdAt === "string" ? item.createdAt : "";
|
|
2292
|
+
return { kind, text, createdAt };
|
|
2293
|
+
}).filter(Boolean);
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
function activityFollowUpText(task) {
|
|
2297
|
+
const items = activityFollowUps(task);
|
|
2298
|
+
if (!items.length) return "";
|
|
2299
|
+
return items.slice(-5).map((item) => {
|
|
2300
|
+
const time = item.createdAt ? formatShortTime(item.createdAt) : "";
|
|
2301
|
+
const prefix = [prettifyActivityToken(item.kind), time].filter(Boolean).join(" ");
|
|
2302
|
+
return prefix ? `${prefix}: ${item.text}` : item.text;
|
|
2303
|
+
}).join("\n");
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
function renderActivityDetailGroup(titleText, fields) {
|
|
2307
|
+
const section = document.createElement("div");
|
|
2308
|
+
section.className = "activity-detail-section";
|
|
2309
|
+
const title = document.createElement("div");
|
|
2310
|
+
title.className = "activity-detail-section-title";
|
|
2311
|
+
title.textContent = titleText;
|
|
2312
|
+
const grid = document.createElement("div");
|
|
2313
|
+
grid.className = "activity-detail-grid";
|
|
2314
|
+
fields.forEach((field) => {
|
|
2315
|
+
grid.appendChild(renderActivityDetailField(field.label, field.value));
|
|
2316
|
+
});
|
|
2317
|
+
section.appendChild(title);
|
|
2318
|
+
section.appendChild(grid);
|
|
2319
|
+
return section;
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
function renderActivityDetailField(labelText, valueText) {
|
|
2323
|
+
const field = document.createElement("div");
|
|
2324
|
+
field.className = "activity-detail-field";
|
|
2325
|
+
const label = document.createElement("div");
|
|
2326
|
+
label.className = "activity-detail-label";
|
|
2327
|
+
label.textContent = labelText;
|
|
2328
|
+
const value = document.createElement("div");
|
|
2329
|
+
value.className = "activity-detail-value";
|
|
2330
|
+
value.textContent = valueText;
|
|
2331
|
+
field.appendChild(label);
|
|
2332
|
+
field.appendChild(value);
|
|
2333
|
+
return field;
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
function renderActivityTextSection(titleText, bodyText) {
|
|
2337
|
+
const section = document.createElement("div");
|
|
2338
|
+
section.className = "activity-detail-section";
|
|
2339
|
+
const title = document.createElement("div");
|
|
2340
|
+
title.className = "activity-detail-section-title";
|
|
2341
|
+
title.textContent = titleText;
|
|
2342
|
+
const body = document.createElement("div");
|
|
2343
|
+
body.className = "activity-detail-text";
|
|
2344
|
+
body.textContent = bodyText;
|
|
2345
|
+
section.appendChild(title);
|
|
2346
|
+
section.appendChild(body);
|
|
2347
|
+
return section;
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
function renderActivityTimeline(tasks, loading) {
|
|
2351
|
+
const section = document.createElement("div");
|
|
2352
|
+
section.className = "activity-detail-section";
|
|
2353
|
+
const title = document.createElement("div");
|
|
2354
|
+
title.className = "activity-detail-section-title";
|
|
2355
|
+
title.textContent = "Timeline";
|
|
2356
|
+
const list = document.createElement("div");
|
|
2357
|
+
list.className = "activity-timeline";
|
|
2358
|
+
|
|
2359
|
+
if (loading) {
|
|
2360
|
+
const empty = document.createElement("div");
|
|
2361
|
+
empty.className = "activity-timeline-empty";
|
|
2362
|
+
empty.textContent = "Loading related events.";
|
|
2363
|
+
list.appendChild(empty);
|
|
2364
|
+
} else {
|
|
2365
|
+
tasks.slice()
|
|
2366
|
+
.sort((a, b) => activityTimestamp(a) - activityTimestamp(b))
|
|
2367
|
+
.forEach((task) => list.appendChild(renderActivityTimelineItem(task)));
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
section.appendChild(title);
|
|
2371
|
+
section.appendChild(list);
|
|
2372
|
+
return section;
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
function renderActivityTimelineItem(task) {
|
|
2376
|
+
const item = document.createElement("button");
|
|
2377
|
+
item.type = "button";
|
|
2378
|
+
item.className = "activity-timeline-item";
|
|
2379
|
+
item.addEventListener("click", () => selectActivityTask(task.taskId, task));
|
|
2380
|
+
|
|
2381
|
+
const row = document.createElement("div");
|
|
2382
|
+
row.className = "activity-timeline-row";
|
|
2383
|
+
const title = document.createElement("div");
|
|
2384
|
+
title.className = "activity-timeline-title";
|
|
2385
|
+
title.textContent = task.title || activityTaskTitle(task);
|
|
2386
|
+
const time = document.createElement("div");
|
|
2387
|
+
time.className = "activity-timeline-time";
|
|
2388
|
+
time.textContent = formatShortTime(task.updatedAt || task.createdAt);
|
|
2389
|
+
row.appendChild(title);
|
|
2390
|
+
row.appendChild(time);
|
|
2391
|
+
item.appendChild(row);
|
|
2392
|
+
|
|
2393
|
+
const summaryText = activityListSummary(task);
|
|
2394
|
+
if (summaryText) {
|
|
2395
|
+
const summary = document.createElement("div");
|
|
2396
|
+
summary.className = "activity-timeline-summary";
|
|
2397
|
+
summary.textContent = summaryText;
|
|
2398
|
+
item.appendChild(summary);
|
|
2399
|
+
}
|
|
2400
|
+
return item;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
function activityTimestamp(task) {
|
|
2404
|
+
const raw = task?.createdAt || task?.updatedAt || "";
|
|
2405
|
+
const value = Date.parse(raw);
|
|
2406
|
+
return Number.isFinite(value) ? value : 0;
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
function renderActivityTechnicalDetails(task) {
|
|
2410
|
+
const details = document.createElement("details");
|
|
2411
|
+
details.className = "activity-technical";
|
|
2412
|
+
const summary = document.createElement("summary");
|
|
2413
|
+
summary.textContent = "Technical details";
|
|
2414
|
+
details.appendChild(summary);
|
|
2415
|
+
|
|
2416
|
+
details.appendChild(renderActivityDetailField("Task ID", task.taskId || ""));
|
|
2417
|
+
if (task.route) details.appendChild(renderActivityDetailField("Route", task.route));
|
|
2418
|
+
if (task.source) details.appendChild(renderActivityDetailField("Source", task.source));
|
|
2419
|
+
if (task.refs && task.refs.length) {
|
|
2420
|
+
details.appendChild(renderActivityPreBlock("Refs", JSON.stringify(task.refs, null, 2)));
|
|
2421
|
+
}
|
|
2422
|
+
if (task.data && Object.keys(task.data).length) {
|
|
2423
|
+
details.appendChild(renderActivityPreBlock("Data", JSON.stringify(task.data, null, 2)));
|
|
2424
|
+
}
|
|
2425
|
+
return details;
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
function renderActivityPreBlock(titleText, bodyText) {
|
|
2429
|
+
const section = document.createElement("div");
|
|
2430
|
+
section.className = "activity-detail-section";
|
|
2431
|
+
const title = document.createElement("div");
|
|
2432
|
+
title.className = "activity-detail-section-title";
|
|
2433
|
+
title.textContent = titleText;
|
|
2434
|
+
const body = document.createElement("pre");
|
|
2435
|
+
body.className = "activity-detail-pre";
|
|
2436
|
+
body.textContent = bodyText;
|
|
2437
|
+
section.appendChild(title);
|
|
2438
|
+
section.appendChild(body);
|
|
2439
|
+
return section;
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
function readActivityDataText(task, key) {
|
|
2443
|
+
const value = task?.data?.[key];
|
|
2444
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
2445
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
2446
|
+
return "";
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
function shortActivityId(value) {
|
|
2450
|
+
const text = String(value || "");
|
|
2451
|
+
if (text.length <= 18) return text;
|
|
2452
|
+
return `${text.slice(0, 8)}...${text.slice(-6)}`;
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
function prettifyActivityToken(value) {
|
|
2456
|
+
const text = String(value || "").trim();
|
|
2457
|
+
if (!text) return "";
|
|
2458
|
+
return text
|
|
2459
|
+
.replace(/[_:-]+/g, " ")
|
|
2460
|
+
.replace(/\s+/g, " ")
|
|
2461
|
+
.trim()
|
|
2462
|
+
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
function formatDateTime(value) {
|
|
2466
|
+
const time = Date.parse(value || "");
|
|
2467
|
+
if (!Number.isFinite(time)) return "";
|
|
2468
|
+
return new Date(time).toLocaleString([], {
|
|
2469
|
+
month: "2-digit",
|
|
2470
|
+
day: "2-digit",
|
|
2471
|
+
hour: "2-digit",
|
|
2472
|
+
minute: "2-digit",
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
function renderCmItem(item) {
|
|
2477
|
+
const box = document.createElement("div");
|
|
2478
|
+
box.className = "cm-item";
|
|
2479
|
+
box.dataset.cmItemId = item.id;
|
|
2480
|
+
|
|
2481
|
+
const head = document.createElement("div");
|
|
2482
|
+
head.className = "cm-item-head";
|
|
2483
|
+
const kind = document.createElement("div");
|
|
2484
|
+
kind.className = "cm-item-kind";
|
|
2485
|
+
kind.textContent = item.title || item.type;
|
|
2486
|
+
const time = document.createElement("div");
|
|
2487
|
+
time.className = "cm-item-time";
|
|
2488
|
+
time.textContent = formatShortTime(item.ts);
|
|
2489
|
+
head.appendChild(kind);
|
|
2490
|
+
head.appendChild(time);
|
|
2491
|
+
box.appendChild(head);
|
|
2492
|
+
|
|
2493
|
+
if (item.text && !item.memoryProposal) {
|
|
2494
|
+
const text = document.createElement("div");
|
|
2495
|
+
text.className = "memory-proposal-text";
|
|
2496
|
+
text.textContent = item.text;
|
|
2497
|
+
box.appendChild(text);
|
|
2498
|
+
}
|
|
2499
|
+
if (item.memoryProposal) {
|
|
2500
|
+
box.appendChild(renderMemoryProposal(item.memoryProposal, { readonly: item.status !== "pending" }));
|
|
2501
|
+
}
|
|
2502
|
+
return box;
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
function formatShortTime(value) {
|
|
2506
|
+
const time = Date.parse(value || "");
|
|
2507
|
+
if (!Number.isFinite(time)) return "";
|
|
2508
|
+
return new Date(time).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
function normalizeChatHistoryRecord(record) {
|
|
2512
|
+
if (!record || typeof record !== "object") return null;
|
|
2513
|
+
const role = record.role === "user" ? "user" : "akemon";
|
|
2514
|
+
const text = typeof record.text === "string" ? record.text : "";
|
|
2515
|
+
const memoryProposal = record.memoryProposal && typeof record.memoryProposal === "object"
|
|
2516
|
+
? record.memoryProposal
|
|
2517
|
+
: null;
|
|
2518
|
+
const task = record.task && typeof record.task === "object" ? record.task : null;
|
|
2519
|
+
if (!text && !memoryProposal) return null;
|
|
2520
|
+
return {
|
|
2521
|
+
role,
|
|
2522
|
+
text,
|
|
2523
|
+
error: record.error === true,
|
|
2524
|
+
memoryProposal,
|
|
2525
|
+
task,
|
|
2526
|
+
ts: typeof record.ts === "string" ? record.ts : "",
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
function loadChatHistory() {
|
|
2531
|
+
try {
|
|
2532
|
+
const raw = localStorage.getItem(chatHistoryKey());
|
|
2533
|
+
const parsed = raw ? JSON.parse(raw) : [];
|
|
2534
|
+
state.chatHistory = Array.isArray(parsed)
|
|
2535
|
+
? parsed.map(normalizeChatHistoryRecord).filter(Boolean).slice(-CHAT_HISTORY_LIMIT)
|
|
2536
|
+
: [];
|
|
2537
|
+
} catch {
|
|
2538
|
+
state.chatHistory = [];
|
|
2539
|
+
}
|
|
2540
|
+
renderChatHistory();
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
function saveChatHistory() {
|
|
2544
|
+
try {
|
|
2545
|
+
const records = state.chatHistory.slice(-CHAT_HISTORY_LIMIT);
|
|
2546
|
+
state.chatHistory = records;
|
|
2547
|
+
localStorage.setItem(chatHistoryKey(), JSON.stringify(records));
|
|
2548
|
+
} catch {
|
|
2549
|
+
// Local history is best-effort UI state.
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
function renderChatHistory() {
|
|
2554
|
+
el.chatMessages.textContent = "";
|
|
2555
|
+
if (!state.chatHistory.length) {
|
|
2556
|
+
const empty = document.createElement("div");
|
|
2557
|
+
empty.className = "chat-empty";
|
|
2558
|
+
empty.textContent = "No messages.";
|
|
2559
|
+
el.chatMessages.appendChild(empty);
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
state.chatHistory.forEach((record) => {
|
|
2563
|
+
appendChatMessage(record.role, record.text, record.error, {
|
|
2564
|
+
fromHistory: true,
|
|
2565
|
+
memoryProposal: record.memoryProposal,
|
|
2566
|
+
task: record.task,
|
|
2567
|
+
});
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
function clearChatHistory() {
|
|
2572
|
+
state.chatHistory = [];
|
|
2573
|
+
try {
|
|
2574
|
+
localStorage.removeItem(chatHistoryKey());
|
|
2575
|
+
} catch {
|
|
2576
|
+
// Local history is best-effort UI state.
|
|
2577
|
+
}
|
|
2578
|
+
renderChatHistory();
|
|
2579
|
+
setStatus("Chat cleared");
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
function updateStoredMemoryProposalStatus(proposalId, statusText) {
|
|
2583
|
+
let updated = false;
|
|
2584
|
+
for (let i = state.chatHistory.length - 1; i >= 0; i -= 1) {
|
|
2585
|
+
const proposal = state.chatHistory[i].memoryProposal;
|
|
2586
|
+
if (!proposal || typeof proposal !== "object") continue;
|
|
2587
|
+
if ((proposalId && proposal.id === proposalId) || (!proposalId && !updated)) {
|
|
2588
|
+
proposal.uiStatus = statusText;
|
|
2589
|
+
updated = true;
|
|
2590
|
+
if (proposalId) break;
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
if (updated) saveChatHistory();
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
function updateStoredCmMemoryProposalStatus(proposalId, statusText) {
|
|
2597
|
+
let updated = false;
|
|
2598
|
+
for (let i = state.cmItems.length - 1; i >= 0; i -= 1) {
|
|
2599
|
+
const item = state.cmItems[i];
|
|
2600
|
+
const proposal = item && item.memoryProposal;
|
|
2601
|
+
if (!proposal || typeof proposal !== "object") continue;
|
|
2602
|
+
if ((proposalId && proposal.id === proposalId) || (!proposalId && !updated)) {
|
|
2603
|
+
proposal.uiStatus = statusText;
|
|
2604
|
+
item.status = /ignored/i.test(statusText) ? "ignored" : "accepted";
|
|
2605
|
+
updated = true;
|
|
2606
|
+
if (proposalId) break;
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
if (updated) {
|
|
2610
|
+
saveCmItems();
|
|
2611
|
+
renderCmItems();
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
function taskSurfaceView(task) {
|
|
2616
|
+
return task && task.view && typeof task.view === "object" ? task.view : null;
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
function rememberAgentName(agentName) {
|
|
2620
|
+
if (typeof agentName !== "string" || !agentName.trim()) return;
|
|
2621
|
+
state.agentName = agentName.trim();
|
|
2622
|
+
el.chatMeta.textContent = state.agentName;
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
function isChatTaskActive(task) {
|
|
2626
|
+
if (!task || typeof task !== "object") return false;
|
|
2627
|
+
if (task.stage === "done" || task.stage === "blocked") return false;
|
|
2628
|
+
if (task.status === "succeeded" || task.status === "failed" || task.status === "info") return false;
|
|
2629
|
+
return true;
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
function isStaleChatTask(task) {
|
|
2633
|
+
if (!task || typeof task !== "object") return false;
|
|
2634
|
+
const createdAt = typeof task.createdAt === "string" ? Date.parse(task.createdAt) : NaN;
|
|
2635
|
+
if (!Number.isFinite(createdAt)) return false;
|
|
2636
|
+
return Date.now() - createdAt > CHAT_ACTIVE_TASK_RECOVERY_MS;
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
function isRecoverableChatTask(task) {
|
|
2640
|
+
if (!isChatTaskActive(task)) return false;
|
|
2641
|
+
if (isStaleChatTask(task)) return false;
|
|
2642
|
+
const data = task.data && typeof task.data === "object" ? task.data : {};
|
|
2643
|
+
return task.route === "core_cm"
|
|
2644
|
+
&& data.source === "local_chat"
|
|
2645
|
+
&& task.conversationId === chatConversationId();
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
function persistActiveChatTask(record) {
|
|
2649
|
+
if (!record || !record.taskId) return;
|
|
2650
|
+
try {
|
|
2651
|
+
localStorage.setItem(activeChatTaskKey(), JSON.stringify({
|
|
2652
|
+
taskId: record.taskId,
|
|
2653
|
+
requestId: record.requestId || "",
|
|
2654
|
+
conversationId: chatConversationId(),
|
|
2655
|
+
startedAt: record.startedAt || new Date().toISOString(),
|
|
2656
|
+
}));
|
|
2657
|
+
} catch {
|
|
2658
|
+
// Active task recovery is best-effort UI state.
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
function loadPersistedActiveChatTask() {
|
|
2663
|
+
try {
|
|
2664
|
+
const raw = localStorage.getItem(activeChatTaskKey());
|
|
2665
|
+
const parsed = raw ? JSON.parse(raw) : null;
|
|
2666
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
2667
|
+
if (typeof parsed.taskId !== "string" || !parsed.taskId) return null;
|
|
2668
|
+
const startedAt = typeof parsed.startedAt === "string" ? parsed.startedAt : "";
|
|
2669
|
+
const ageMs = Date.now() - Date.parse(startedAt || "0");
|
|
2670
|
+
if (Number.isFinite(ageMs) && ageMs > CHAT_ACTIVE_TASK_RECOVERY_MS) {
|
|
2671
|
+
localStorage.removeItem(activeChatTaskKey());
|
|
2672
|
+
return null;
|
|
2673
|
+
}
|
|
2674
|
+
return {
|
|
2675
|
+
taskId: parsed.taskId,
|
|
2676
|
+
requestId: typeof parsed.requestId === "string" ? parsed.requestId : "",
|
|
2677
|
+
startedAt,
|
|
2678
|
+
};
|
|
2679
|
+
} catch {
|
|
2680
|
+
return null;
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
function clearPersistedActiveChatTask(taskId) {
|
|
2685
|
+
try {
|
|
2686
|
+
const active = loadPersistedActiveChatTask();
|
|
2687
|
+
if (!active || !taskId || active.taskId === taskId) {
|
|
2688
|
+
localStorage.removeItem(activeChatTaskKey());
|
|
2689
|
+
}
|
|
2690
|
+
} catch {
|
|
2691
|
+
// Active task recovery is best-effort UI state.
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
function restoreActiveChatTask(record, options) {
|
|
2696
|
+
if (!record || !record.taskId) return;
|
|
2697
|
+
state.activeChatTaskId = record.taskId;
|
|
2698
|
+
state.activeChatRequestId = record.requestId || "";
|
|
2699
|
+
setChatBusy(true);
|
|
2700
|
+
if (!options || options.status !== false) {
|
|
2701
|
+
setStatus("Akemon is still handling the previous request");
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
function clearActiveChatTask(taskId, options) {
|
|
2706
|
+
if (taskId && state.activeChatTaskId && state.activeChatTaskId !== taskId) return;
|
|
2707
|
+
const clearedTaskId = state.activeChatTaskId || taskId || "";
|
|
2708
|
+
state.activeChatTaskId = "";
|
|
2709
|
+
state.activeChatRequestId = "";
|
|
2710
|
+
setChatBusy(false);
|
|
2711
|
+
clearPersistedActiveChatTask(clearedTaskId);
|
|
2712
|
+
if (!options || options.status !== false) setStatus("Akemon is ready");
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
function restoreActiveChatTaskFromStorage() {
|
|
2716
|
+
const active = loadPersistedActiveChatTask();
|
|
2717
|
+
if (!active) return;
|
|
2718
|
+
restoreActiveChatTask(active, { status: false });
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
function reconcileActiveChatTaskFromTasks(tasks) {
|
|
2722
|
+
if (!Array.isArray(tasks)) return;
|
|
2723
|
+
if (state.activeChatTaskId) {
|
|
2724
|
+
const current = tasks.find((task) => task && task.taskId === state.activeChatTaskId);
|
|
2725
|
+
if (current && (isStaleChatTask(current) || !isChatTaskActive(current))) {
|
|
2726
|
+
clearActiveChatTask(current.taskId, { status: false });
|
|
2727
|
+
}
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
const active = tasks.find(isRecoverableChatTask);
|
|
2731
|
+
if (active) {
|
|
2732
|
+
persistActiveChatTask({ taskId: active.taskId, requestId: "", startedAt: active.createdAt || active.updatedAt });
|
|
2733
|
+
restoreActiveChatTask({ taskId: active.taskId, requestId: "" }, { status: false });
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
async function refreshActiveChatTask(options) {
|
|
2738
|
+
if (!state.token || !state.activeChatTaskId) return;
|
|
2739
|
+
const taskId = state.activeChatTaskId;
|
|
2740
|
+
try {
|
|
2741
|
+
const data = await api("/self/tasks/" + encodeURIComponent(taskId), { method: "GET" });
|
|
2742
|
+
const task = data.task || null;
|
|
2743
|
+
if (task && isStaleChatTask(task)) {
|
|
2744
|
+
clearActiveChatTask(taskId, { status: false });
|
|
2745
|
+
if (!options || options.quiet !== true) setStatus("Akemon is ready");
|
|
2746
|
+
return;
|
|
2747
|
+
}
|
|
2748
|
+
if (!task || !isChatTaskActive(task)) {
|
|
2749
|
+
if (task) completeActiveChatTaskFromTask(task, { quiet: options?.quiet === true });
|
|
2750
|
+
clearActiveChatTask(taskId, { status: options?.quiet === true ? false : true });
|
|
2751
|
+
if (task && (!options || options.quiet !== true)) refreshActivity({ quiet: true }).catch(() => {});
|
|
2752
|
+
} else if (!options || options.quiet !== true) {
|
|
2753
|
+
setStatus(chatTaskSummary(task) || "Akemon is handling the request");
|
|
2754
|
+
}
|
|
2755
|
+
} catch (error) {
|
|
2756
|
+
if (!options || options.quiet !== true) throw error;
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
function setChatBusy(busy) {
|
|
2761
|
+
state.chatBusy = busy;
|
|
2762
|
+
el.chatSend.disabled = false;
|
|
2763
|
+
el.chatSend.textContent = busy ? "Queue" : "Send";
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
function chatTaskSummary(task) {
|
|
2767
|
+
if (!task || typeof task !== "object") return "";
|
|
2768
|
+
const view = taskSurfaceView(task);
|
|
2769
|
+
const stage = view?.stage?.label || activityStageLabel(task.stage) || activityStatusLabel(task.status, task);
|
|
2770
|
+
const summary = view?.summary || view?.nextAction || task.summary || task.nextAction || "";
|
|
2771
|
+
return [stage, summary].filter(Boolean).join(" - ");
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
function appendChatMessage(role, text, error, options) {
|
|
2775
|
+
options = options || {};
|
|
2776
|
+
const empty = el.chatMessages.querySelector(".chat-empty");
|
|
2777
|
+
if (empty) empty.remove();
|
|
2778
|
+
const item = document.createElement("div");
|
|
2779
|
+
item.className = "chat-message " + (role === "user" ? "user" : "akemon") + (error ? " error" : "");
|
|
2780
|
+
const label = document.createElement("div");
|
|
2781
|
+
label.className = "chat-message-role";
|
|
2782
|
+
label.textContent = role === "user" ? "You" : "Akemon";
|
|
2783
|
+
const body = document.createElement("div");
|
|
2784
|
+
body.className = "chat-message-text";
|
|
2785
|
+
body.textContent = text || "";
|
|
2786
|
+
item.appendChild(label);
|
|
2787
|
+
item.appendChild(body);
|
|
2788
|
+
const taskText = chatTaskSummary(options.task);
|
|
2789
|
+
if (taskText) {
|
|
2790
|
+
const task = document.createElement("div");
|
|
2791
|
+
task.className = "chat-message-task";
|
|
2792
|
+
task.textContent = taskText;
|
|
2793
|
+
item.appendChild(task);
|
|
2794
|
+
}
|
|
2795
|
+
el.chatMessages.appendChild(item);
|
|
2796
|
+
el.chatMessages.scrollTop = el.chatMessages.scrollHeight;
|
|
2797
|
+
if (options.memoryProposal && !options.fromHistory) {
|
|
2798
|
+
appendCmItem({
|
|
2799
|
+
type: "memory_proposal",
|
|
2800
|
+
title: "Memory CM",
|
|
2801
|
+
text: memoryProposalText(options.memoryProposal),
|
|
2802
|
+
memoryProposal: options.memoryProposal,
|
|
2803
|
+
eventIds: options.eventIds || null,
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2806
|
+
if (!options.fromHistory) {
|
|
2807
|
+
state.chatHistory.push({
|
|
2808
|
+
role: role === "user" ? "user" : "akemon",
|
|
2809
|
+
text: text || "",
|
|
2810
|
+
error: error === true,
|
|
2811
|
+
task: options.task || null,
|
|
2812
|
+
ts: new Date().toISOString(),
|
|
2813
|
+
});
|
|
2814
|
+
saveChatHistory();
|
|
2815
|
+
}
|
|
2816
|
+
return item;
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
function renderMemoryProposal(proposal, options) {
|
|
2820
|
+
options = options || {};
|
|
2821
|
+
const box = document.createElement("div");
|
|
2822
|
+
box.className = "memory-proposal";
|
|
2823
|
+
box.dataset.proposalId = proposal.id || "";
|
|
2824
|
+
|
|
2825
|
+
const title = document.createElement("div");
|
|
2826
|
+
title.className = "memory-proposal-title";
|
|
2827
|
+
title.textContent = "Memory suggestion";
|
|
2828
|
+
box.appendChild(title);
|
|
2829
|
+
|
|
2830
|
+
const text = document.createElement("div");
|
|
2831
|
+
text.className = "memory-proposal-text";
|
|
2832
|
+
text.textContent = memoryProposalText(proposal);
|
|
2833
|
+
box.appendChild(text);
|
|
2834
|
+
|
|
2835
|
+
const status = document.createElement("div");
|
|
2836
|
+
status.className = "memory-proposal-status";
|
|
2837
|
+
if (proposal.uiStatus) {
|
|
2838
|
+
status.textContent = proposal.uiStatus;
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
if (!options.readonly && !proposal.uiStatus) {
|
|
2842
|
+
const actions = document.createElement("div");
|
|
2843
|
+
actions.className = "memory-proposal-actions";
|
|
2844
|
+
const remember = document.createElement("button");
|
|
2845
|
+
remember.className = "btn btn-primary";
|
|
2846
|
+
remember.type = "button";
|
|
2847
|
+
remember.textContent = "Remember";
|
|
2848
|
+
remember.addEventListener("click", () => handleMemoryProposalAction(proposal, "accept", box));
|
|
2849
|
+
const ignore = document.createElement("button");
|
|
2850
|
+
ignore.className = "btn";
|
|
2851
|
+
ignore.type = "button";
|
|
2852
|
+
ignore.textContent = "Ignore";
|
|
2853
|
+
ignore.addEventListener("click", () => handleMemoryProposalAction(proposal, "reject", box));
|
|
2854
|
+
actions.appendChild(remember);
|
|
2855
|
+
actions.appendChild(ignore);
|
|
2856
|
+
box.appendChild(actions);
|
|
2857
|
+
} else if (options.readonly && !status.textContent) {
|
|
2858
|
+
status.textContent = "From local chat history.";
|
|
2859
|
+
}
|
|
2860
|
+
box.appendChild(status);
|
|
2861
|
+
return box;
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
function memoryProposalText(proposal) {
|
|
2865
|
+
const lines = [];
|
|
2866
|
+
if (proposal.summary) lines.push(proposal.summary);
|
|
2867
|
+
if (Array.isArray(proposal.self) && proposal.self.length) {
|
|
2868
|
+
lines.push("Akemon can remember: " + proposal.self.join("; "));
|
|
2869
|
+
}
|
|
2870
|
+
if (Array.isArray(proposal.work) && proposal.work.length) {
|
|
2871
|
+
lines.push("Work memory: " + proposal.work.join("; "));
|
|
2872
|
+
}
|
|
2873
|
+
if (!lines.length) return "Akemon found something that may be useful to remember.";
|
|
2874
|
+
return lines.join("\n");
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
async function handleMemoryProposalAction(proposal, action, box) {
|
|
2878
|
+
if (state.chatBusy) return;
|
|
2879
|
+
const buttons = box.querySelectorAll("button");
|
|
2880
|
+
buttons.forEach((button) => { button.disabled = true; });
|
|
2881
|
+
const status = box.querySelector(".memory-proposal-status");
|
|
2882
|
+
if (status) status.textContent = action === "accept" ? "Remembering..." : "Ignoring...";
|
|
2883
|
+
setStatus(action === "accept" ? "Saving memory" : "Ignoring memory suggestion");
|
|
2884
|
+
try {
|
|
2885
|
+
const command = action === "accept"
|
|
2886
|
+
? `/memory accept ${proposal.id || "latest"} all`
|
|
2887
|
+
: `/memory reject ${proposal.id || "latest"}`;
|
|
2888
|
+
const result = await sendAkemonMessage(command);
|
|
2889
|
+
const statusText = action === "accept" ? "Remembered." : "Ignored.";
|
|
2890
|
+
if (status) status.textContent = statusText;
|
|
2891
|
+
updateStoredMemoryProposalStatus(proposal.id || "", statusText);
|
|
2892
|
+
updateStoredCmMemoryProposalStatus(proposal.id || "", statusText);
|
|
2893
|
+
appendChatMessage("akemon", result.output, false, { task: result.task });
|
|
2894
|
+
result.cmItems.forEach((item) => appendCmItem(item));
|
|
2895
|
+
setStatus(action === "accept" ? "Memory saved" : "Memory suggestion ignored");
|
|
2896
|
+
} catch (error) {
|
|
2897
|
+
buttons.forEach((button) => { button.disabled = false; });
|
|
2898
|
+
const message = friendlyChatError(error);
|
|
2899
|
+
if (status) status.textContent = message;
|
|
2900
|
+
setStatus(message, true);
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
function createAkemonChatMessage(text, options) {
|
|
2905
|
+
options = options || {};
|
|
2906
|
+
const message = {
|
|
2907
|
+
schemaVersion: 1,
|
|
2908
|
+
type: "akemon.message",
|
|
2909
|
+
id: randomId("msg"),
|
|
2910
|
+
source: { kind: "owner", id: "owner", transport: "local" },
|
|
2911
|
+
target: { kind: "agent", id: state.agentName, transport: "local" },
|
|
2912
|
+
createdAt: new Date().toISOString(),
|
|
2913
|
+
conversationId: chatConversationId(),
|
|
2914
|
+
memoryScope: "owner",
|
|
2915
|
+
permissions: {},
|
|
2916
|
+
transport: "local",
|
|
2917
|
+
payload: {
|
|
2918
|
+
text,
|
|
2919
|
+
format: "text",
|
|
2920
|
+
kind: "chat",
|
|
2921
|
+
},
|
|
2922
|
+
};
|
|
2923
|
+
if (options.followUpForTaskId) {
|
|
2924
|
+
message.metadata = { followUpForTaskId: options.followUpForTaskId };
|
|
2925
|
+
}
|
|
2926
|
+
return message;
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
function localChatTaskIdFromMessage(message) {
|
|
2930
|
+
const suffix = message && message.id ? message.id : (message && message.conversationId ? message.conversationId : "local_" + Date.now());
|
|
2931
|
+
return "task_" + String(suffix).replace(/[^a-zA-Z0-9_.:-]/g, "_");
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
async function ensureAgentName() {
|
|
2935
|
+
if (state.agentName) return;
|
|
2936
|
+
await refreshSessions({ quiet: true });
|
|
2937
|
+
if (!state.agentName) throw new Error("Akemon name unavailable");
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
async function sendAkemonMessage(text, options) {
|
|
2941
|
+
return sendAkemonMessageEnvelope(createAkemonChatMessage(text, options));
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
async function sendAkemonMessageEnvelope(message, options) {
|
|
2945
|
+
options = options || {};
|
|
2946
|
+
if (!requireOwnerToken()) throw new Error("Owner token required");
|
|
2947
|
+
await ensureAgentName();
|
|
2948
|
+
const controller = new AbortController();
|
|
2949
|
+
const timeout = setTimeout(() => controller.abort(), CHAT_REQUEST_TIMEOUT_MS);
|
|
2950
|
+
try {
|
|
2951
|
+
const data = await api("/self/message", {
|
|
2952
|
+
method: "POST",
|
|
2953
|
+
body: JSON.stringify(Object.assign({ message }, options.wait === false ? { wait: false } : {})),
|
|
2954
|
+
signal: controller.signal,
|
|
2955
|
+
});
|
|
2956
|
+
const task = data.task || null;
|
|
2957
|
+
const taskView = data.taskView || task?.view || null;
|
|
2958
|
+
if (task && taskView && !task.view) task.view = taskView;
|
|
2959
|
+
return {
|
|
2960
|
+
accepted: data.accepted === true,
|
|
2961
|
+
queued: data.queued === true,
|
|
2962
|
+
async: data.async === true,
|
|
2963
|
+
wait: data.wait,
|
|
2964
|
+
output: normalizeAkemonOutput(data.output || (data.response && data.response.payload && data.response.payload.text) || ""),
|
|
2965
|
+
memoryProposal: data.memoryProposal || null,
|
|
2966
|
+
eventIds: data.eventIds || null,
|
|
2967
|
+
cmItems: Array.isArray(data.cmItems) ? data.cmItems : [],
|
|
2968
|
+
task,
|
|
2969
|
+
taskView,
|
|
2970
|
+
};
|
|
2971
|
+
} catch (error) {
|
|
2972
|
+
if (error && error.name === "AbortError") {
|
|
2973
|
+
throw new Error("Akemon did not answer before the local request timed out.");
|
|
2974
|
+
}
|
|
2975
|
+
throw error;
|
|
2976
|
+
} finally {
|
|
2977
|
+
clearTimeout(timeout);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
function normalizeAkemonOutput(output) {
|
|
2982
|
+
const text = String(output || "").trim();
|
|
2983
|
+
if (!text) return "Akemon did not return any text for this message.";
|
|
2984
|
+
const engineError = text.match(/^I could not complete the Core CM response because the engine returned an error:\s*(.+)$/i);
|
|
2985
|
+
if (engineError) return `Akemon could not get a usable engine response: ${engineError[1]}`;
|
|
2986
|
+
if (/engine returned an empty response/i.test(text)) {
|
|
2987
|
+
return "Akemon's engine returned an empty response. Try sending the request again.";
|
|
2988
|
+
}
|
|
2989
|
+
return text;
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
function friendlyChatError(error) {
|
|
2993
|
+
const name = error && error.name ? error.name : "";
|
|
2994
|
+
const message = error && error.message ? error.message : String(error || "");
|
|
2995
|
+
if (name === "AbortError" || /timed out|timeout/i.test(message)) {
|
|
2996
|
+
return "Akemon took too long to answer. I restored your draft so you can try again.";
|
|
2997
|
+
}
|
|
2998
|
+
if (/Owner token required/i.test(message)) {
|
|
2999
|
+
return "Owner token required. Paste the owner token and try again.";
|
|
3000
|
+
}
|
|
3001
|
+
if (/Owner token rejected/i.test(message)) {
|
|
3002
|
+
return "Owner token rejected. Paste the current owner token and try again.";
|
|
3003
|
+
}
|
|
3004
|
+
if (/Failed to fetch|NetworkError|Load failed|fetch/i.test(message)) {
|
|
3005
|
+
return "I could not reach the local Akemon service. Check that the 4321 service is running, then try again.";
|
|
3006
|
+
}
|
|
3007
|
+
return message || "Akemon could not send this message.";
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
function chatTaskReportText(task) {
|
|
3011
|
+
if (!task || typeof task !== "object") return "";
|
|
3012
|
+
const view = taskSurfaceView(task);
|
|
3013
|
+
const reviews = Array.isArray(view?.reviews) ? view.reviews : [];
|
|
3014
|
+
const latestReview = reviews.length ? reviews[reviews.length - 1] : null;
|
|
3015
|
+
const data = task.data && typeof task.data === "object" ? task.data : {};
|
|
3016
|
+
return (view?.report && view.report.text)
|
|
3017
|
+
|| (latestReview && latestReview.reportText)
|
|
3018
|
+
|| (typeof data.reportText === "string" ? data.reportText : "")
|
|
3019
|
+
|| task.summary
|
|
3020
|
+
|| task.nextAction
|
|
3021
|
+
|| "";
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
function completeActiveChatTaskFromTask(task, options) {
|
|
3025
|
+
options = options || {};
|
|
3026
|
+
if (!task || !task.taskId || state.completedChatTaskIds.has(task.taskId)) return;
|
|
3027
|
+
state.completedChatTaskIds.add(task.taskId);
|
|
3028
|
+
const failed = task.status === "failed" || task.stage === "blocked";
|
|
3029
|
+
const output = normalizeAkemonOutput(chatTaskReportText(task));
|
|
3030
|
+
appendChatMessage("akemon", output, failed, { task });
|
|
3031
|
+
if (failed) setStatus("Akemon task stopped", true);
|
|
3032
|
+
else if (!options.quiet) setStatus("Akemon replied");
|
|
3033
|
+
refreshActivity({ quiet: true }).catch(() => {});
|
|
3034
|
+
refreshSessions({ quiet: true }).catch(() => {});
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
async function watchActiveChatTask(taskId, requestId) {
|
|
3038
|
+
if (!taskId) return;
|
|
3039
|
+
for (;;) {
|
|
3040
|
+
if (!state.activeChatTaskId || state.activeChatTaskId !== taskId) return;
|
|
3041
|
+
if (requestId && state.activeChatRequestId && state.activeChatRequestId !== requestId) return;
|
|
3042
|
+
try {
|
|
3043
|
+
const data = await api("/self/tasks/" + encodeURIComponent(taskId), { method: "GET" });
|
|
3044
|
+
const task = data.task || null;
|
|
3045
|
+
if (task && isStaleChatTask(task)) {
|
|
3046
|
+
clearActiveChatTask(taskId, { status: false });
|
|
3047
|
+
setStatus("Akemon is ready");
|
|
3048
|
+
return;
|
|
3049
|
+
}
|
|
3050
|
+
if (task && !isChatTaskActive(task)) {
|
|
3051
|
+
completeActiveChatTaskFromTask(task);
|
|
3052
|
+
clearActiveChatTask(taskId, { status: false });
|
|
3053
|
+
return;
|
|
3054
|
+
}
|
|
3055
|
+
if (task) setStatus(chatTaskSummary(task) || "Akemon is handling the request");
|
|
3056
|
+
} catch (error) {
|
|
3057
|
+
if (!state.pageUnloading) setStatus(friendlyChatError(error), true);
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
3060
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
async function submitChat(event) {
|
|
3065
|
+
event.preventDefault();
|
|
3066
|
+
const draft = el.chatInput.value;
|
|
3067
|
+
const text = draft.trim();
|
|
3068
|
+
if (!text) return;
|
|
3069
|
+
el.chatInput.value = "";
|
|
3070
|
+
const isFollowUp = state.chatBusy && !!state.activeChatTaskId;
|
|
3071
|
+
appendChatMessage("user", text);
|
|
3072
|
+
if (isFollowUp) {
|
|
3073
|
+
setStatus("Queueing follow-up");
|
|
3074
|
+
try {
|
|
3075
|
+
const result = await sendAkemonMessage(text, { followUpForTaskId: state.activeChatTaskId });
|
|
3076
|
+
appendChatMessage("akemon", result.output, false, { task: result.task });
|
|
3077
|
+
refreshActivity({ quiet: true }).catch(() => {});
|
|
3078
|
+
setStatus("Follow-up queued");
|
|
3079
|
+
} catch (error) {
|
|
3080
|
+
if (state.pageUnloading) return;
|
|
3081
|
+
if (!el.chatInput.value.trim()) el.chatInput.value = draft;
|
|
3082
|
+
const message = friendlyChatError(error);
|
|
3083
|
+
appendChatMessage("akemon", message, true);
|
|
3084
|
+
setStatus(message, true);
|
|
3085
|
+
} finally {
|
|
3086
|
+
if (!state.pageUnloading) el.chatInput.focus();
|
|
3087
|
+
}
|
|
3088
|
+
return;
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
const message = createAkemonChatMessage(text);
|
|
3092
|
+
const requestId = message.id;
|
|
3093
|
+
state.activeChatTaskId = localChatTaskIdFromMessage(message);
|
|
3094
|
+
state.activeChatRequestId = requestId;
|
|
3095
|
+
persistActiveChatTask({
|
|
3096
|
+
taskId: state.activeChatTaskId,
|
|
3097
|
+
requestId,
|
|
3098
|
+
startedAt: message.createdAt,
|
|
3099
|
+
});
|
|
3100
|
+
setChatBusy(true);
|
|
3101
|
+
setStatus("Akemon is framing the request");
|
|
3102
|
+
let keepActiveTask = false;
|
|
3103
|
+
try {
|
|
3104
|
+
const result = await sendAkemonMessageEnvelope(message, { wait: false });
|
|
3105
|
+
if (result.async || result.accepted) {
|
|
3106
|
+
const taskId = result.task?.taskId || state.activeChatTaskId;
|
|
3107
|
+
state.activeChatTaskId = taskId;
|
|
3108
|
+
persistActiveChatTask({
|
|
3109
|
+
taskId,
|
|
3110
|
+
requestId,
|
|
3111
|
+
startedAt: message.createdAt,
|
|
3112
|
+
});
|
|
3113
|
+
keepActiveTask = true;
|
|
3114
|
+
setStatus(chatTaskSummary(result.task) || "Akemon is handling the request");
|
|
3115
|
+
watchActiveChatTask(taskId, requestId).catch((error) => {
|
|
3116
|
+
if (!state.pageUnloading) setStatus(friendlyChatError(error), true);
|
|
3117
|
+
});
|
|
3118
|
+
refreshActivity({ quiet: true }).catch(() => {});
|
|
3119
|
+
return;
|
|
3120
|
+
}
|
|
3121
|
+
appendChatMessage("akemon", result.output, false, {
|
|
3122
|
+
memoryProposal: result.memoryProposal,
|
|
3123
|
+
eventIds: result.eventIds,
|
|
3124
|
+
task: result.task,
|
|
3125
|
+
});
|
|
3126
|
+
result.cmItems.forEach((item) => appendCmItem(item));
|
|
3127
|
+
refreshActivity({ quiet: true }).catch(() => {});
|
|
3128
|
+
setStatus("Akemon replied");
|
|
3129
|
+
refreshSessions({ quiet: true }).catch(() => {});
|
|
3130
|
+
} catch (error) {
|
|
3131
|
+
if (state.pageUnloading) return;
|
|
3132
|
+
if (!el.chatInput.value.trim()) el.chatInput.value = draft;
|
|
3133
|
+
const message = friendlyChatError(error);
|
|
3134
|
+
appendChatMessage("akemon", message, true);
|
|
3135
|
+
setStatus(message, true);
|
|
3136
|
+
} finally {
|
|
3137
|
+
if (!keepActiveTask && !state.pageUnloading && state.activeChatRequestId === requestId) {
|
|
3138
|
+
clearActiveChatTask(state.activeChatTaskId, { status: false });
|
|
3139
|
+
}
|
|
3140
|
+
if (!state.pageUnloading) el.chatInput.focus();
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
function createTerminal() {
|
|
3145
|
+
if (!window.Terminal) {
|
|
3146
|
+
setStatus("xterm failed to load", true);
|
|
3147
|
+
return null;
|
|
3148
|
+
}
|
|
3149
|
+
const term = new Terminal({
|
|
3150
|
+
cursorBlink: false,
|
|
3151
|
+
cursorInactiveStyle: "block",
|
|
3152
|
+
cursorStyle: "block",
|
|
3153
|
+
convertEol: true,
|
|
3154
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
3155
|
+
fontSize: FONT_SIZE,
|
|
3156
|
+
lineHeight: LINE_HEIGHT,
|
|
3157
|
+
scrollback: 5000,
|
|
3158
|
+
theme: {
|
|
3159
|
+
background: "#ffffff",
|
|
3160
|
+
foreground: "#1a1a1a",
|
|
3161
|
+
cursor: "#3a8a52",
|
|
3162
|
+
cursorAccent: "#ffffff",
|
|
3163
|
+
selectionBackground: "#c8e0c4",
|
|
3164
|
+
selectionForeground: "#1a1a1a",
|
|
3165
|
+
black: "#1a1a1a",
|
|
3166
|
+
red: "#c43830",
|
|
3167
|
+
green: "#2a7a3a",
|
|
3168
|
+
yellow: "#8a6d10",
|
|
3169
|
+
blue: "#2060a8",
|
|
3170
|
+
magenta: "#8a3a8a",
|
|
3171
|
+
cyan: "#1a7a7a",
|
|
3172
|
+
white: "#d0ccc5",
|
|
3173
|
+
brightBlack: "#666360",
|
|
3174
|
+
brightRed: "#e05040",
|
|
3175
|
+
brightGreen: "#3a9a4a",
|
|
3176
|
+
brightYellow: "#a08520",
|
|
3177
|
+
brightBlue: "#3080c8",
|
|
3178
|
+
brightMagenta: "#a050a0",
|
|
3179
|
+
brightCyan: "#2a9a9a",
|
|
3180
|
+
brightWhite: "#f8f7f6",
|
|
3181
|
+
},
|
|
3182
|
+
});
|
|
3183
|
+
if (window.FitAddon) {
|
|
3184
|
+
state.fitAddon = new FitAddon.FitAddon();
|
|
3185
|
+
term.loadAddon(state.fitAddon);
|
|
3186
|
+
}
|
|
3187
|
+
if (window.WebLinksAddon) {
|
|
3188
|
+
term.loadAddon(new WebLinksAddon.WebLinksAddon());
|
|
3189
|
+
}
|
|
3190
|
+
if (window.SearchAddon) {
|
|
3191
|
+
state.searchAddon = new SearchAddon.SearchAddon();
|
|
3192
|
+
term.loadAddon(state.searchAddon);
|
|
3193
|
+
}
|
|
3194
|
+
term.open(el.terminal);
|
|
3195
|
+
state.term = term;
|
|
3196
|
+
el.terminal.addEventListener("pointerdown", focusTerminalForPointer, true);
|
|
3197
|
+
el.terminal.addEventListener("mousedown", focusTerminalForPointer, true);
|
|
3198
|
+
el.terminal.addEventListener("click", () => refreshTerminal());
|
|
3199
|
+
if (typeof term.attachCustomKeyEventHandler === "function") {
|
|
3200
|
+
term.attachCustomKeyEventHandler((event) => {
|
|
3201
|
+
if (event.type !== "keydown" || event.key !== "Enter" || !event.shiftKey) return true;
|
|
3202
|
+
const session = activeSession();
|
|
3203
|
+
if (!state.activeId || !session || session.status !== "running" || session.inputMode !== "tui") return true;
|
|
3204
|
+
event.preventDefault();
|
|
3205
|
+
sendInput(state.activeId, "\n").catch((error) => setStatus(error.message, true));
|
|
3206
|
+
return false;
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3209
|
+
if (typeof term.attachCustomWheelEventHandler === "function") {
|
|
3210
|
+
term.attachCustomWheelEventHandler((event) => handleTerminalWheel(event));
|
|
3211
|
+
}
|
|
3212
|
+
term.onData((data) => {
|
|
3213
|
+
if (!state.activeId) return;
|
|
3214
|
+
const session = activeSession();
|
|
3215
|
+
if (!session || session.status !== "running") return;
|
|
3216
|
+
const input = filterTerminalControlResponses(data);
|
|
3217
|
+
if (!input) return;
|
|
3218
|
+
sendInput(state.activeId, input).catch((error) => setStatus(error.message, true));
|
|
3219
|
+
});
|
|
3220
|
+
term.onResize((dims) => syncTerminalSize(dims, true));
|
|
3221
|
+
resizeTerminal(false, true);
|
|
3222
|
+
requestTerminalResize(false, true);
|
|
3223
|
+
setTimeout(() => requestTerminalResize(false, true), 60);
|
|
3224
|
+
return term;
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
function updateStartFormHints() {
|
|
3228
|
+
if (el.tool.value === "custom") {
|
|
3229
|
+
el.command.placeholder = "cmd required";
|
|
3230
|
+
} else {
|
|
3231
|
+
el.command.placeholder = "cmd override";
|
|
3232
|
+
}
|
|
3233
|
+
if (el.inputMode.value === "auto") {
|
|
3234
|
+
el.inputMode.title = (el.tool.value === "codex" || el.tool.value === "cursor")
|
|
3235
|
+
? "auto: tui"
|
|
3236
|
+
: "auto: line";
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
function resizeTerminal(send, force) {
|
|
3241
|
+
if (!state.term) return;
|
|
3242
|
+
if (!isTerminalRenderable()) return;
|
|
3243
|
+
if (state.fitAddon) {
|
|
3244
|
+
state.fitAddon.fit();
|
|
3245
|
+
}
|
|
3246
|
+
syncTerminalSize({ cols: state.term.cols, rows: state.term.rows }, send, force);
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
function syncTerminalSize(dims, send, force) {
|
|
3250
|
+
if (!dims || !Number.isFinite(dims.cols) || !Number.isFinite(dims.rows)) return;
|
|
3251
|
+
const changed = dims.cols !== state.lastResize.cols || dims.rows !== state.lastResize.rows;
|
|
3252
|
+
state.lastResize = dims;
|
|
3253
|
+
el.sizeText.textContent = dims.cols + "x" + dims.rows;
|
|
3254
|
+
const session = activeSession();
|
|
3255
|
+
if (send !== false && session && session.status === "running" && (changed || force === true)) {
|
|
3256
|
+
api("/self/workbench/sessions/" + encodeURIComponent(session.sessionId) + "/resize", {
|
|
3257
|
+
method: "POST",
|
|
3258
|
+
body: JSON.stringify(dims),
|
|
3259
|
+
}).catch((error) => setStatus(error.message, true));
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
function requestTerminalResize(send, force) {
|
|
3264
|
+
const shouldSend = send !== false;
|
|
3265
|
+
if (state.resizeFrame) cancelAnimationFrame(state.resizeFrame);
|
|
3266
|
+
state.resizeFrame = requestAnimationFrame(() => {
|
|
3267
|
+
state.resizeFrame = 0;
|
|
3268
|
+
resizeTerminal(shouldSend, force);
|
|
3269
|
+
});
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
function isTerminalRenderable() {
|
|
3273
|
+
if (document.hidden) return false;
|
|
3274
|
+
const rect = el.terminal.getBoundingClientRect();
|
|
3275
|
+
return rect.width > 0 && rect.height > 0;
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
function isFormControlFocused() {
|
|
3279
|
+
const active = document.activeElement;
|
|
3280
|
+
if (!active) return false;
|
|
3281
|
+
return active.tagName === "INPUT" || active.tagName === "SELECT" || active.tagName === "TEXTAREA";
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
function restoreTerminalFocus(force) {
|
|
3285
|
+
if (!state.term || !isTerminalRenderable()) return;
|
|
3286
|
+
if (!force && isFormControlFocused()) return;
|
|
3287
|
+
const refresh = () => {
|
|
3288
|
+
if (!state.term || !isTerminalRenderable()) return;
|
|
3289
|
+
ensureTerminalCursorVisible();
|
|
3290
|
+
state.term.focus();
|
|
3291
|
+
state.term.refresh(0, Math.max(0, state.term.rows - 1));
|
|
3292
|
+
};
|
|
3293
|
+
refresh();
|
|
3294
|
+
requestAnimationFrame(refresh);
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
function handleTerminalWheel(event) {
|
|
3298
|
+
if (!state.term) return true;
|
|
3299
|
+
if (event.ctrlKey) return true;
|
|
3300
|
+
const lines = terminalWheelLines(event);
|
|
3301
|
+
event.preventDefault();
|
|
3302
|
+
event.stopPropagation();
|
|
3303
|
+
if (lines !== 0) state.term.scrollLines(lines);
|
|
3304
|
+
return false;
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
function terminalWheelLines(event) {
|
|
3308
|
+
const linePixels = Math.max(1, FONT_SIZE * LINE_HEIGHT);
|
|
3309
|
+
let delta = event.deltaY || 0;
|
|
3310
|
+
if (event.deltaMode === 1) {
|
|
3311
|
+
delta = event.deltaY;
|
|
3312
|
+
} else if (event.deltaMode === 2) {
|
|
3313
|
+
delta = event.deltaY * Math.max(1, Math.floor((state.term ? state.term.rows : 24) * 0.8));
|
|
3314
|
+
} else {
|
|
3315
|
+
delta = event.deltaY / linePixels;
|
|
3316
|
+
}
|
|
3317
|
+
state.wheelScrollRemainder += delta;
|
|
3318
|
+
const whole = state.wheelScrollRemainder < 0
|
|
3319
|
+
? Math.ceil(state.wheelScrollRemainder)
|
|
3320
|
+
: Math.floor(state.wheelScrollRemainder);
|
|
3321
|
+
if (whole !== 0) state.wheelScrollRemainder -= whole;
|
|
3322
|
+
return whole;
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
function focusTerminalForPointer() {
|
|
3326
|
+
if (!state.term || !isTerminalRenderable()) return;
|
|
3327
|
+
ensureTerminalCursorVisible();
|
|
3328
|
+
state.term.focus();
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
function filterTerminalControlResponses(data) {
|
|
3332
|
+
let text = state.terminalResponseBuffer + data;
|
|
3333
|
+
state.terminalResponseBuffer = "";
|
|
3334
|
+
let output = "";
|
|
3335
|
+
let index = 0;
|
|
3336
|
+
while (index < text.length) {
|
|
3337
|
+
const oscStart = text.indexOf("\x1b]", index);
|
|
3338
|
+
const csiStart = text.indexOf("\x1b[", index);
|
|
3339
|
+
const start = minPositiveIndex(oscStart, csiStart);
|
|
3340
|
+
if (start === -1) {
|
|
3341
|
+
output += text.slice(index);
|
|
3342
|
+
break;
|
|
3343
|
+
}
|
|
3344
|
+
output += text.slice(index, start);
|
|
3345
|
+
|
|
3346
|
+
if (start === oscStart) {
|
|
3347
|
+
const parsed = readOscSequence(text, start);
|
|
3348
|
+
if (!parsed) return bufferTerminalResponseRemainder(text, start, output);
|
|
3349
|
+
if (!isColorQueryResponse(parsed.sequence)) output += parsed.sequence;
|
|
3350
|
+
index = parsed.end;
|
|
3351
|
+
continue;
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
const parsed = readCsiSequence(text, start);
|
|
3355
|
+
if (!parsed) return bufferTerminalResponseRemainder(text, start, output);
|
|
3356
|
+
if (!isTerminalGeneratedCsiResponse(parsed.sequence)) output += parsed.sequence;
|
|
3357
|
+
index = parsed.end;
|
|
3358
|
+
}
|
|
3359
|
+
return output;
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
function minPositiveIndex(left, right) {
|
|
3363
|
+
if (left === -1) return right;
|
|
3364
|
+
if (right === -1) return left;
|
|
3365
|
+
return Math.min(left, right);
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
function readOscSequence(text, start) {
|
|
3369
|
+
const belEnd = text.indexOf("\x07", start);
|
|
3370
|
+
const stEnd = text.indexOf("\x1b\\", start);
|
|
3371
|
+
const hasBel = belEnd !== -1 && (stEnd === -1 || belEnd < stEnd);
|
|
3372
|
+
const end = hasBel ? belEnd : stEnd;
|
|
3373
|
+
if (end === -1) return null;
|
|
3374
|
+
return {
|
|
3375
|
+
sequence: text.slice(start, end + (hasBel ? 1 : 2)),
|
|
3376
|
+
end: end + (hasBel ? 1 : 2),
|
|
3377
|
+
};
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
function readCsiSequence(text, start) {
|
|
3381
|
+
for (let i = start + 2; i < text.length; i += 1) {
|
|
3382
|
+
const code = text.charCodeAt(i);
|
|
3383
|
+
if (code >= 0x40 && code <= 0x7e) {
|
|
3384
|
+
return { sequence: text.slice(start, i + 1), end: i + 1 };
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
return null;
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3390
|
+
function bufferTerminalResponseRemainder(text, start, output) {
|
|
3391
|
+
state.terminalResponseBuffer = text.slice(start);
|
|
3392
|
+
if (state.terminalResponseBuffer.length > TERMINAL_RESPONSE_BUFFER_LIMIT) {
|
|
3393
|
+
output += state.terminalResponseBuffer;
|
|
3394
|
+
state.terminalResponseBuffer = "";
|
|
3395
|
+
}
|
|
3396
|
+
return output;
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
function isColorQueryResponse(sequence) {
|
|
3400
|
+
return /^\x1b\](10|11|12);rgb:[0-9a-f]{1,4}\/[0-9a-f]{1,4}\/[0-9a-f]{1,4}(\x07|\x1b\\)$/i.test(sequence);
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
function isTerminalGeneratedCsiResponse(sequence) {
|
|
3404
|
+
return /^\x1b\[(\?|>)?[0-9;]*c$/.test(sequence)
|
|
3405
|
+
|| /^\x1b\[\??[0-9]+;[0-9]+R$/.test(sequence)
|
|
3406
|
+
|| /^\x1b\[\??[0-9;]*n$/.test(sequence);
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
function shouldForceTerminalCursor() {
|
|
3410
|
+
const session = activeSession();
|
|
3411
|
+
return !!session && (session.tool === "shell" || session.tool === "custom");
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
function ensureTerminalCursorVisible() {
|
|
3415
|
+
if (!state.term || !shouldForceTerminalCursor()) return;
|
|
3416
|
+
state.term.write("\x1b[?25h");
|
|
3417
|
+
state.lastCursorControl = "forced visible";
|
|
3418
|
+
updateDebugText();
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3421
|
+
function trackCursorControl(chunk) {
|
|
3422
|
+
if (typeof chunk !== "string") return;
|
|
3423
|
+
if (chunk.includes("\x1b[?25l")) state.lastCursorControl = "hidden by output";
|
|
3424
|
+
if (chunk.includes("\x1b[?25h")) state.lastCursorControl = "shown by output";
|
|
3425
|
+
updateDebugText();
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
function writeTerminalOutput(chunk) {
|
|
3429
|
+
if (!state.term || !chunk) return;
|
|
3430
|
+
trackCursorControl(chunk);
|
|
3431
|
+
state.term.write(chunk);
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
function updateDebugText() {
|
|
3435
|
+
if (!el.debugText) return;
|
|
3436
|
+
el.debugText.textContent = state.lastCursorControl ? "cursor: " + state.lastCursorControl : "";
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
function refreshTerminal() {
|
|
3440
|
+
if (!state.term || !isTerminalRenderable()) return;
|
|
3441
|
+
const refresh = () => {
|
|
3442
|
+
if (state.term && isTerminalRenderable()) state.term.refresh(0, Math.max(0, state.term.rows - 1));
|
|
3443
|
+
};
|
|
3444
|
+
refresh();
|
|
3445
|
+
requestAnimationFrame(refresh);
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
function activeSession() {
|
|
3449
|
+
return state.sessions.find((session) => session.sessionId === state.activeId) || null;
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
function sessionSelectLabel(session) {
|
|
3453
|
+
const name = session.label || session.tool || session.sessionId;
|
|
3454
|
+
return "[" + session.status + "] " + name;
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
function renderTerminalSessionSelect() {
|
|
3458
|
+
const select = el.terminalSessionSelect;
|
|
3459
|
+
if (!select) return;
|
|
3460
|
+
const sessions = visibleSessionList();
|
|
3461
|
+
select.textContent = "";
|
|
3462
|
+
if (!sessions.length) {
|
|
3463
|
+
const option = document.createElement("option");
|
|
3464
|
+
option.value = "";
|
|
3465
|
+
option.textContent = "No sessions";
|
|
3466
|
+
select.appendChild(option);
|
|
3467
|
+
select.disabled = true;
|
|
3468
|
+
return;
|
|
3469
|
+
}
|
|
3470
|
+
select.disabled = false;
|
|
3471
|
+
for (const session of sessions) {
|
|
3472
|
+
const option = document.createElement("option");
|
|
3473
|
+
option.value = session.sessionId;
|
|
3474
|
+
option.textContent = sessionSelectLabel(session);
|
|
3475
|
+
option.title = session.commandLineDisplay || session.sessionId;
|
|
3476
|
+
select.appendChild(option);
|
|
3477
|
+
}
|
|
3478
|
+
if (state.activeId && sessions.some((session) => session.sessionId === state.activeId)) {
|
|
3479
|
+
select.value = state.activeId;
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3483
|
+
function updateActiveHeader() {
|
|
3484
|
+
const session = activeSession();
|
|
3485
|
+
renderTerminalSessionSelect();
|
|
3486
|
+
if (!session) {
|
|
3487
|
+
el.activeStatus.textContent = "idle";
|
|
3488
|
+
el.activeStatus.className = "status-pill";
|
|
3489
|
+
el.activeCommand.textContent = "No active session";
|
|
3490
|
+
el.activeInputMode.disabled = true;
|
|
3491
|
+
el.activeInputMode.value = "line";
|
|
3492
|
+
return;
|
|
3493
|
+
}
|
|
3494
|
+
el.activeStatus.textContent = session.status;
|
|
3495
|
+
el.activeStatus.className = "status-pill " + (session.status === "running" ? "status-running" : "status-ended");
|
|
3496
|
+
el.activeCommand.textContent = (session.commandLineDisplay || session.sessionId) + " [" + (session.inputMode || "line") + "]";
|
|
3497
|
+
el.activeInputMode.disabled = session.status !== "running";
|
|
3498
|
+
el.activeInputMode.value = session.inputMode || "line";
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
function renderSessions() {
|
|
3502
|
+
const visibleSessions = visibleSessionList();
|
|
3503
|
+
const hiddenCount = Math.max(0, state.sessions.length - visibleSessions.length);
|
|
3504
|
+
el.sessionHistoryToggle.textContent = state.showSessionHistory ? "Active" : "History";
|
|
3505
|
+
el.sessionHistoryToggle.title = state.showSessionHistory
|
|
3506
|
+
? "Show only active and recent sessions"
|
|
3507
|
+
: "Show all completed sessions";
|
|
3508
|
+
|
|
3509
|
+
if (!visibleSessions.length) {
|
|
3510
|
+
state.sessionItems.clear();
|
|
3511
|
+
el.list.innerHTML = "";
|
|
3512
|
+
const empty = document.createElement("div");
|
|
3513
|
+
empty.className = "empty";
|
|
3514
|
+
empty.textContent = state.sessions.length ? "No active sessions." : "No sessions.";
|
|
3515
|
+
el.list.appendChild(empty);
|
|
3516
|
+
if (hiddenCount > 0) el.list.appendChild(renderSessionHistoryNote(hiddenCount));
|
|
3517
|
+
updateActiveHeader();
|
|
3518
|
+
return;
|
|
3519
|
+
}
|
|
3520
|
+
const seen = new Set();
|
|
3521
|
+
const empty = el.list.querySelector(".empty");
|
|
3522
|
+
if (empty) empty.remove();
|
|
3523
|
+
for (const note of el.list.querySelectorAll(".session-history-note")) note.remove();
|
|
3524
|
+
for (const session of visibleSessions) {
|
|
3525
|
+
seen.add(session.sessionId);
|
|
3526
|
+
let item = state.sessionItems.get(session.sessionId);
|
|
3527
|
+
if (!item) {
|
|
3528
|
+
item = document.createElement("button");
|
|
3529
|
+
item.type = "button";
|
|
3530
|
+
item.innerHTML = [
|
|
3531
|
+
'<div class="session-line">',
|
|
3532
|
+
'<span class="session-name"></span>',
|
|
3533
|
+
'<span class="status-pill"></span>',
|
|
3534
|
+
'</div>',
|
|
3535
|
+
'<div class="session-meta"></div>',
|
|
3536
|
+
].join("");
|
|
3537
|
+
item.addEventListener("click", () => showSessionInTerminal(session.sessionId));
|
|
3538
|
+
state.sessionItems.set(session.sessionId, item);
|
|
3539
|
+
}
|
|
3540
|
+
item.className = "session-item" + (session.sessionId === state.activeId ? " active" : "");
|
|
3541
|
+
item.querySelector(".session-name").textContent = session.label || session.tool || session.sessionId;
|
|
3542
|
+
const pill = item.querySelector(".status-pill");
|
|
3543
|
+
pill.textContent = session.status;
|
|
3544
|
+
pill.className = "status-pill";
|
|
3545
|
+
if (session.status === "running") pill.classList.add("status-running");
|
|
3546
|
+
else pill.classList.add("status-ended");
|
|
3547
|
+
item.querySelector(".session-meta").textContent = (session.inputMode || "line") + " · " + (session.commandLineDisplay || session.sessionId);
|
|
3548
|
+
if (item.parentNode !== el.list) el.list.appendChild(item);
|
|
3549
|
+
}
|
|
3550
|
+
for (const [sessionId, item] of state.sessionItems) {
|
|
3551
|
+
if (seen.has(sessionId)) continue;
|
|
3552
|
+
item.remove();
|
|
3553
|
+
state.sessionItems.delete(sessionId);
|
|
3554
|
+
}
|
|
3555
|
+
for (const session of visibleSessions) {
|
|
3556
|
+
const item = state.sessionItems.get(session.sessionId);
|
|
3557
|
+
if (item && item.parentNode === el.list && el.list.lastElementChild !== item) el.list.appendChild(item);
|
|
3558
|
+
}
|
|
3559
|
+
if (hiddenCount > 0 && !state.showSessionHistory) el.list.appendChild(renderSessionHistoryNote(hiddenCount));
|
|
3560
|
+
updateActiveHeader();
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
function visibleSessionList() {
|
|
3564
|
+
if (state.showSessionHistory) return state.sessions;
|
|
3565
|
+
|
|
3566
|
+
const visibleIds = new Set();
|
|
3567
|
+
const recentCompleted = [];
|
|
3568
|
+
for (const session of state.sessions) {
|
|
3569
|
+
if (session.status === "running" || session.sessionId === state.activeId) {
|
|
3570
|
+
visibleIds.add(session.sessionId);
|
|
3571
|
+
continue;
|
|
3572
|
+
}
|
|
3573
|
+
if (recentCompleted.length < 5) recentCompleted.push(session.sessionId);
|
|
3574
|
+
}
|
|
3575
|
+
recentCompleted.forEach((sessionId) => visibleIds.add(sessionId));
|
|
3576
|
+
return state.sessions.filter((session) => visibleIds.has(session.sessionId));
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3579
|
+
function renderSessionHistoryNote(hiddenCount) {
|
|
3580
|
+
const note = document.createElement("div");
|
|
3581
|
+
note.className = "session-history-note";
|
|
3582
|
+
note.textContent = hiddenCount + " older completed session" + (hiddenCount === 1 ? " is" : "s are") + " hidden.";
|
|
3583
|
+
return note;
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
function mergeSession(snapshot) {
|
|
3587
|
+
const index = state.sessions.findIndex((session) => session.sessionId === snapshot.sessionId);
|
|
3588
|
+
if (index === -1) state.sessions.unshift(snapshot);
|
|
3589
|
+
else state.sessions[index] = Object.assign({}, state.sessions[index], snapshot);
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
async function refreshSessions(options) {
|
|
3593
|
+
if (!state.token) {
|
|
3594
|
+
if (!options || options.quiet !== true) requireOwnerToken();
|
|
3595
|
+
return;
|
|
3596
|
+
}
|
|
3597
|
+
const previousActiveId = state.activeId;
|
|
3598
|
+
const data = await api("/self/workbench/sessions", { method: "GET" });
|
|
3599
|
+
rememberAgentName(data.agentName);
|
|
3600
|
+
state.sessions = Array.isArray(data.sessions) ? data.sessions : [];
|
|
3601
|
+
const serverActiveId = typeof data.activeSessionId === "string" ? data.activeSessionId : "";
|
|
3602
|
+
if (serverActiveId && state.sessions.some((session) => session.sessionId === serverActiveId)) {
|
|
3603
|
+
state.activeId = serverActiveId;
|
|
3604
|
+
}
|
|
3605
|
+
if (!state.activeId && state.sessions[0]) state.activeId = state.sessions[0].sessionId;
|
|
3606
|
+
if (state.activeId && !activeSession()) state.activeId = "";
|
|
3607
|
+
if (!state.activeId && state.sessions[0]) state.activeId = state.sessions[0].sessionId;
|
|
3608
|
+
renderSessions();
|
|
3609
|
+
if (state.activeId && state.activeId !== previousActiveId) {
|
|
3610
|
+
await activateSession(state.activeId);
|
|
3611
|
+
} else {
|
|
3612
|
+
updateActiveHeader();
|
|
3613
|
+
}
|
|
3614
|
+
setStatus("Connected");
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
async function activateSession(sessionId, keepStream) {
|
|
3618
|
+
const activationRequestId = ++state.activationRequestId;
|
|
3619
|
+
state.activeId = sessionId;
|
|
3620
|
+
renderSessions();
|
|
3621
|
+
let syncedSession = null;
|
|
3622
|
+
try {
|
|
3623
|
+
syncedSession = await syncActiveSession(sessionId);
|
|
3624
|
+
} catch (error) {
|
|
3625
|
+
if (isCurrentActivation(sessionId, activationRequestId)) setStatus(error.message, true);
|
|
3626
|
+
}
|
|
3627
|
+
if (!isCurrentActivation(sessionId, activationRequestId)) {
|
|
3628
|
+
return;
|
|
3629
|
+
}
|
|
3630
|
+
if (syncedSession) {
|
|
3631
|
+
mergeSession(syncedSession);
|
|
3632
|
+
renderSessions();
|
|
3633
|
+
}
|
|
3634
|
+
if (!state.term) createTerminal();
|
|
3635
|
+
const session = activeSession();
|
|
3636
|
+
if (state.term) {
|
|
3637
|
+
resizeTerminal(true, true);
|
|
3638
|
+
state.terminalResponseBuffer = "";
|
|
3639
|
+
state.wheelScrollRemainder = 0;
|
|
3640
|
+
state.term.reset();
|
|
3641
|
+
if (session && session.tail) writeTerminalOutput(session.tail);
|
|
3642
|
+
restoreTerminalFocus(true);
|
|
3643
|
+
requestTerminalResize(true, true);
|
|
3644
|
+
setTimeout(() => requestTerminalResize(true, true), 60);
|
|
3645
|
+
}
|
|
3646
|
+
updateActiveHeader();
|
|
3647
|
+
if (!keepStream) startStream(sessionId);
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
async function showSessionInTerminal(sessionId) {
|
|
3651
|
+
try {
|
|
3652
|
+
await activateSession(sessionId);
|
|
3653
|
+
setActivePage("terminal");
|
|
3654
|
+
} catch (error) {
|
|
3655
|
+
setStatus(error.message, true);
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
function isCurrentActivation(sessionId, activationRequestId) {
|
|
3660
|
+
return state.activationRequestId === activationRequestId && state.activeId === sessionId;
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
async function syncActiveSession(sessionId) {
|
|
3664
|
+
if (!state.token || !sessionId) return;
|
|
3665
|
+
const data = await api("/self/workbench/sessions/" + encodeURIComponent(sessionId) + "/activate", {
|
|
3666
|
+
method: "POST",
|
|
3667
|
+
});
|
|
3668
|
+
return data && data.session ? data.session : null;
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
function clearStreamRetry() {
|
|
3672
|
+
if (!state.streamRetryTimer) return;
|
|
3673
|
+
clearTimeout(state.streamRetryTimer);
|
|
3674
|
+
state.streamRetryTimer = 0;
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
function shouldReconnectStream(sessionId, controller) {
|
|
3678
|
+
if (controller.signal.aborted) return false;
|
|
3679
|
+
if (state.streamAbort !== controller || state.activeId !== sessionId) return false;
|
|
3680
|
+
const session = activeSession();
|
|
3681
|
+
return !session || session.status === "running";
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
function scheduleStreamReconnect(sessionId, controller, attempt) {
|
|
3685
|
+
if (!shouldReconnectStream(sessionId, controller)) return;
|
|
3686
|
+
const delay = Math.min(30000, 1000 * Math.pow(2, Math.min(attempt, 5)));
|
|
3687
|
+
clearStreamRetry();
|
|
3688
|
+
setStatus("Stream disconnected; retry " + (attempt + 1) + " in " + Math.round(delay / 1000) + "s", true);
|
|
3689
|
+
state.streamRetryTimer = setTimeout(() => {
|
|
3690
|
+
state.streamRetryTimer = 0;
|
|
3691
|
+
if (shouldReconnectStream(sessionId, controller)) startStream(sessionId, attempt + 1);
|
|
3692
|
+
}, delay);
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
function startStream(sessionId, attempt) {
|
|
3696
|
+
clearStreamRetry();
|
|
3697
|
+
if (state.streamAbort) state.streamAbort.abort();
|
|
3698
|
+
const controller = new AbortController();
|
|
3699
|
+
state.streamAbort = controller;
|
|
3700
|
+
const retryAttempt = attempt || 0;
|
|
3701
|
+
if (retryAttempt > 0) setStatus("Reconnecting stream " + sessionId + " (attempt " + retryAttempt + ")");
|
|
3702
|
+
|
|
3703
|
+
fetch("/self/workbench/sessions/" + encodeURIComponent(sessionId) + "/stream", {
|
|
3704
|
+
method: "GET",
|
|
3705
|
+
headers: authHeaders(),
|
|
3706
|
+
signal: controller.signal,
|
|
3707
|
+
}).then(async (res) => {
|
|
3708
|
+
if (!res.ok) {
|
|
3709
|
+
const error = new Error("stream HTTP " + res.status);
|
|
3710
|
+
error.status = res.status;
|
|
3711
|
+
throw error;
|
|
3712
|
+
}
|
|
3713
|
+
if (!res.body) throw new Error("stream body unavailable");
|
|
3714
|
+
setStatus("Streaming " + sessionId);
|
|
3715
|
+
const reader = res.body.getReader();
|
|
3716
|
+
const decoder = new TextDecoder();
|
|
3717
|
+
let buffer = "";
|
|
3718
|
+
for (;;) {
|
|
3719
|
+
const result = await reader.read();
|
|
3720
|
+
if (result.done) break;
|
|
3721
|
+
buffer += decoder.decode(result.value, { stream: true });
|
|
3722
|
+
const parts = buffer.split("\n\n");
|
|
3723
|
+
buffer = parts.pop() || "";
|
|
3724
|
+
for (const part of parts) handleSseBlock(part);
|
|
3725
|
+
}
|
|
3726
|
+
scheduleStreamReconnect(sessionId, controller, retryAttempt);
|
|
3727
|
+
}).catch((error) => {
|
|
3728
|
+
if (controller.signal.aborted) return;
|
|
3729
|
+
if (error.status === 401 || error.status === 403) {
|
|
3730
|
+
clearOwnerToken("Owner token rejected");
|
|
3731
|
+
return;
|
|
3732
|
+
}
|
|
3733
|
+
if (error.status === 404) {
|
|
3734
|
+
setStatus("Stream session not found", true);
|
|
3735
|
+
return;
|
|
3736
|
+
}
|
|
3737
|
+
setStatus(error.message, true);
|
|
3738
|
+
scheduleStreamReconnect(sessionId, controller, retryAttempt);
|
|
3739
|
+
});
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3742
|
+
function handleSseBlock(block) {
|
|
3743
|
+
const lines = block.split(/\r?\n/);
|
|
3744
|
+
let eventName = "message";
|
|
3745
|
+
let dataText = "";
|
|
3746
|
+
for (const line of lines) {
|
|
3747
|
+
if (line.startsWith("event:")) eventName = line.slice(6).trim();
|
|
3748
|
+
else if (line.startsWith("data:")) dataText += line.slice(5).trim();
|
|
3749
|
+
}
|
|
3750
|
+
if (!dataText) return;
|
|
3751
|
+
let data;
|
|
3752
|
+
try { data = JSON.parse(dataText); }
|
|
3753
|
+
catch { return; }
|
|
3754
|
+
if (data.snapshot) {
|
|
3755
|
+
mergeSession(data.snapshot);
|
|
3756
|
+
renderSessions();
|
|
3757
|
+
}
|
|
3758
|
+
if (eventName === "output" && data.sessionId === state.activeId && state.term) {
|
|
3759
|
+
const chunk = data.chunk || "";
|
|
3760
|
+
writeTerminalOutput(chunk);
|
|
3761
|
+
}
|
|
3762
|
+
if (eventName === "exit") {
|
|
3763
|
+
setStatus("Session ended");
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3767
|
+
async function sendInput(sessionId, input) {
|
|
3768
|
+
await api("/self/workbench/sessions/" + encodeURIComponent(sessionId) + "/input", {
|
|
3769
|
+
method: "POST",
|
|
3770
|
+
body: JSON.stringify({ input }),
|
|
3771
|
+
});
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
function splitArgs(input) {
|
|
3775
|
+
const tokens = [];
|
|
3776
|
+
let current = "";
|
|
3777
|
+
let quote = "";
|
|
3778
|
+
let escaping = false;
|
|
3779
|
+
for (const char of input) {
|
|
3780
|
+
if (escaping) {
|
|
3781
|
+
current += char;
|
|
3782
|
+
escaping = false;
|
|
3783
|
+
continue;
|
|
3784
|
+
}
|
|
3785
|
+
if (char === "\\") {
|
|
3786
|
+
escaping = true;
|
|
3787
|
+
continue;
|
|
3788
|
+
}
|
|
3789
|
+
if (quote) {
|
|
3790
|
+
if (char === quote) quote = "";
|
|
3791
|
+
else current += char;
|
|
3792
|
+
continue;
|
|
3793
|
+
}
|
|
3794
|
+
if (char === '"' || char === "'") {
|
|
3795
|
+
quote = char;
|
|
3796
|
+
continue;
|
|
3797
|
+
}
|
|
3798
|
+
if (/\s/.test(char)) {
|
|
3799
|
+
if (current) {
|
|
3800
|
+
tokens.push(current);
|
|
3801
|
+
current = "";
|
|
3802
|
+
}
|
|
3803
|
+
continue;
|
|
3804
|
+
}
|
|
3805
|
+
current += char;
|
|
3806
|
+
}
|
|
3807
|
+
if (escaping) throw new Error("Trailing escape in args");
|
|
3808
|
+
if (quote) throw new Error("Unterminated quote in args");
|
|
3809
|
+
if (current) tokens.push(current);
|
|
3810
|
+
return tokens;
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
async function startSession(event) {
|
|
3814
|
+
event.preventDefault();
|
|
3815
|
+
if (!requireOwnerToken()) return;
|
|
3816
|
+
if (!state.term) createTerminal();
|
|
3817
|
+
resizeTerminal(false);
|
|
3818
|
+
const request = {
|
|
3819
|
+
tool: el.tool.value,
|
|
3820
|
+
args: splitArgs(el.args.value.trim()),
|
|
3821
|
+
cols: state.term ? state.term.cols : state.lastResize.cols,
|
|
3822
|
+
rows: state.term ? state.term.rows : state.lastResize.rows,
|
|
3823
|
+
};
|
|
3824
|
+
if (el.command.value.trim()) request.command = el.command.value.trim();
|
|
3825
|
+
if (el.workdir.value.trim()) request.workdir = el.workdir.value.trim();
|
|
3826
|
+
if (el.label.value.trim()) request.label = el.label.value.trim();
|
|
3827
|
+
if (el.allowOutside.checked) request.allowOutsideWorkdir = true;
|
|
3828
|
+
if (el.inputMode.value === "line" || el.inputMode.value === "tui") request.inputMode = el.inputMode.value;
|
|
3829
|
+
|
|
3830
|
+
const data = await api("/self/workbench/sessions", {
|
|
3831
|
+
method: "POST",
|
|
3832
|
+
body: JSON.stringify(request),
|
|
3833
|
+
});
|
|
3834
|
+
mergeSession(data.session);
|
|
3835
|
+
renderSessions();
|
|
3836
|
+
await activateSession(data.session.sessionId);
|
|
3837
|
+
setActivePage("terminal");
|
|
3838
|
+
setStatus("Started " + data.session.sessionId);
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
async function setActiveInputMode() {
|
|
3842
|
+
if (!state.activeId) return;
|
|
3843
|
+
const inputMode = el.activeInputMode.value === "tui" ? "tui" : "line";
|
|
3844
|
+
const data = await api("/self/workbench/sessions/" + encodeURIComponent(state.activeId) + "/input-mode", {
|
|
3845
|
+
method: "POST",
|
|
3846
|
+
body: JSON.stringify({ inputMode }),
|
|
3847
|
+
});
|
|
3848
|
+
mergeSession(data.session);
|
|
3849
|
+
renderSessions();
|
|
3850
|
+
updateActiveHeader();
|
|
3851
|
+
setStatus("Input mode: " + inputMode);
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3854
|
+
async function stopActive() {
|
|
3855
|
+
if (!state.activeId) return;
|
|
3856
|
+
const data = await api("/self/workbench/sessions/" + encodeURIComponent(state.activeId) + "/stop", {
|
|
3857
|
+
method: "POST",
|
|
3858
|
+
});
|
|
3859
|
+
mergeSession(data.session);
|
|
3860
|
+
renderSessions();
|
|
3861
|
+
setStatus("Stopped " + state.activeId);
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3864
|
+
el.tokenSave.addEventListener("click", () => {
|
|
3865
|
+
const token = el.tokenInput.value.trim();
|
|
3866
|
+
if (!token) return;
|
|
3867
|
+
localStorage.setItem(TOKEN_STORAGE_KEY, token);
|
|
3868
|
+
state.token = token;
|
|
3869
|
+
setAuthVisible(false, "");
|
|
3870
|
+
refreshSessions().catch((error) => setStatus(error.message, true));
|
|
3871
|
+
});
|
|
3872
|
+
el.startForm.addEventListener("submit", (event) => {
|
|
3873
|
+
startSession(event).catch((error) => setStatus(error.message, true));
|
|
3874
|
+
});
|
|
3875
|
+
el.tool.addEventListener("change", updateStartFormHints);
|
|
3876
|
+
el.sessionHistoryToggle.addEventListener("click", () => {
|
|
3877
|
+
state.showSessionHistory = !state.showSessionHistory;
|
|
3878
|
+
renderSessions();
|
|
3879
|
+
});
|
|
3880
|
+
el.refresh.addEventListener("click", () => refreshSessions().catch((error) => setStatus(error.message, true)));
|
|
3881
|
+
el.resize.addEventListener("click", () => resizeTerminal(true, true));
|
|
3882
|
+
el.stop.addEventListener("click", () => stopActive().catch((error) => setStatus(error.message, true)));
|
|
3883
|
+
el.terminalSessionSelect.addEventListener("change", () => {
|
|
3884
|
+
const sessionId = el.terminalSessionSelect.value;
|
|
3885
|
+
if (!sessionId || sessionId === state.activeId) return;
|
|
3886
|
+
activateSession(sessionId).catch((error) => setStatus(error.message, true));
|
|
3887
|
+
});
|
|
3888
|
+
el.activityRefresh.addEventListener("click", () => refreshActivity().catch((error) => setStatus(error.message, true)));
|
|
3889
|
+
el.activitySearch.addEventListener("input", () => {
|
|
3890
|
+
reconcileActivitySelection();
|
|
3891
|
+
renderActivity();
|
|
3892
|
+
renderSelectedActivityDetail({ quiet: true }).catch((error) => setStatus(error.message, true));
|
|
3893
|
+
});
|
|
3894
|
+
el.activityStatusFilter.addEventListener("change", () => {
|
|
3895
|
+
state.activitySelectedTaskId = "";
|
|
3896
|
+
refreshActivity().catch((error) => setStatus(error.message, true));
|
|
3897
|
+
});
|
|
3898
|
+
el.activityRouteFilter.addEventListener("change", () => {
|
|
3899
|
+
state.activitySelectedTaskId = "";
|
|
3900
|
+
refreshActivity().catch((error) => setStatus(error.message, true));
|
|
3901
|
+
});
|
|
3902
|
+
el.activeInputMode.addEventListener("change", () => {
|
|
3903
|
+
setActiveInputMode().catch((error) => setStatus(error.message, true));
|
|
3904
|
+
});
|
|
3905
|
+
el.chatForm.addEventListener("submit", (event) => {
|
|
3906
|
+
submitChat(event).catch((error) => {
|
|
3907
|
+
const message = friendlyChatError(error);
|
|
3908
|
+
appendChatMessage("akemon", message, true);
|
|
3909
|
+
setStatus(message, true);
|
|
3910
|
+
setChatBusy(false);
|
|
3911
|
+
});
|
|
3912
|
+
});
|
|
3913
|
+
el.chatClear.addEventListener("click", clearChatHistory);
|
|
3914
|
+
function isChatInputComposing(event) {
|
|
3915
|
+
return event.isComposing
|
|
3916
|
+
|| state.chatComposing
|
|
3917
|
+
|| event.keyCode === 229
|
|
3918
|
+
|| Date.now() - state.chatCompositionEndedAt < 50;
|
|
3919
|
+
}
|
|
3920
|
+
el.chatInput.addEventListener("compositionstart", () => {
|
|
3921
|
+
state.chatComposing = true;
|
|
3922
|
+
});
|
|
3923
|
+
el.chatInput.addEventListener("compositionend", () => {
|
|
3924
|
+
state.chatComposing = false;
|
|
3925
|
+
state.chatCompositionEndedAt = Date.now();
|
|
3926
|
+
});
|
|
3927
|
+
el.chatInput.addEventListener("keydown", (event) => {
|
|
3928
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
3929
|
+
if (isChatInputComposing(event)) return;
|
|
3930
|
+
event.preventDefault();
|
|
3931
|
+
el.chatForm.requestSubmit();
|
|
3932
|
+
}
|
|
3933
|
+
});
|
|
3934
|
+
|
|
3935
|
+
let resizeTimer = 0;
|
|
3936
|
+
function scheduleTerminalResize() {
|
|
3937
|
+
clearTimeout(resizeTimer);
|
|
3938
|
+
resizeTimer = setTimeout(() => requestTerminalResize(true), 80);
|
|
3939
|
+
}
|
|
3940
|
+
window.addEventListener("resize", scheduleTerminalResize);
|
|
3941
|
+
window.addEventListener("pagehide", () => {
|
|
3942
|
+
state.pageUnloading = true;
|
|
3943
|
+
});
|
|
3944
|
+
window.addEventListener("beforeunload", () => {
|
|
3945
|
+
state.pageUnloading = true;
|
|
3946
|
+
});
|
|
3947
|
+
window.addEventListener("pageshow", () => {
|
|
3948
|
+
state.pageUnloading = false;
|
|
3949
|
+
});
|
|
3950
|
+
if (window.ResizeObserver) {
|
|
3951
|
+
const terminalObserver = new ResizeObserver(scheduleTerminalResize);
|
|
3952
|
+
terminalObserver.observe(el.terminal);
|
|
3953
|
+
terminalObserver.observe(el.terminal.parentElement);
|
|
3954
|
+
}
|
|
3955
|
+
|
|
3956
|
+
function toggleSearch(open) {
|
|
3957
|
+
el.searchBar.classList.toggle("open", open);
|
|
3958
|
+
if (open) {
|
|
3959
|
+
el.searchInput.focus();
|
|
3960
|
+
el.searchInput.select();
|
|
3961
|
+
} else {
|
|
3962
|
+
el.searchInput.value = "";
|
|
3963
|
+
if (state.searchAddon) state.searchAddon.clearDecorations();
|
|
3964
|
+
if (state.term) state.term.focus();
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
el.searchClose.addEventListener("click", () => toggleSearch(false));
|
|
3968
|
+
el.searchNext.addEventListener("click", () => {
|
|
3969
|
+
if (state.searchAddon && el.searchInput.value) state.searchAddon.findNext(el.searchInput.value);
|
|
3970
|
+
});
|
|
3971
|
+
el.searchPrev.addEventListener("click", () => {
|
|
3972
|
+
if (state.searchAddon && el.searchInput.value) state.searchAddon.findPrevious(el.searchInput.value);
|
|
3973
|
+
});
|
|
3974
|
+
el.searchInput.addEventListener("keydown", (e) => {
|
|
3975
|
+
if (e.key === "Enter") {
|
|
3976
|
+
if (state.searchAddon && el.searchInput.value) {
|
|
3977
|
+
if (e.shiftKey) state.searchAddon.findPrevious(el.searchInput.value);
|
|
3978
|
+
else state.searchAddon.findNext(el.searchInput.value);
|
|
3979
|
+
}
|
|
3980
|
+
e.preventDefault();
|
|
3981
|
+
}
|
|
3982
|
+
if (e.key === "Escape") {
|
|
3983
|
+
toggleSearch(false);
|
|
3984
|
+
e.preventDefault();
|
|
3985
|
+
}
|
|
3986
|
+
});
|
|
3987
|
+
document.addEventListener("keydown", (e) => {
|
|
3988
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "f") {
|
|
3989
|
+
e.preventDefault();
|
|
3990
|
+
toggleSearch(true);
|
|
3991
|
+
}
|
|
3992
|
+
});
|
|
3993
|
+
|
|
3994
|
+
loadChatHistory();
|
|
3995
|
+
loadCmItems();
|
|
3996
|
+
restoreActiveChatTaskFromStorage();
|
|
3997
|
+
initPageTabs();
|
|
3998
|
+
initToken();
|
|
3999
|
+
updateStartFormHints();
|
|
4000
|
+
createTerminal();
|
|
4001
|
+
refreshSessions({ quiet: true }).catch((error) => setStatus(error.message, true));
|
|
4002
|
+
refreshActivity({ quiet: true }).catch(() => {});
|
|
4003
|
+
refreshActiveChatTask({ quiet: true }).catch(() => {});
|
|
4004
|
+
setInterval(() => {
|
|
4005
|
+
if (state.token) refreshSessions({ quiet: true }).catch(() => {});
|
|
4006
|
+
if (state.token && state.activeChatTaskId) refreshActiveChatTask({ quiet: true }).catch(() => {});
|
|
4007
|
+
if (state.token && state.activePage === "activity") refreshActivity({ quiet: true }).catch(() => {});
|
|
4008
|
+
}, 5000);
|
|
4009
|
+
</script>
|
|
4010
|
+
</body>
|
|
4011
|
+
</html>
|