office-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.runtime-dist/scripts/bundle-host-package.js +46 -0
- package/.runtime-dist/scripts/demo-multi-agent.js +130 -0
- package/.runtime-dist/scripts/home-agent-host.js +1403 -0
- package/.runtime-dist/scripts/host-doctor.js +28 -0
- package/.runtime-dist/scripts/host-login.js +32 -0
- package/.runtime-dist/scripts/host-menu.js +227 -0
- package/.runtime-dist/scripts/host-open.js +20 -0
- package/.runtime-dist/scripts/install-host.js +108 -0
- package/.runtime-dist/scripts/lib/host-config.js +171 -0
- package/.runtime-dist/scripts/lib/local-runner.js +287 -0
- package/.runtime-dist/scripts/office-cli.js +698 -0
- package/.runtime-dist/scripts/run-local-project.js +277 -0
- package/.runtime-dist/src/auth/session-token.js +62 -0
- package/.runtime-dist/src/discord/outbox-ledger.js +56 -0
- package/.runtime-dist/src/do/AgentDO.js +205 -0
- package/.runtime-dist/src/do/GatewayShardDO.js +9 -0
- package/.runtime-dist/src/do/ProjectDO.js +829 -0
- package/.runtime-dist/src/do/TaskDO.js +356 -0
- package/.runtime-dist/src/index.js +123 -0
- package/.runtime-dist/src/project/office-view.js +405 -0
- package/.runtime-dist/src/project/read-model.js +79 -0
- package/.runtime-dist/src/routes/agents-bootstrap.js +9 -0
- package/.runtime-dist/src/routes/agents-descriptor.js +12 -0
- package/.runtime-dist/src/routes/agents-events.js +17 -0
- package/.runtime-dist/src/routes/agents-heartbeat.js +21 -0
- package/.runtime-dist/src/routes/agents-task-context.js +17 -0
- package/.runtime-dist/src/routes/bundles.js +198 -0
- package/.runtime-dist/src/routes/local-host.js +49 -0
- package/.runtime-dist/src/routes/projects.js +119 -0
- package/.runtime-dist/src/routes/tasks.js +67 -0
- package/.runtime-dist/src/task/reducer.js +464 -0
- package/.runtime-dist/src/types/project.js +1 -0
- package/.runtime-dist/src/types/protocol.js +3 -0
- package/.runtime-dist/src/types/runtime.js +1 -0
- package/README.md +148 -0
- package/bin/double-penetration-host.mjs +83 -0
- package/package.json +48 -0
- package/public/index.html +1581 -0
- package/public/install-host.ps1 +64 -0
- package/scripts/run-runtime-script.mjs +43 -0
|
@@ -0,0 +1,1581 @@
|
|
|
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" />
|
|
6
|
+
<title>Office Core</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
color-scheme: dark;
|
|
10
|
+
--bg: #171411;
|
|
11
|
+
--panel: rgba(33, 29, 25, 0.92);
|
|
12
|
+
--panel-soft: rgba(255, 255, 255, 0.03);
|
|
13
|
+
--panel-hover: rgba(255, 255, 255, 0.05);
|
|
14
|
+
--border: rgba(255, 255, 255, 0.08);
|
|
15
|
+
--border-strong: rgba(255, 255, 255, 0.16);
|
|
16
|
+
--text: #f3ede4;
|
|
17
|
+
--text-muted: #b4a99c;
|
|
18
|
+
--text-faint: #877d72;
|
|
19
|
+
--accent: #d7a35a;
|
|
20
|
+
--success: #66b57f;
|
|
21
|
+
--danger: #e37669;
|
|
22
|
+
--shadow: 0 20px 56px rgba(0, 0, 0, 0.32);
|
|
23
|
+
--radius-xl: 24px;
|
|
24
|
+
--radius-lg: 18px;
|
|
25
|
+
--radius-md: 14px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
* {
|
|
29
|
+
box-sizing: border-box;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
html,
|
|
33
|
+
body {
|
|
34
|
+
height: 100%;
|
|
35
|
+
margin: 0;
|
|
36
|
+
font-family:
|
|
37
|
+
ui-sans-serif,
|
|
38
|
+
system-ui,
|
|
39
|
+
-apple-system,
|
|
40
|
+
BlinkMacSystemFont,
|
|
41
|
+
"Segoe UI",
|
|
42
|
+
sans-serif;
|
|
43
|
+
background:
|
|
44
|
+
radial-gradient(circle at top left, rgba(215, 163, 90, 0.08), transparent 24%),
|
|
45
|
+
linear-gradient(180deg, #191511 0%, #14110f 100%);
|
|
46
|
+
color: var(--text);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
body {
|
|
50
|
+
min-height: 100vh;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
button,
|
|
54
|
+
input,
|
|
55
|
+
select,
|
|
56
|
+
textarea {
|
|
57
|
+
font: inherit;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
button {
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.app {
|
|
65
|
+
display: grid;
|
|
66
|
+
grid-template-columns: 300px minmax(0, 1fr);
|
|
67
|
+
min-height: 100vh;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.sidebar {
|
|
71
|
+
border-right: 1px solid var(--border);
|
|
72
|
+
background: rgba(23, 20, 17, 0.9);
|
|
73
|
+
backdrop-filter: blur(18px);
|
|
74
|
+
padding: 20px 16px;
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
gap: 16px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.brand {
|
|
81
|
+
display: flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
justify-content: space-between;
|
|
84
|
+
gap: 12px;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.brand-title {
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
gap: 10px;
|
|
91
|
+
font-weight: 700;
|
|
92
|
+
letter-spacing: 0.02em;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.brand-title::before {
|
|
96
|
+
content: "";
|
|
97
|
+
width: 12px;
|
|
98
|
+
height: 12px;
|
|
99
|
+
border-radius: 4px;
|
|
100
|
+
background: linear-gradient(135deg, var(--accent), #8b5d24);
|
|
101
|
+
box-shadow: 0 0 0 6px rgba(215, 163, 90, 0.12);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.section-title {
|
|
105
|
+
margin: 0;
|
|
106
|
+
font-size: 0.76rem;
|
|
107
|
+
text-transform: uppercase;
|
|
108
|
+
letter-spacing: 0.12em;
|
|
109
|
+
color: var(--text-faint);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.conversation-list {
|
|
113
|
+
display: flex;
|
|
114
|
+
flex-direction: column;
|
|
115
|
+
gap: 8px;
|
|
116
|
+
overflow: auto;
|
|
117
|
+
min-height: 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.conversation {
|
|
121
|
+
width: 100%;
|
|
122
|
+
text-align: left;
|
|
123
|
+
border: 1px solid transparent;
|
|
124
|
+
border-radius: var(--radius-md);
|
|
125
|
+
background: rgba(255, 255, 255, 0.02);
|
|
126
|
+
color: var(--text);
|
|
127
|
+
padding: 14px;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.conversation:hover {
|
|
131
|
+
background: var(--panel-hover);
|
|
132
|
+
border-color: var(--border);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.conversation.active {
|
|
136
|
+
background: rgba(215, 163, 90, 0.11);
|
|
137
|
+
border-color: rgba(215, 163, 90, 0.34);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.conversation-name {
|
|
141
|
+
font-size: 0.96rem;
|
|
142
|
+
font-weight: 600;
|
|
143
|
+
margin-bottom: 4px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.conversation-meta {
|
|
147
|
+
font-size: 0.82rem;
|
|
148
|
+
color: var(--text-muted);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.sidebar-card {
|
|
152
|
+
border: 1px solid var(--border);
|
|
153
|
+
border-radius: var(--radius-lg);
|
|
154
|
+
background: rgba(255, 255, 255, 0.03);
|
|
155
|
+
padding: 16px;
|
|
156
|
+
color: var(--text-muted);
|
|
157
|
+
line-height: 1.55;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.sidebar-card p {
|
|
161
|
+
margin: 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.actions {
|
|
165
|
+
display: flex;
|
|
166
|
+
gap: 10px;
|
|
167
|
+
flex-wrap: wrap;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.button {
|
|
171
|
+
border: 1px solid var(--border);
|
|
172
|
+
background: rgba(255, 255, 255, 0.03);
|
|
173
|
+
color: var(--text);
|
|
174
|
+
border-radius: 999px;
|
|
175
|
+
padding: 10px 15px;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.button:hover {
|
|
179
|
+
background: rgba(255, 255, 255, 0.06);
|
|
180
|
+
border-color: var(--border-strong);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.button.primary {
|
|
184
|
+
background: linear-gradient(180deg, rgba(215, 163, 90, 0.22), rgba(215, 163, 90, 0.1));
|
|
185
|
+
border-color: rgba(215, 163, 90, 0.34);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.button.small {
|
|
189
|
+
padding: 8px 12px;
|
|
190
|
+
font-size: 0.88rem;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.main {
|
|
194
|
+
padding: 18px 22px 22px;
|
|
195
|
+
display: grid;
|
|
196
|
+
grid-template-rows: auto auto auto minmax(0, 1fr) auto;
|
|
197
|
+
gap: 16px;
|
|
198
|
+
min-width: 0;
|
|
199
|
+
min-height: 100vh;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.topbar {
|
|
203
|
+
display: flex;
|
|
204
|
+
justify-content: space-between;
|
|
205
|
+
align-items: flex-start;
|
|
206
|
+
gap: 16px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.eyebrow {
|
|
210
|
+
font-size: 0.75rem;
|
|
211
|
+
text-transform: uppercase;
|
|
212
|
+
letter-spacing: 0.12em;
|
|
213
|
+
color: var(--text-faint);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.project-title {
|
|
217
|
+
margin: 6px 0 0;
|
|
218
|
+
font-size: clamp(1.5rem, 2.2vw, 2.1rem);
|
|
219
|
+
font-weight: 700;
|
|
220
|
+
line-height: 1.08;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.project-subtitle {
|
|
224
|
+
margin-top: 6px;
|
|
225
|
+
color: var(--text-muted);
|
|
226
|
+
line-height: 1.5;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.pill-row {
|
|
230
|
+
display: flex;
|
|
231
|
+
gap: 10px;
|
|
232
|
+
flex-wrap: wrap;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.pill {
|
|
236
|
+
display: inline-flex;
|
|
237
|
+
align-items: center;
|
|
238
|
+
gap: 8px;
|
|
239
|
+
border-radius: 999px;
|
|
240
|
+
padding: 8px 12px;
|
|
241
|
+
border: 1px solid var(--border);
|
|
242
|
+
background: rgba(255, 255, 255, 0.04);
|
|
243
|
+
color: var(--text-muted);
|
|
244
|
+
font-size: 0.85rem;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.pill::before {
|
|
248
|
+
content: "";
|
|
249
|
+
width: 8px;
|
|
250
|
+
height: 8px;
|
|
251
|
+
border-radius: 999px;
|
|
252
|
+
background: var(--success);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.summary-card,
|
|
256
|
+
.thread-card,
|
|
257
|
+
.composer-card {
|
|
258
|
+
border: 1px solid var(--border);
|
|
259
|
+
border-radius: var(--radius-xl);
|
|
260
|
+
background: var(--panel);
|
|
261
|
+
backdrop-filter: blur(16px);
|
|
262
|
+
box-shadow: var(--shadow);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.summary-card {
|
|
266
|
+
padding: 18px 20px;
|
|
267
|
+
display: grid;
|
|
268
|
+
gap: 14px;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.summary-grid {
|
|
272
|
+
display: grid;
|
|
273
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
274
|
+
gap: 12px;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.summary-block {
|
|
278
|
+
padding: 14px;
|
|
279
|
+
border-radius: var(--radius-md);
|
|
280
|
+
background: var(--panel-soft);
|
|
281
|
+
border: 1px solid var(--border);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.summary-label {
|
|
285
|
+
font-size: 0.75rem;
|
|
286
|
+
letter-spacing: 0.12em;
|
|
287
|
+
text-transform: uppercase;
|
|
288
|
+
color: var(--text-faint);
|
|
289
|
+
margin-bottom: 6px;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.summary-value {
|
|
293
|
+
line-height: 1.55;
|
|
294
|
+
color: var(--text);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.active-agents {
|
|
298
|
+
display: flex;
|
|
299
|
+
gap: 10px;
|
|
300
|
+
flex-wrap: wrap;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.agent-chip {
|
|
304
|
+
display: inline-flex;
|
|
305
|
+
align-items: center;
|
|
306
|
+
gap: 10px;
|
|
307
|
+
padding: 10px 12px;
|
|
308
|
+
border-radius: 999px;
|
|
309
|
+
background: rgba(255, 255, 255, 0.04);
|
|
310
|
+
border: 1px solid var(--border);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.agent-chip.active {
|
|
314
|
+
border-color: rgba(215, 163, 90, 0.34);
|
|
315
|
+
background: rgba(215, 163, 90, 0.11);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.agent-name {
|
|
319
|
+
font-weight: 600;
|
|
320
|
+
font-size: 0.93rem;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.agent-meta {
|
|
324
|
+
font-size: 0.79rem;
|
|
325
|
+
color: var(--text-muted);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.remove-agent {
|
|
329
|
+
width: 24px;
|
|
330
|
+
height: 24px;
|
|
331
|
+
border-radius: 999px;
|
|
332
|
+
border: 1px solid var(--border);
|
|
333
|
+
background: rgba(255, 255, 255, 0.03);
|
|
334
|
+
color: var(--text-muted);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.remove-agent:hover {
|
|
338
|
+
border-color: rgba(227, 118, 105, 0.3);
|
|
339
|
+
color: #ffd6d1;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.thread-card {
|
|
343
|
+
min-height: 0;
|
|
344
|
+
display: flex;
|
|
345
|
+
flex-direction: column;
|
|
346
|
+
overflow: hidden;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.thread-header {
|
|
350
|
+
padding: 18px 20px 0;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.thread-header h2 {
|
|
354
|
+
margin: 0;
|
|
355
|
+
font-size: 1.05rem;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.thread-header p {
|
|
359
|
+
margin: 6px 0 0;
|
|
360
|
+
color: var(--text-muted);
|
|
361
|
+
line-height: 1.5;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.thread {
|
|
365
|
+
min-height: 0;
|
|
366
|
+
overflow: auto;
|
|
367
|
+
padding: 18px 20px 20px;
|
|
368
|
+
display: flex;
|
|
369
|
+
flex-direction: column;
|
|
370
|
+
gap: 14px;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.empty-thread {
|
|
374
|
+
margin: auto 0;
|
|
375
|
+
padding: 40px 24px;
|
|
376
|
+
text-align: center;
|
|
377
|
+
color: var(--text-muted);
|
|
378
|
+
border: 1px dashed var(--border);
|
|
379
|
+
border-radius: var(--radius-lg);
|
|
380
|
+
line-height: 1.6;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.message {
|
|
384
|
+
display: flex;
|
|
385
|
+
flex-direction: column;
|
|
386
|
+
gap: 6px;
|
|
387
|
+
max-width: min(860px, 94%);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.message.user {
|
|
391
|
+
align-self: flex-end;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.message.agent,
|
|
395
|
+
.message.system {
|
|
396
|
+
align-self: flex-start;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.message-meta {
|
|
400
|
+
padding: 0 2px;
|
|
401
|
+
font-size: 0.8rem;
|
|
402
|
+
color: var(--text-faint);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.bubble {
|
|
406
|
+
padding: 16px 18px;
|
|
407
|
+
border-radius: 22px;
|
|
408
|
+
line-height: 1.6;
|
|
409
|
+
white-space: pre-wrap;
|
|
410
|
+
word-break: break-word;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.message.user .bubble {
|
|
414
|
+
background: linear-gradient(180deg, rgba(215, 163, 90, 0.21), rgba(215, 163, 90, 0.1));
|
|
415
|
+
border: 1px solid rgba(215, 163, 90, 0.28);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.message.agent .bubble {
|
|
419
|
+
background: rgba(255, 255, 255, 0.04);
|
|
420
|
+
border: 1px solid var(--border);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.message.system .bubble {
|
|
424
|
+
background: rgba(102, 181, 127, 0.08);
|
|
425
|
+
border: 1px solid rgba(102, 181, 127, 0.16);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.composer-card {
|
|
429
|
+
padding: 16px;
|
|
430
|
+
display: grid;
|
|
431
|
+
gap: 12px;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.composer-top {
|
|
435
|
+
display: flex;
|
|
436
|
+
justify-content: space-between;
|
|
437
|
+
align-items: center;
|
|
438
|
+
gap: 12px;
|
|
439
|
+
color: var(--text-muted);
|
|
440
|
+
font-size: 0.88rem;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
textarea,
|
|
444
|
+
select,
|
|
445
|
+
input {
|
|
446
|
+
width: 100%;
|
|
447
|
+
border-radius: 18px;
|
|
448
|
+
border: 1px solid var(--border);
|
|
449
|
+
background: rgba(0, 0, 0, 0.14);
|
|
450
|
+
color: var(--text);
|
|
451
|
+
padding: 14px 16px;
|
|
452
|
+
outline: none;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
textarea {
|
|
456
|
+
min-height: 116px;
|
|
457
|
+
resize: vertical;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.composer-actions {
|
|
461
|
+
display: flex;
|
|
462
|
+
justify-content: flex-end;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.modal-shell {
|
|
466
|
+
position: fixed;
|
|
467
|
+
inset: 0;
|
|
468
|
+
background: rgba(6, 5, 4, 0.62);
|
|
469
|
+
backdrop-filter: blur(18px);
|
|
470
|
+
display: none;
|
|
471
|
+
align-items: center;
|
|
472
|
+
justify-content: center;
|
|
473
|
+
padding: 20px;
|
|
474
|
+
z-index: 100;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.modal-shell.open {
|
|
478
|
+
display: flex;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.modal {
|
|
482
|
+
width: min(520px, 100%);
|
|
483
|
+
padding: 22px;
|
|
484
|
+
border-radius: 28px;
|
|
485
|
+
border: 1px solid var(--border);
|
|
486
|
+
background: #211d19;
|
|
487
|
+
box-shadow: var(--shadow);
|
|
488
|
+
display: grid;
|
|
489
|
+
gap: 16px;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.modal h3 {
|
|
493
|
+
margin: 0;
|
|
494
|
+
font-size: 1.16rem;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.modal p {
|
|
498
|
+
margin: 0;
|
|
499
|
+
color: var(--text-muted);
|
|
500
|
+
line-height: 1.5;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.field-grid {
|
|
504
|
+
display: grid;
|
|
505
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
506
|
+
gap: 12px;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.field {
|
|
510
|
+
display: grid;
|
|
511
|
+
gap: 8px;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.field label {
|
|
515
|
+
color: var(--text-muted);
|
|
516
|
+
font-size: 0.85rem;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.modal-actions {
|
|
520
|
+
display: flex;
|
|
521
|
+
justify-content: flex-end;
|
|
522
|
+
gap: 10px;
|
|
523
|
+
flex-wrap: wrap;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.code {
|
|
527
|
+
border-radius: 16px;
|
|
528
|
+
border: 1px solid var(--border);
|
|
529
|
+
background: rgba(0, 0, 0, 0.14);
|
|
530
|
+
padding: 14px;
|
|
531
|
+
color: var(--text-muted);
|
|
532
|
+
font-family: ui-monospace, Consolas, monospace;
|
|
533
|
+
font-size: 0.82rem;
|
|
534
|
+
line-height: 1.55;
|
|
535
|
+
white-space: pre-wrap;
|
|
536
|
+
word-break: break-word;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
@media (max-width: 960px) {
|
|
540
|
+
.app {
|
|
541
|
+
grid-template-columns: 1fr;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.sidebar {
|
|
545
|
+
border-right: 0;
|
|
546
|
+
border-bottom: 1px solid var(--border);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.summary-grid,
|
|
550
|
+
.field-grid {
|
|
551
|
+
grid-template-columns: 1fr;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
</style>
|
|
555
|
+
</head>
|
|
556
|
+
<body>
|
|
557
|
+
<div class="app">
|
|
558
|
+
<aside class="sidebar">
|
|
559
|
+
<div class="brand">
|
|
560
|
+
<div class="brand-title">Office Core</div>
|
|
561
|
+
<button class="button small" id="new-conversation-btn">New</button>
|
|
562
|
+
</div>
|
|
563
|
+
|
|
564
|
+
<div>
|
|
565
|
+
<h2 class="section-title">Conversations</h2>
|
|
566
|
+
</div>
|
|
567
|
+
<div class="conversation-list" id="conversation-list"></div>
|
|
568
|
+
|
|
569
|
+
<div class="sidebar-card">
|
|
570
|
+
<p>Only the current room stays live. Older conversations keep compacted context and get fresh agents when reopened.</p>
|
|
571
|
+
<div class="actions" style="margin-top: 12px">
|
|
572
|
+
<button class="button small" id="copy-install-btn">Copy install</button>
|
|
573
|
+
<button class="button small" id="open-config-btn">Configure</button>
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
</aside>
|
|
577
|
+
|
|
578
|
+
<main class="main">
|
|
579
|
+
<section class="topbar">
|
|
580
|
+
<div>
|
|
581
|
+
<div class="eyebrow">Current project</div>
|
|
582
|
+
<h1 class="project-title" id="project-title">Office Core</h1>
|
|
583
|
+
<div class="project-subtitle" id="project-subtitle">One room. One visible thread. Fresh agents when you need them.</div>
|
|
584
|
+
</div>
|
|
585
|
+
<div class="actions">
|
|
586
|
+
<span class="pill" id="connection-pill">Connected</span>
|
|
587
|
+
<button class="button primary" id="open-add-agent-btn">Add Agent</button>
|
|
588
|
+
<button class="button" id="header-config-btn">Configure</button>
|
|
589
|
+
</div>
|
|
590
|
+
</section>
|
|
591
|
+
|
|
592
|
+
<section class="summary-card">
|
|
593
|
+
<div class="summary-grid">
|
|
594
|
+
<div class="summary-block">
|
|
595
|
+
<div class="summary-label">Summary</div>
|
|
596
|
+
<div class="summary-value" id="summary-text">No summary yet.</div>
|
|
597
|
+
</div>
|
|
598
|
+
<div class="summary-block">
|
|
599
|
+
<div class="summary-label">Latest directive</div>
|
|
600
|
+
<div class="summary-value" id="directive-text">No directive yet.</div>
|
|
601
|
+
</div>
|
|
602
|
+
<div class="summary-block">
|
|
603
|
+
<div class="summary-label">Machine</div>
|
|
604
|
+
<div class="summary-value" id="host-text">No host connected yet.</div>
|
|
605
|
+
</div>
|
|
606
|
+
</div>
|
|
607
|
+
<div class="active-agents" id="active-agents"></div>
|
|
608
|
+
</section>
|
|
609
|
+
|
|
610
|
+
<section class="thread-card">
|
|
611
|
+
<div class="thread-header">
|
|
612
|
+
<h2>Room</h2>
|
|
613
|
+
<p>Messages sent here are forwarded to the visible local agent shells attached to this project.</p>
|
|
614
|
+
</div>
|
|
615
|
+
<div class="thread" id="thread">
|
|
616
|
+
<div class="empty-thread">Start a conversation and then add the agents you need.</div>
|
|
617
|
+
</div>
|
|
618
|
+
</section>
|
|
619
|
+
|
|
620
|
+
<section class="composer-card">
|
|
621
|
+
<div class="composer-top">
|
|
622
|
+
<div id="composer-status">Broadcasting to every active agent in this room.</div>
|
|
623
|
+
</div>
|
|
624
|
+
<textarea id="composer-input" placeholder="Tell the room what needs to happen."></textarea>
|
|
625
|
+
<div class="composer-actions">
|
|
626
|
+
<button class="button primary" id="send-btn">Send</button>
|
|
627
|
+
</div>
|
|
628
|
+
</section>
|
|
629
|
+
</main>
|
|
630
|
+
</div>
|
|
631
|
+
|
|
632
|
+
<div class="modal-shell" id="add-agent-modal">
|
|
633
|
+
<div class="modal">
|
|
634
|
+
<div>
|
|
635
|
+
<h3>Add Agent</h3>
|
|
636
|
+
<p>Spawn a fresh local shell in the current project directory. The agent receives only onboarding.txt and this room’s summary.txt.</p>
|
|
637
|
+
</div>
|
|
638
|
+
<div class="field-grid">
|
|
639
|
+
<div class="field">
|
|
640
|
+
<label for="spawn-runner">Agent</label>
|
|
641
|
+
<select id="spawn-runner">
|
|
642
|
+
<option value="codex">Codex</option>
|
|
643
|
+
<option value="claude">Claude</option>
|
|
644
|
+
</select>
|
|
645
|
+
</div>
|
|
646
|
+
<div class="field">
|
|
647
|
+
<label for="spawn-effort">Effort</label>
|
|
648
|
+
<select id="spawn-effort">
|
|
649
|
+
<option value="low">Low</option>
|
|
650
|
+
<option value="medium">Medium</option>
|
|
651
|
+
<option value="high" selected>High</option>
|
|
652
|
+
</select>
|
|
653
|
+
</div>
|
|
654
|
+
<div class="field">
|
|
655
|
+
<label for="spawn-host">Machine</label>
|
|
656
|
+
<select id="spawn-host"></select>
|
|
657
|
+
</div>
|
|
658
|
+
<div class="field">
|
|
659
|
+
<label for="spawn-name">Name</label>
|
|
660
|
+
<input id="spawn-name" readonly />
|
|
661
|
+
</div>
|
|
662
|
+
</div>
|
|
663
|
+
<div class="code" id="spawn-note"></div>
|
|
664
|
+
<div class="modal-actions">
|
|
665
|
+
<button class="button" data-close-modal="add-agent-modal">Cancel</button>
|
|
666
|
+
<button class="button primary" id="confirm-spawn-btn">Spawn</button>
|
|
667
|
+
</div>
|
|
668
|
+
</div>
|
|
669
|
+
</div>
|
|
670
|
+
|
|
671
|
+
<div class="modal-shell" id="config-modal">
|
|
672
|
+
<div class="modal">
|
|
673
|
+
<div>
|
|
674
|
+
<h3>Configure</h3>
|
|
675
|
+
<p>Choose the control plane and the room to open by default.</p>
|
|
676
|
+
</div>
|
|
677
|
+
<div class="field">
|
|
678
|
+
<label for="config-api-base">API base</label>
|
|
679
|
+
<input id="config-api-base" />
|
|
680
|
+
</div>
|
|
681
|
+
<div class="field">
|
|
682
|
+
<label for="config-project-id">Project id</label>
|
|
683
|
+
<input id="config-project-id" />
|
|
684
|
+
</div>
|
|
685
|
+
<div class="field">
|
|
686
|
+
<label for="config-project-name">Display name</label>
|
|
687
|
+
<input id="config-project-name" placeholder="Optional for a new conversation" />
|
|
688
|
+
</div>
|
|
689
|
+
<div class="code" id="install-command"></div>
|
|
690
|
+
<div class="modal-actions">
|
|
691
|
+
<button class="button" data-close-modal="config-modal">Cancel</button>
|
|
692
|
+
<button class="button primary" id="save-config-btn">Save</button>
|
|
693
|
+
</div>
|
|
694
|
+
</div>
|
|
695
|
+
</div>
|
|
696
|
+
|
|
697
|
+
<script>
|
|
698
|
+
const STORAGE_KEY = "office-core.current-room.config.v1";
|
|
699
|
+
const CONVERSATIONS_KEY = "office-core.current-room.conversations.v1";
|
|
700
|
+
const LIVE_HOST_KEY = "office-core.current-room.live-host.v1";
|
|
701
|
+
|
|
702
|
+
const state = {
|
|
703
|
+
config: loadConfig(),
|
|
704
|
+
conversations: loadConversations(),
|
|
705
|
+
liveHost: loadLiveHost(),
|
|
706
|
+
project: null,
|
|
707
|
+
office: null,
|
|
708
|
+
focusTaskId: null,
|
|
709
|
+
selectedHostId: null,
|
|
710
|
+
selectedSessionId: null,
|
|
711
|
+
selectedAgentId: null,
|
|
712
|
+
pendingHostMoveProjectId: null,
|
|
713
|
+
isRefreshing: false,
|
|
714
|
+
projectEtag: null,
|
|
715
|
+
lastProjectSignature: "",
|
|
716
|
+
lastRoomSignature: "",
|
|
717
|
+
lastConversationSignature: "",
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
const els = {
|
|
721
|
+
conversationList: document.getElementById("conversation-list"),
|
|
722
|
+
projectTitle: document.getElementById("project-title"),
|
|
723
|
+
projectSubtitle: document.getElementById("project-subtitle"),
|
|
724
|
+
connectionPill: document.getElementById("connection-pill"),
|
|
725
|
+
summaryText: document.getElementById("summary-text"),
|
|
726
|
+
directiveText: document.getElementById("directive-text"),
|
|
727
|
+
hostText: document.getElementById("host-text"),
|
|
728
|
+
activeAgents: document.getElementById("active-agents"),
|
|
729
|
+
thread: document.getElementById("thread"),
|
|
730
|
+
composerInput: document.getElementById("composer-input"),
|
|
731
|
+
composerStatus: document.getElementById("composer-status"),
|
|
732
|
+
sendBtn: document.getElementById("send-btn"),
|
|
733
|
+
openAddAgentBtn: document.getElementById("open-add-agent-btn"),
|
|
734
|
+
addAgentModal: document.getElementById("add-agent-modal"),
|
|
735
|
+
spawnRunner: document.getElementById("spawn-runner"),
|
|
736
|
+
spawnEffort: document.getElementById("spawn-effort"),
|
|
737
|
+
spawnHost: document.getElementById("spawn-host"),
|
|
738
|
+
spawnName: document.getElementById("spawn-name"),
|
|
739
|
+
spawnNote: document.getElementById("spawn-note"),
|
|
740
|
+
confirmSpawnBtn: document.getElementById("confirm-spawn-btn"),
|
|
741
|
+
openConfigBtn: document.getElementById("open-config-btn"),
|
|
742
|
+
headerConfigBtn: document.getElementById("header-config-btn"),
|
|
743
|
+
configModal: document.getElementById("config-modal"),
|
|
744
|
+
configApiBase: document.getElementById("config-api-base"),
|
|
745
|
+
configProjectId: document.getElementById("config-project-id"),
|
|
746
|
+
configProjectName: document.getElementById("config-project-name"),
|
|
747
|
+
saveConfigBtn: document.getElementById("save-config-btn"),
|
|
748
|
+
installCommand: document.getElementById("install-command"),
|
|
749
|
+
copyInstallBtn: document.getElementById("copy-install-btn"),
|
|
750
|
+
newConversationBtn: document.getElementById("new-conversation-btn"),
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
init();
|
|
754
|
+
|
|
755
|
+
function init() {
|
|
756
|
+
hydrateConfigFromUrl();
|
|
757
|
+
bindEvents();
|
|
758
|
+
renderStaticConfig();
|
|
759
|
+
renderConversationList();
|
|
760
|
+
void refreshProject();
|
|
761
|
+
setInterval(() => {
|
|
762
|
+
void refreshProject();
|
|
763
|
+
}, 1500);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function bindEvents() {
|
|
767
|
+
els.sendBtn.addEventListener("click", () => void sendRoomMessage());
|
|
768
|
+
els.composerInput.addEventListener("keydown", (event) => {
|
|
769
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
770
|
+
event.preventDefault();
|
|
771
|
+
void sendRoomMessage();
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
els.openAddAgentBtn.addEventListener("click", () => openModal("add-agent-modal"));
|
|
775
|
+
els.openConfigBtn.addEventListener("click", () => openModal("config-modal"));
|
|
776
|
+
els.headerConfigBtn.addEventListener("click", () => openModal("config-modal"));
|
|
777
|
+
els.copyInstallBtn.addEventListener("click", () => void copyInstallCommand());
|
|
778
|
+
els.newConversationBtn.addEventListener("click", () => void createConversation());
|
|
779
|
+
els.confirmSpawnBtn.addEventListener("click", () => void queueSpawn());
|
|
780
|
+
els.saveConfigBtn.addEventListener("click", () => saveConfigFromModal());
|
|
781
|
+
els.spawnRunner.addEventListener("change", () => renderSpawnPreview());
|
|
782
|
+
els.spawnHost.addEventListener("change", () => {
|
|
783
|
+
state.selectedHostId = els.spawnHost.value || null;
|
|
784
|
+
renderSpawnPreview();
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
document.querySelectorAll("[data-close-modal]").forEach((button) => {
|
|
788
|
+
button.addEventListener("click", () => closeModal(button.getAttribute("data-close-modal")));
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
[els.addAgentModal, els.configModal].forEach((modal) => {
|
|
792
|
+
modal.addEventListener("click", (event) => {
|
|
793
|
+
if (event.target === modal) {
|
|
794
|
+
closeModal(modal.id);
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function loadConfig() {
|
|
801
|
+
const defaults = {
|
|
802
|
+
apiBase: window.location.origin,
|
|
803
|
+
projectId: new URL(window.location.href).searchParams.get("projectId") || "prj_local",
|
|
804
|
+
projectName: "",
|
|
805
|
+
};
|
|
806
|
+
try {
|
|
807
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
808
|
+
if (!raw) {
|
|
809
|
+
return defaults;
|
|
810
|
+
}
|
|
811
|
+
return { ...defaults, ...JSON.parse(raw) };
|
|
812
|
+
} catch {
|
|
813
|
+
return defaults;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function loadLiveHost() {
|
|
818
|
+
try {
|
|
819
|
+
const raw = localStorage.getItem(LIVE_HOST_KEY);
|
|
820
|
+
if (!raw) {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
const parsed = JSON.parse(raw);
|
|
824
|
+
if (!parsed?.hostId || !parsed?.projectId) {
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
hostId: String(parsed.hostId),
|
|
829
|
+
projectId: String(parsed.projectId),
|
|
830
|
+
displayName: parsed.displayName ? String(parsed.displayName) : "",
|
|
831
|
+
updatedAt: parsed.updatedAt ? String(parsed.updatedAt) : "",
|
|
832
|
+
};
|
|
833
|
+
} catch {
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function saveLiveHost() {
|
|
839
|
+
if (!state.liveHost?.hostId || !state.liveHost?.projectId) {
|
|
840
|
+
localStorage.removeItem(LIVE_HOST_KEY);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
localStorage.setItem(LIVE_HOST_KEY, JSON.stringify(state.liveHost));
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function loadConversations() {
|
|
847
|
+
try {
|
|
848
|
+
const raw = localStorage.getItem(CONVERSATIONS_KEY);
|
|
849
|
+
const parsed = raw ? JSON.parse(raw) : [];
|
|
850
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
851
|
+
} catch {
|
|
852
|
+
return [];
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function saveConfig() {
|
|
857
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(state.config));
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function saveConversations() {
|
|
861
|
+
localStorage.setItem(CONVERSATIONS_KEY, JSON.stringify(state.conversations.slice(0, 40)));
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function hydrateConfigFromUrl() {
|
|
865
|
+
const url = new URL(window.location.href);
|
|
866
|
+
const projectId = url.searchParams.get("projectId");
|
|
867
|
+
const apiBase = url.searchParams.get("apiBase");
|
|
868
|
+
if (projectId) {
|
|
869
|
+
state.config.projectId = projectId;
|
|
870
|
+
}
|
|
871
|
+
if (apiBase) {
|
|
872
|
+
state.config.apiBase = apiBase.replace(/\/$/, "");
|
|
873
|
+
}
|
|
874
|
+
saveConfig();
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function renderStaticConfig() {
|
|
878
|
+
els.configApiBase.value = state.config.apiBase;
|
|
879
|
+
els.configProjectId.value = state.config.projectId;
|
|
880
|
+
els.configProjectName.value = state.config.projectName || "";
|
|
881
|
+
els.installCommand.textContent = buildInstallCommand();
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
async function refreshProject() {
|
|
885
|
+
if (state.isRefreshing || !state.config.projectId) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
state.isRefreshing = true;
|
|
889
|
+
try {
|
|
890
|
+
const payload = await requestJson(`/api/projects/${encodeURIComponent(state.config.projectId)}`, { etag: state.projectEtag });
|
|
891
|
+
if (payload === null) {
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
state.projectEtag = payload.__etag || null;
|
|
895
|
+
state.project = payload;
|
|
896
|
+
state.office = payload.office || null;
|
|
897
|
+
state.focusTaskId = state.office?.current_project?.focus_task_id || payload.active_task_id || null;
|
|
898
|
+
const onlineHosts = (state.office?.hosts || []).filter((host) => host.online);
|
|
899
|
+
if (onlineHosts.length > 0) {
|
|
900
|
+
const live = onlineHosts[0];
|
|
901
|
+
state.liveHost = {
|
|
902
|
+
hostId: live.host_id,
|
|
903
|
+
projectId: state.config.projectId,
|
|
904
|
+
displayName: live.display_name || live.host_id,
|
|
905
|
+
updatedAt: new Date().toISOString(),
|
|
906
|
+
};
|
|
907
|
+
saveLiveHost();
|
|
908
|
+
state.pendingHostMoveProjectId = null;
|
|
909
|
+
}
|
|
910
|
+
if (!state.selectedHostId || !onlineHosts.some((host) => host.host_id === state.selectedHostId)) {
|
|
911
|
+
state.selectedHostId = onlineHosts[0]?.host_id || state.liveHost?.hostId || state.office?.hosts?.[0]?.host_id || null;
|
|
912
|
+
}
|
|
913
|
+
reconcileSelectedSession();
|
|
914
|
+
rememberConversation(payload);
|
|
915
|
+
renderProject();
|
|
916
|
+
if (!onlineHosts.length && state.liveHost?.hostId && state.liveHost.projectId && state.liveHost.projectId !== state.config.projectId) {
|
|
917
|
+
if (state.pendingHostMoveProjectId !== state.config.projectId) {
|
|
918
|
+
state.pendingHostMoveProjectId = state.config.projectId;
|
|
919
|
+
void ensureHostReadyForCurrentRoom(state.liveHost.hostId).then((moved) => {
|
|
920
|
+
if (!moved && state.pendingHostMoveProjectId === state.config.projectId) {
|
|
921
|
+
state.pendingHostMoveProjectId = null;
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
} catch (error) {
|
|
927
|
+
console.error(error);
|
|
928
|
+
els.connectionPill.textContent = "Disconnected";
|
|
929
|
+
} finally {
|
|
930
|
+
state.isRefreshing = false;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function renderProject() {
|
|
935
|
+
if (!state.project || !state.office) {
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const projectSignature = JSON.stringify({
|
|
940
|
+
projectId: state.project.id,
|
|
941
|
+
headline: state.office.current_project?.headline,
|
|
942
|
+
hosts: state.office.hosts?.map((host) => ({
|
|
943
|
+
host_id: host.host_id,
|
|
944
|
+
status: host.status,
|
|
945
|
+
sessions: (host.running_sessions || []).map((session) => session.session_id),
|
|
946
|
+
})),
|
|
947
|
+
settings: state.office.room?.settings,
|
|
948
|
+
roomSeq: state.project.room?.next_seq,
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
if (projectSignature === state.lastProjectSignature) {
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
state.lastProjectSignature = projectSignature;
|
|
956
|
+
els.connectionPill.textContent = state.office.hosts?.some((host) => host.online) ? "Connected" : "Waiting for host";
|
|
957
|
+
els.projectTitle.textContent = state.project.name || state.project.id || state.config.projectId;
|
|
958
|
+
els.projectSubtitle.textContent = state.office.current_project?.headline || "This room keeps the current summary and fresh local agents.";
|
|
959
|
+
els.summaryText.textContent = state.office.current_project?.headline || "No summary yet.";
|
|
960
|
+
els.directiveText.textContent = state.office.attention?.human_directives?.[0]?.text || "No directive yet.";
|
|
961
|
+
els.hostText.textContent = formatHostSummary();
|
|
962
|
+
renderActiveAgents();
|
|
963
|
+
renderConversationList();
|
|
964
|
+
renderThread();
|
|
965
|
+
renderComposerStatus();
|
|
966
|
+
renderSpawnPreview();
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function formatHostSummary() {
|
|
970
|
+
const host = (state.office?.hosts || []).find((entry) => entry.host_id === state.selectedHostId) || state.office?.hosts?.[0];
|
|
971
|
+
if (!host) {
|
|
972
|
+
if (state.liveHost?.displayName && state.liveHost?.projectId && state.liveHost.projectId !== state.config.projectId) {
|
|
973
|
+
return `${state.liveHost.displayName} is active in another room and will move here when needed.`;
|
|
974
|
+
}
|
|
975
|
+
return "No host connected yet.";
|
|
976
|
+
}
|
|
977
|
+
const running = host.running_sessions?.length || 0;
|
|
978
|
+
const queued = host.queued_commands?.length || 0;
|
|
979
|
+
return `${host.display_name} · ${host.online ? "online" : "offline"} · ${running} active · ${queued} queued`;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function renderConversationList() {
|
|
983
|
+
const signature = JSON.stringify({
|
|
984
|
+
current: state.config.projectId,
|
|
985
|
+
items: state.conversations.map((entry) => [entry.projectId, entry.name, entry.lastOpenedAt]),
|
|
986
|
+
});
|
|
987
|
+
if (signature === state.lastConversationSignature) {
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
state.lastConversationSignature = signature;
|
|
991
|
+
els.conversationList.innerHTML = "";
|
|
992
|
+
if (!state.conversations.length) {
|
|
993
|
+
const empty = document.createElement("div");
|
|
994
|
+
empty.className = "conversation-meta";
|
|
995
|
+
empty.textContent = "No saved conversations yet.";
|
|
996
|
+
els.conversationList.appendChild(empty);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
state.conversations.forEach((entry) => {
|
|
1001
|
+
const button = document.createElement("button");
|
|
1002
|
+
button.className = `conversation${entry.projectId === state.config.projectId ? " active" : ""}`;
|
|
1003
|
+
button.innerHTML = `
|
|
1004
|
+
<div class="conversation-name">${escapeHtml(entry.name || entry.projectId)}</div>
|
|
1005
|
+
<div class="conversation-meta">${escapeHtml(entry.projectId)} · ${formatRelative(entry.lastOpenedAt)}</div>
|
|
1006
|
+
`;
|
|
1007
|
+
button.addEventListener("click", () => {
|
|
1008
|
+
void activateConversation(entry.projectId, entry.name || entry.projectId);
|
|
1009
|
+
});
|
|
1010
|
+
els.conversationList.appendChild(button);
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function renderActiveAgents() {
|
|
1015
|
+
els.activeAgents.innerHTML = "";
|
|
1016
|
+
const sessions = getRunningSessions();
|
|
1017
|
+
if (!sessions.length) {
|
|
1018
|
+
const note = document.createElement("div");
|
|
1019
|
+
note.className = "conversation-meta";
|
|
1020
|
+
note.textContent = "No active agents in this room yet.";
|
|
1021
|
+
els.activeAgents.appendChild(note);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
sessions.forEach((session) => {
|
|
1026
|
+
const chip = document.createElement("div");
|
|
1027
|
+
chip.className = `agent-chip${state.selectedSessionId === session.session_id ? " active" : ""}`;
|
|
1028
|
+
chip.innerHTML = `
|
|
1029
|
+
<div>
|
|
1030
|
+
<div class="agent-name">${escapeHtml(session.agent_id)}</div>
|
|
1031
|
+
<div class="agent-meta">${escapeHtml(labelForRunner(session.runner))} · ${escapeHtml(session.mode || "attach")}</div>
|
|
1032
|
+
</div>
|
|
1033
|
+
`;
|
|
1034
|
+
chip.addEventListener("click", () => {
|
|
1035
|
+
state.selectedSessionId = session.session_id;
|
|
1036
|
+
state.selectedAgentId = session.agent_id;
|
|
1037
|
+
renderComposerStatus();
|
|
1038
|
+
renderActiveAgents();
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
const remove = document.createElement("button");
|
|
1042
|
+
remove.className = "remove-agent";
|
|
1043
|
+
remove.type = "button";
|
|
1044
|
+
remove.textContent = "×";
|
|
1045
|
+
remove.addEventListener("click", (event) => {
|
|
1046
|
+
event.stopPropagation();
|
|
1047
|
+
void removeSession(session.host_id, session.session_id);
|
|
1048
|
+
});
|
|
1049
|
+
chip.appendChild(remove);
|
|
1050
|
+
els.activeAgents.appendChild(chip);
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function renderThread() {
|
|
1055
|
+
const messages = (state.office?.room?.recent_messages || []).slice(-120);
|
|
1056
|
+
const nearBottom = isNearBottom(els.thread);
|
|
1057
|
+
const signature = JSON.stringify(messages.map((message) => [message.seq, message.text, message.author_label, message.session_id, message.target_session_ids, message.target_agent_ids]));
|
|
1058
|
+
if (signature === state.lastRoomSignature) {
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
state.lastRoomSignature = signature;
|
|
1062
|
+
els.thread.innerHTML = "";
|
|
1063
|
+
if (!messages.length) {
|
|
1064
|
+
const empty = document.createElement("div");
|
|
1065
|
+
empty.className = "empty-thread";
|
|
1066
|
+
empty.textContent = "This room is quiet. Start the conversation and then add the agents you need.";
|
|
1067
|
+
els.thread.appendChild(empty);
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
messages.forEach((message) => {
|
|
1072
|
+
const wrapper = document.createElement("div");
|
|
1073
|
+
wrapper.className = `message ${normalizeAuthorType(message.author_type)}`;
|
|
1074
|
+
const meta = document.createElement("div");
|
|
1075
|
+
meta.className = "message-meta";
|
|
1076
|
+
meta.textContent = `${message.author_label || message.author_id || "system"} · ${formatTime(message.created_at)}`;
|
|
1077
|
+
const bubble = document.createElement("div");
|
|
1078
|
+
bubble.className = "bubble";
|
|
1079
|
+
bubble.textContent = message.text || "";
|
|
1080
|
+
wrapper.appendChild(meta);
|
|
1081
|
+
wrapper.appendChild(bubble);
|
|
1082
|
+
|
|
1083
|
+
const targetText = describeMessageTarget(message);
|
|
1084
|
+
if (targetText) {
|
|
1085
|
+
const target = document.createElement("div");
|
|
1086
|
+
target.className = "message-meta";
|
|
1087
|
+
target.textContent = targetText;
|
|
1088
|
+
wrapper.appendChild(target);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
els.thread.appendChild(wrapper);
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
if (nearBottom) {
|
|
1095
|
+
requestAnimationFrame(() => {
|
|
1096
|
+
els.thread.scrollTop = els.thread.scrollHeight;
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function renderComposerStatus() {
|
|
1102
|
+
if (state.selectedSessionId && state.selectedAgentId) {
|
|
1103
|
+
els.composerStatus.textContent = `Sending to ${state.selectedAgentId}.`;
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
if (state.selectedHostId && !isBroadcastMode()) {
|
|
1107
|
+
const host = (state.office?.hosts || []).find((entry) => entry.host_id === state.selectedHostId);
|
|
1108
|
+
els.composerStatus.textContent = `Sending to ${host?.display_name || state.selectedHostId}.`;
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
els.composerStatus.textContent = "Broadcasting to every active agent in this room.";
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function renderSpawnPreview() {
|
|
1115
|
+
const hosts = state.office?.hosts || [];
|
|
1116
|
+
const fallbackHosts = hosts.length
|
|
1117
|
+
? hosts
|
|
1118
|
+
: state.liveHost?.hostId
|
|
1119
|
+
? [
|
|
1120
|
+
{
|
|
1121
|
+
host_id: state.liveHost.hostId,
|
|
1122
|
+
display_name: state.liveHost.displayName || state.liveHost.hostId,
|
|
1123
|
+
online: false,
|
|
1124
|
+
default_workdir: "",
|
|
1125
|
+
},
|
|
1126
|
+
]
|
|
1127
|
+
: [];
|
|
1128
|
+
if (els.spawnHost.options.length !== fallbackHosts.length) {
|
|
1129
|
+
els.spawnHost.innerHTML = fallbackHosts
|
|
1130
|
+
.map((host) => `<option value="${escapeAttr(host.host_id)}">${escapeHtml(host.display_name)}${host.online ? "" : " (move here)"}</option>`)
|
|
1131
|
+
.join("");
|
|
1132
|
+
}
|
|
1133
|
+
els.spawnHost.value = state.selectedHostId || fallbackHosts[0]?.host_id || "";
|
|
1134
|
+
const runner = els.spawnRunner.value || "codex";
|
|
1135
|
+
els.spawnName.value = getNextAgentName(runner);
|
|
1136
|
+
const host = fallbackHosts.find((entry) => entry.host_id === els.spawnHost.value);
|
|
1137
|
+
els.spawnNote.textContent = host
|
|
1138
|
+
? host.online
|
|
1139
|
+
? `${host.display_name} will open a visible ${labelForRunner(runner)} shell in ${host.default_workdir || "its configured project directory"}.`
|
|
1140
|
+
: `${host.display_name} will be moved into this room, then a visible ${labelForRunner(runner)} shell will open here.`
|
|
1141
|
+
: "Select a connected machine to spawn this room agent.";
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function isBroadcastMode() {
|
|
1145
|
+
return !state.selectedSessionId && !state.selectedAgentId;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
async function sendRoomMessage() {
|
|
1149
|
+
const text = els.composerInput.value.trim();
|
|
1150
|
+
if (!text || !state.config.projectId) {
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const target = buildTargetPayload();
|
|
1155
|
+
if (isBroadcastMode() && !state.office?.room?.settings?.all_agents_listening) {
|
|
1156
|
+
if (!target.target_host_ids && !target.target_session_ids && !target.target_agent_ids) {
|
|
1157
|
+
alert("Turn on All agents listening or select a target agent/machine first.");
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
await requestJson(`/api/projects/${encodeURIComponent(state.config.projectId)}/room/messages`, {
|
|
1163
|
+
method: "POST",
|
|
1164
|
+
body: {
|
|
1165
|
+
author_type: "user",
|
|
1166
|
+
author_id: "human",
|
|
1167
|
+
author_label: state.office?.room?.settings?.conductor_name || "You",
|
|
1168
|
+
text,
|
|
1169
|
+
task_id: state.focusTaskId,
|
|
1170
|
+
...target,
|
|
1171
|
+
},
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
els.composerInput.value = "";
|
|
1175
|
+
state.lastProjectSignature = "";
|
|
1176
|
+
state.lastRoomSignature = "";
|
|
1177
|
+
void refreshProject();
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
async function queueSpawn() {
|
|
1181
|
+
const hostId = els.spawnHost.value;
|
|
1182
|
+
const runner = els.spawnRunner.value;
|
|
1183
|
+
const effort = els.spawnEffort.value;
|
|
1184
|
+
let host = (state.office?.hosts || []).find((entry) => entry.host_id === hostId);
|
|
1185
|
+
if (!host) {
|
|
1186
|
+
alert("Choose a connected machine first.");
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
if (!host.online) {
|
|
1190
|
+
const moved = await ensureHostReadyForCurrentRoom(hostId);
|
|
1191
|
+
if (!moved) {
|
|
1192
|
+
alert("That machine is offline.");
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
await refreshProject();
|
|
1196
|
+
host = (state.office?.hosts || []).find((entry) => entry.host_id === hostId);
|
|
1197
|
+
if (!host?.online) {
|
|
1198
|
+
alert("That machine is offline.");
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const agentId = getNextAgentName(runner);
|
|
1204
|
+
await requestJson(`/api/projects/${encodeURIComponent(state.config.projectId)}/local-host/commands/queue`, {
|
|
1205
|
+
method: "POST",
|
|
1206
|
+
body: {
|
|
1207
|
+
host_id: hostId,
|
|
1208
|
+
runner,
|
|
1209
|
+
agent_id: agentId,
|
|
1210
|
+
mode: "attach",
|
|
1211
|
+
effort,
|
|
1212
|
+
workdir: host.default_workdir || "",
|
|
1213
|
+
task_id: state.focusTaskId,
|
|
1214
|
+
prompt: `Join the current room as ${agentId}. Read onboarding.txt and summary.txt, then wait for room prompts.`,
|
|
1215
|
+
},
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
closeModal("add-agent-modal");
|
|
1219
|
+
state.selectedHostId = hostId;
|
|
1220
|
+
state.selectedSessionId = null;
|
|
1221
|
+
state.selectedAgentId = agentId;
|
|
1222
|
+
state.lastProjectSignature = "";
|
|
1223
|
+
state.lastRoomSignature = "";
|
|
1224
|
+
void refreshProject();
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
async function removeSession(hostId, sessionId) {
|
|
1228
|
+
await requestJson(`/api/projects/${encodeURIComponent(state.config.projectId)}/local-host/sessions/remove`, {
|
|
1229
|
+
method: "POST",
|
|
1230
|
+
body: {
|
|
1231
|
+
host_id: hostId,
|
|
1232
|
+
session_id: sessionId,
|
|
1233
|
+
},
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
if (state.selectedSessionId === sessionId) {
|
|
1237
|
+
clearTarget();
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
state.lastProjectSignature = "";
|
|
1241
|
+
state.lastRoomSignature = "";
|
|
1242
|
+
void refreshProject();
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
async function createConversation() {
|
|
1246
|
+
const rawName = window.prompt("Name the new conversation.");
|
|
1247
|
+
if (!rawName) {
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
const name = rawName.trim();
|
|
1251
|
+
if (!name) {
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const projectId = slugify(name);
|
|
1256
|
+
await requestJson("/api/projects", {
|
|
1257
|
+
method: "POST",
|
|
1258
|
+
body: {
|
|
1259
|
+
project_id: projectId,
|
|
1260
|
+
name,
|
|
1261
|
+
},
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
rememberConversation({ id: projectId, name });
|
|
1265
|
+
await activateConversation(projectId, name);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
async function activateConversation(projectId, projectName) {
|
|
1269
|
+
const nextProjectId = String(projectId || "").trim();
|
|
1270
|
+
if (!nextProjectId) {
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
const previousProjectId = state.config.projectId;
|
|
1275
|
+
const currentOnlineHost = (state.office?.hosts || []).find((host) => host.online);
|
|
1276
|
+
const hostId = state.selectedHostId || currentOnlineHost?.host_id || state.liveHost?.hostId || null;
|
|
1277
|
+
const sourceProjectId = currentOnlineHost?.host_id === hostId ? previousProjectId : state.liveHost?.projectId || previousProjectId;
|
|
1278
|
+
if (hostId && sourceProjectId && sourceProjectId !== nextProjectId) {
|
|
1279
|
+
try {
|
|
1280
|
+
await requestJson(`/api/projects/${encodeURIComponent(sourceProjectId)}/local-host/switch`, {
|
|
1281
|
+
method: "POST",
|
|
1282
|
+
body: {
|
|
1283
|
+
host_id: hostId,
|
|
1284
|
+
target_project_id: nextProjectId,
|
|
1285
|
+
target_project_name: projectName || nextProjectId,
|
|
1286
|
+
},
|
|
1287
|
+
});
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
console.error(error);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
state.config.projectId = nextProjectId;
|
|
1294
|
+
state.config.projectName = projectName || nextProjectId;
|
|
1295
|
+
state.selectedSessionId = null;
|
|
1296
|
+
state.selectedAgentId = null;
|
|
1297
|
+
saveConfig();
|
|
1298
|
+
syncUrl();
|
|
1299
|
+
state.lastProjectSignature = "";
|
|
1300
|
+
state.lastRoomSignature = "";
|
|
1301
|
+
await refreshProject();
|
|
1302
|
+
if (hostId && sourceProjectId && sourceProjectId !== nextProjectId) {
|
|
1303
|
+
await waitForHostOnline(nextProjectId, hostId);
|
|
1304
|
+
state.lastProjectSignature = "";
|
|
1305
|
+
state.lastRoomSignature = "";
|
|
1306
|
+
await refreshProject();
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
async function ensureHostReadyForCurrentRoom(hostId) {
|
|
1311
|
+
const knownHost = state.liveHost?.hostId === hostId ? state.liveHost : null;
|
|
1312
|
+
const sourceProjectId = knownHost?.projectId;
|
|
1313
|
+
if (!sourceProjectId || sourceProjectId === state.config.projectId) {
|
|
1314
|
+
return false;
|
|
1315
|
+
}
|
|
1316
|
+
try {
|
|
1317
|
+
await requestJson(`/api/projects/${encodeURIComponent(sourceProjectId)}/local-host/switch`, {
|
|
1318
|
+
method: "POST",
|
|
1319
|
+
body: {
|
|
1320
|
+
host_id: hostId,
|
|
1321
|
+
target_project_id: state.config.projectId,
|
|
1322
|
+
target_project_name: state.config.projectName || state.project?.name || state.config.projectId,
|
|
1323
|
+
},
|
|
1324
|
+
});
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
console.error(error);
|
|
1327
|
+
return false;
|
|
1328
|
+
}
|
|
1329
|
+
const moved = await waitForHostOnline(state.config.projectId, hostId);
|
|
1330
|
+
if (moved) {
|
|
1331
|
+
state.pendingHostMoveProjectId = null;
|
|
1332
|
+
}
|
|
1333
|
+
return moved;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
async function waitForHostOnline(projectId, hostId) {
|
|
1337
|
+
const startedAt = Date.now();
|
|
1338
|
+
while (Date.now() - startedAt < 15000) {
|
|
1339
|
+
try {
|
|
1340
|
+
const payload = await requestJson(`/api/projects/${encodeURIComponent(projectId)}`);
|
|
1341
|
+
const host = (payload.office?.hosts || []).find((entry) => entry.host_id === hostId);
|
|
1342
|
+
if (host?.online) {
|
|
1343
|
+
return true;
|
|
1344
|
+
}
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
console.error(error);
|
|
1347
|
+
}
|
|
1348
|
+
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
1349
|
+
}
|
|
1350
|
+
return false;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function rememberConversation(project) {
|
|
1354
|
+
const projectId = String(project?.id || project?.project_id || state.config.projectId || "").trim();
|
|
1355
|
+
if (!projectId) {
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
const name = String(project?.name || projectId);
|
|
1359
|
+
const existingIndex = state.conversations.findIndex((entry) => entry.projectId === projectId);
|
|
1360
|
+
const entry = {
|
|
1361
|
+
projectId,
|
|
1362
|
+
name,
|
|
1363
|
+
lastOpenedAt: new Date().toISOString(),
|
|
1364
|
+
};
|
|
1365
|
+
if (existingIndex >= 0) {
|
|
1366
|
+
state.conversations.splice(existingIndex, 1);
|
|
1367
|
+
}
|
|
1368
|
+
state.conversations.unshift(entry);
|
|
1369
|
+
saveConversations();
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function openModal(id) {
|
|
1373
|
+
renderSpawnPreview();
|
|
1374
|
+
document.getElementById(id)?.classList.add("open");
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function closeModal(id) {
|
|
1378
|
+
document.getElementById(id)?.classList.remove("open");
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
function saveConfigFromModal() {
|
|
1382
|
+
state.config.apiBase = els.configApiBase.value.trim().replace(/\/$/, "") || window.location.origin;
|
|
1383
|
+
state.config.projectId = els.configProjectId.value.trim() || state.config.projectId;
|
|
1384
|
+
state.config.projectName = els.configProjectName.value.trim();
|
|
1385
|
+
saveConfig();
|
|
1386
|
+
syncUrl();
|
|
1387
|
+
renderStaticConfig();
|
|
1388
|
+
closeModal("config-modal");
|
|
1389
|
+
state.lastProjectSignature = "";
|
|
1390
|
+
state.lastConversationSignature = "";
|
|
1391
|
+
void refreshProject();
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function syncUrl() {
|
|
1395
|
+
const url = new URL(window.location.href);
|
|
1396
|
+
url.searchParams.set("projectId", state.config.projectId);
|
|
1397
|
+
if (state.config.apiBase && state.config.apiBase !== window.location.origin) {
|
|
1398
|
+
url.searchParams.set("apiBase", state.config.apiBase);
|
|
1399
|
+
} else {
|
|
1400
|
+
url.searchParams.delete("apiBase");
|
|
1401
|
+
}
|
|
1402
|
+
window.history.replaceState({}, "", url);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
async function copyInstallCommand() {
|
|
1406
|
+
const command = buildInstallCommand();
|
|
1407
|
+
try {
|
|
1408
|
+
await navigator.clipboard.writeText(command);
|
|
1409
|
+
} catch {
|
|
1410
|
+
window.prompt("Copy install command", command);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
function buildInstallCommand() {
|
|
1415
|
+
return `npm install -g "${state.config.apiBase}/downloads/office-core.tgz"; office-core install --baseUrl "${state.config.apiBase}" --project "${state.config.projectId}" --workdir "C:\\path\\to\\project"`;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function buildTargetPayload() {
|
|
1419
|
+
if (isBroadcastMode()) {
|
|
1420
|
+
if (state.office?.room?.settings?.all_agents_listening) {
|
|
1421
|
+
return { target_host_ids: null, target_session_ids: null, target_agent_ids: null };
|
|
1422
|
+
}
|
|
1423
|
+
if (state.selectedHostId) {
|
|
1424
|
+
return { target_host_ids: [state.selectedHostId], target_session_ids: null, target_agent_ids: null };
|
|
1425
|
+
}
|
|
1426
|
+
return { target_host_ids: null, target_session_ids: null, target_agent_ids: null };
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
if (state.selectedSessionId && state.selectedAgentId) {
|
|
1430
|
+
return {
|
|
1431
|
+
target_session_ids: [state.selectedSessionId],
|
|
1432
|
+
target_agent_ids: [state.selectedAgentId],
|
|
1433
|
+
target_host_ids: null,
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
if (state.selectedHostId) {
|
|
1437
|
+
return { target_host_ids: [state.selectedHostId], target_session_ids: null, target_agent_ids: null };
|
|
1438
|
+
}
|
|
1439
|
+
return { target_host_ids: null, target_session_ids: null, target_agent_ids: null };
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function getRunningSessions() {
|
|
1443
|
+
const sessions = [];
|
|
1444
|
+
for (const host of state.office?.hosts || []) {
|
|
1445
|
+
for (const session of host.running_sessions || []) {
|
|
1446
|
+
sessions.push({ ...session, host_id: host.host_id, host_name: host.display_name });
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
return sessions;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function getNextAgentName(runner) {
|
|
1453
|
+
const base = runner === "claude" ? "Claude" : "Codex";
|
|
1454
|
+
const used = new Set();
|
|
1455
|
+
for (const host of state.office?.hosts || []) {
|
|
1456
|
+
for (const session of host.running_sessions || []) {
|
|
1457
|
+
used.add(session.agent_id);
|
|
1458
|
+
}
|
|
1459
|
+
for (const command of host.queued_commands || []) {
|
|
1460
|
+
if (command.agent_id) {
|
|
1461
|
+
used.add(command.agent_id);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
let index = 1;
|
|
1467
|
+
while (used.has(`${base} #${index}`)) {
|
|
1468
|
+
index += 1;
|
|
1469
|
+
}
|
|
1470
|
+
return `${base} #${index}`;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
function reconcileSelectedSession() {
|
|
1474
|
+
const sessions = getRunningSessions();
|
|
1475
|
+
if (state.selectedSessionId && sessions.some((session) => session.session_id === state.selectedSessionId)) {
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
state.selectedSessionId = null;
|
|
1479
|
+
state.selectedAgentId = null;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function clearTarget() {
|
|
1483
|
+
state.selectedSessionId = null;
|
|
1484
|
+
state.selectedAgentId = null;
|
|
1485
|
+
renderActiveAgents();
|
|
1486
|
+
renderComposerStatus();
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
function describeMessageTarget(message) {
|
|
1490
|
+
if (message.target_session_ids?.length || message.target_agent_ids?.length) {
|
|
1491
|
+
return "Targeted message";
|
|
1492
|
+
}
|
|
1493
|
+
if (message.target_host_ids?.length) {
|
|
1494
|
+
return "Sent to one machine";
|
|
1495
|
+
}
|
|
1496
|
+
if (message.reply_to_seq != null) {
|
|
1497
|
+
return `Reply to #${message.reply_to_seq}`;
|
|
1498
|
+
}
|
|
1499
|
+
return "";
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function normalizeAuthorType(authorType) {
|
|
1503
|
+
if (authorType === "user" || authorType === "agent" || authorType === "system") {
|
|
1504
|
+
return authorType;
|
|
1505
|
+
}
|
|
1506
|
+
return "system";
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function formatRelative(value) {
|
|
1510
|
+
if (!value) return "just now";
|
|
1511
|
+
const deltaMs = Date.now() - new Date(value).getTime();
|
|
1512
|
+
const deltaMinutes = Math.round(deltaMs / 60000);
|
|
1513
|
+
if (deltaMinutes < 1) return "just now";
|
|
1514
|
+
if (deltaMinutes < 60) return `${deltaMinutes}m ago`;
|
|
1515
|
+
const deltaHours = Math.round(deltaMinutes / 60);
|
|
1516
|
+
if (deltaHours < 24) return `${deltaHours}h ago`;
|
|
1517
|
+
const deltaDays = Math.round(deltaHours / 24);
|
|
1518
|
+
return `${deltaDays}d ago`;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function formatTime(value) {
|
|
1522
|
+
try {
|
|
1523
|
+
return new Date(value).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
|
|
1524
|
+
} catch {
|
|
1525
|
+
return "";
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
function labelForRunner(runner) {
|
|
1530
|
+
return runner === "claude" ? "Claude" : "Codex";
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
async function requestJson(pathname, options = {}) {
|
|
1534
|
+
const url = pathname.startsWith("http") ? pathname : `${state.config.apiBase}${pathname}`;
|
|
1535
|
+
const headers = { "content-type": "application/json" };
|
|
1536
|
+
if (options.etag) {
|
|
1537
|
+
headers["if-none-match"] = options.etag;
|
|
1538
|
+
}
|
|
1539
|
+
const response = await fetch(url, {
|
|
1540
|
+
method: options.method || "GET",
|
|
1541
|
+
headers,
|
|
1542
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
1543
|
+
});
|
|
1544
|
+
if (response.status === 304) {
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
if (!response.ok) {
|
|
1548
|
+
throw new Error((await response.text()) || `${response.status}`);
|
|
1549
|
+
}
|
|
1550
|
+
const data = await response.json();
|
|
1551
|
+
data.__etag = response.headers.get("etag") || null;
|
|
1552
|
+
return data;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
function slugify(value) {
|
|
1556
|
+
return `prj_${value
|
|
1557
|
+
.toLowerCase()
|
|
1558
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
1559
|
+
.replace(/^_+|_+$/g, "")
|
|
1560
|
+
.slice(0, 40) || "room"}`;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
function escapeHtml(value) {
|
|
1564
|
+
return String(value)
|
|
1565
|
+
.replaceAll("&", "&")
|
|
1566
|
+
.replaceAll("<", "<")
|
|
1567
|
+
.replaceAll(">", ">")
|
|
1568
|
+
.replaceAll('"', """)
|
|
1569
|
+
.replaceAll("'", "'");
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
function escapeAttr(value) {
|
|
1573
|
+
return escapeHtml(value);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function isNearBottom(element) {
|
|
1577
|
+
return element.scrollHeight - element.scrollTop - element.clientHeight < 120;
|
|
1578
|
+
}
|
|
1579
|
+
</script>
|
|
1580
|
+
</body>
|
|
1581
|
+
</html>
|