@controlflow-ai/daemon 0.1.2 → 0.1.4
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/README.md +54 -6
- package/bin/daemon.js +6 -1
- package/package.json +3 -1
- package/src/agent-avatar.ts +30 -0
- package/src/agent-key.ts +28 -0
- package/src/agent-permissions.ts +359 -0
- package/src/agent-runtime.ts +795 -28
- package/src/agent-workspace.ts +183 -0
- package/src/app.ts +1970 -79
- package/src/args.ts +54 -7
- package/src/cli.ts +873 -14
- package/src/client.ts +472 -10
- package/src/coco.ts +9 -40
- package/src/codex.ts +33 -5
- package/src/config.ts +28 -4
- package/src/console.ts +230 -20
- package/src/daemon-client.ts +116 -3
- package/src/daemon.ts +937 -99
- package/src/db.ts +3128 -122
- package/src/delivery-ws.ts +269 -0
- package/src/format.ts +4 -1
- package/src/lark/cli.ts +3 -3
- package/src/lark/event-router.ts +60 -4
- package/src/lark/inbound-events.ts +156 -3
- package/src/lark/server-integration.ts +659 -111
- package/src/lark/ws-daemon.ts +136 -10
- package/src/local-api.ts +545 -15
- package/src/local-auth.ts +33 -1
- package/src/message-attachments.ts +71 -0
- package/src/messaging-cli.ts +741 -0
- package/src/messaging-status.ts +669 -0
- package/src/migrations/024_agents_model.ts +10 -0
- package/src/migrations/025_room_archive.ts +44 -0
- package/src/migrations/026_project_archive.ts +44 -0
- package/src/migrations/027_agent_permission_profiles.ts +16 -0
- package/src/migrations/028_lark_websocket_restart_state.ts +16 -0
- package/src/migrations/029_held_message_drafts.ts +32 -0
- package/src/migrations/030_agent_room_read_state.ts +25 -0
- package/src/migrations/031_room_tasks.ts +29 -0
- package/src/migrations/032_room_reminders.ts +29 -0
- package/src/migrations/033_room_saved_messages.ts +25 -0
- package/src/migrations/034_agent_activity_events.ts +27 -0
- package/src/migrations/035_agent_avatars.ts +17 -0
- package/src/migrations/036_project_agent_defaults.ts +21 -0
- package/src/migrations/037_message_attachments.ts +36 -0
- package/src/migrations/038_agent_activity_room_scope.ts +64 -0
- package/src/migrations/039_message_attachments_path.ts +34 -0
- package/src/migrations/040_message_attachments_file_schema.ts +80 -0
- package/src/migrations/041_room_system_events.ts +30 -0
- package/src/migrations/042_message_attachment_file_kind.ts +52 -0
- package/src/migrations/043_room_mode_skill_registry.ts +92 -0
- package/src/migrations/044_workflow_runtime.ts +69 -0
- package/src/migrations/045_skill_repository_ownership.ts +64 -0
- package/src/migrations.ts +69 -1
- package/src/neeko.ts +40 -4
- package/src/runtime-env.ts +179 -0
- package/src/runtime-registry.ts +83 -13
- package/src/server.ts +244 -4
- package/src/token-file.ts +13 -6
- package/src/types.ts +362 -0
- package/src/workflow-runtime.ts +275 -0
- package/src/web.ts +0 -904
package/src/web.ts
DELETED
|
@@ -1,904 +0,0 @@
|
|
|
1
|
-
export function dashboardHtml(): string {
|
|
2
|
-
return `<!doctype html>
|
|
3
|
-
<html lang="zh-CN">
|
|
4
|
-
<head>
|
|
5
|
-
<meta charset="utf-8">
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
-
<title>Pal Rooms</title>
|
|
8
|
-
<style>
|
|
9
|
-
:root {
|
|
10
|
-
color-scheme: light;
|
|
11
|
-
--ink: #181714;
|
|
12
|
-
--muted: #6b675f;
|
|
13
|
-
--faint: #938d82;
|
|
14
|
-
--line: #d9d0c2;
|
|
15
|
-
--paper: #f5efe4;
|
|
16
|
-
--panel: #fffaf1;
|
|
17
|
-
--panel-2: #faf3e7;
|
|
18
|
-
--active: #1f6f5b;
|
|
19
|
-
--active-soft: #dceee6;
|
|
20
|
-
--blue: #235bd8;
|
|
21
|
-
--blue-soft: #dfe8ff;
|
|
22
|
-
--danger: #a33a2b;
|
|
23
|
-
--shadow: 0 18px 44px rgba(41, 35, 26, 0.11);
|
|
24
|
-
--mono: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
|
|
25
|
-
}
|
|
26
|
-
* { box-sizing: border-box; }
|
|
27
|
-
body {
|
|
28
|
-
margin: 0;
|
|
29
|
-
min-height: 100vh;
|
|
30
|
-
color: var(--ink);
|
|
31
|
-
background:
|
|
32
|
-
linear-gradient(rgba(24, 23, 20, 0.035) 1px, transparent 1px),
|
|
33
|
-
linear-gradient(90deg, rgba(24, 23, 20, 0.035) 1px, transparent 1px),
|
|
34
|
-
var(--paper);
|
|
35
|
-
background-size: 28px 28px;
|
|
36
|
-
font-family: "Avenir Next", "Gill Sans", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
37
|
-
}
|
|
38
|
-
button, input, textarea, select { font: inherit; min-width: 0; }
|
|
39
|
-
button {
|
|
40
|
-
border: 1px solid var(--ink);
|
|
41
|
-
background: var(--ink);
|
|
42
|
-
color: #fffaf1;
|
|
43
|
-
min-height: 38px;
|
|
44
|
-
padding: 0 12px;
|
|
45
|
-
cursor: pointer;
|
|
46
|
-
font-weight: 800;
|
|
47
|
-
}
|
|
48
|
-
button.secondary { background: var(--panel); color: var(--ink); border-color: var(--line); }
|
|
49
|
-
button.icon { width: 38px; padding: 0; }
|
|
50
|
-
button:disabled { opacity: .55; cursor: not-allowed; }
|
|
51
|
-
input, textarea, select {
|
|
52
|
-
width: 100%;
|
|
53
|
-
border: 1px solid var(--line);
|
|
54
|
-
background: #fffdf8;
|
|
55
|
-
color: var(--ink);
|
|
56
|
-
padding: 10px 11px;
|
|
57
|
-
outline: none;
|
|
58
|
-
}
|
|
59
|
-
textarea { resize: vertical; min-height: 88px; line-height: 1.45; }
|
|
60
|
-
input:focus, textarea:focus, select:focus { border-color: var(--ink); box-shadow: 0 0 0 3px rgba(24, 23, 20, .08); }
|
|
61
|
-
.app {
|
|
62
|
-
display: grid;
|
|
63
|
-
grid-template-columns: 300px minmax(360px, 1fr) 330px;
|
|
64
|
-
gap: 14px;
|
|
65
|
-
width: min(1540px, calc(100vw - 28px));
|
|
66
|
-
height: calc(100vh - 28px);
|
|
67
|
-
margin: 14px auto;
|
|
68
|
-
overflow: hidden;
|
|
69
|
-
}
|
|
70
|
-
.panel {
|
|
71
|
-
min-width: 0;
|
|
72
|
-
border: 1px solid var(--line);
|
|
73
|
-
background: rgba(255, 250, 241, .92);
|
|
74
|
-
box-shadow: var(--shadow);
|
|
75
|
-
}
|
|
76
|
-
.sidebar, .inspector { display: grid; grid-template-rows: auto auto 1fr; overflow: hidden; }
|
|
77
|
-
.brand, .panel-head, .composer, .toolbox { border-bottom: 1px solid var(--line); padding: 14px; }
|
|
78
|
-
.brand h1 { margin: 0; font-size: 30px; line-height: .92; letter-spacing: 0; }
|
|
79
|
-
.brand p, .hint, .meta, .empty, .error { color: var(--muted); font-size: 12px; line-height: 1.45; }
|
|
80
|
-
.create-room, .create-agent { display: grid; gap: 8px; padding: 14px; border-bottom: 1px solid var(--line); background: var(--panel-2); }
|
|
81
|
-
.form-row { display: grid; grid-template-columns: 1fr auto; gap: 8px; }
|
|
82
|
-
.rooms, .agents, .members, .messages { overflow: auto; padding: 10px; }
|
|
83
|
-
.room, .agent, .member {
|
|
84
|
-
display: grid;
|
|
85
|
-
gap: 7px;
|
|
86
|
-
width: 100%;
|
|
87
|
-
margin-bottom: 8px;
|
|
88
|
-
padding: 11px;
|
|
89
|
-
border: 1px solid var(--line);
|
|
90
|
-
background: #fffdf8;
|
|
91
|
-
text-align: left;
|
|
92
|
-
}
|
|
93
|
-
.room { cursor: pointer; }
|
|
94
|
-
.room.active { border-color: var(--ink); background: #fff7d9; }
|
|
95
|
-
.room-title, .agent-title, .member-title { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; font-weight: 900; }
|
|
96
|
-
.room-title span, .agent-title span, .member-title span {
|
|
97
|
-
min-width: 0;
|
|
98
|
-
overflow: hidden;
|
|
99
|
-
text-overflow: ellipsis;
|
|
100
|
-
white-space: nowrap;
|
|
101
|
-
}
|
|
102
|
-
.pills { display: flex; flex-wrap: wrap; gap: 5px; }
|
|
103
|
-
.pill {
|
|
104
|
-
display: inline-flex;
|
|
105
|
-
align-items: center;
|
|
106
|
-
min-height: 22px;
|
|
107
|
-
padding: 2px 7px;
|
|
108
|
-
border: 1px solid var(--line);
|
|
109
|
-
background: var(--panel);
|
|
110
|
-
color: var(--muted);
|
|
111
|
-
font-size: 11px;
|
|
112
|
-
font-weight: 800;
|
|
113
|
-
max-width: 100%;
|
|
114
|
-
overflow: hidden;
|
|
115
|
-
text-overflow: ellipsis;
|
|
116
|
-
}
|
|
117
|
-
.pill.good { color: var(--active); border-color: #a7d7c4; background: var(--active-soft); }
|
|
118
|
-
.pill.blue { color: var(--blue); border-color: #b7c8f6; background: var(--blue-soft); }
|
|
119
|
-
.chat { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; overflow: hidden; min-height: 0; }
|
|
120
|
-
.panel-head { display: flex; align-items: center; justify-content: space-between; gap: 14px; min-height: 74px; min-width: 0; background: var(--panel); }
|
|
121
|
-
.panel-head > div { min-width: 0; }
|
|
122
|
-
.panel-head h2 { margin: 0; font-size: 22px; letter-spacing: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
123
|
-
.panel-head .meta { font-family: var(--mono); overflow-wrap: anywhere; word-break: break-word; }
|
|
124
|
-
.messages { display: flex; flex-direction: column; gap: 10px; background: rgba(250, 243, 231, .7); }
|
|
125
|
-
.message {
|
|
126
|
-
max-width: min(760px, 92%);
|
|
127
|
-
border: 1px solid var(--line);
|
|
128
|
-
background: #fffdf8;
|
|
129
|
-
padding: 10px 12px;
|
|
130
|
-
box-shadow: 5px 5px 0 rgba(24, 23, 20, .045);
|
|
131
|
-
}
|
|
132
|
-
.message.mine { align-self: flex-end; border-color: #b7c8f6; background: #f4f7ff; }
|
|
133
|
-
.message.agent { border-color: #a7d7c4; background: #f3fbf7; }
|
|
134
|
-
.message-head { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 7px; }
|
|
135
|
-
.message-body { white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.52; }
|
|
136
|
-
.composer { display: grid; gap: 8px; background: var(--panel); }
|
|
137
|
-
.composer.readonly { display: block; }
|
|
138
|
-
.composer-controls { display: grid; grid-template-columns: 150px auto; gap: 8px; align-items: end; }
|
|
139
|
-
.split { display: grid; grid-template-columns: minmax(0, 1fr) 120px; gap: 8px; }
|
|
140
|
-
.toolbox { display: grid; gap: 8px; background: var(--panel-2); }
|
|
141
|
-
.section-title { margin: 0; color: var(--muted); font-size: 12px; font-weight: 900; letter-spacing: .12em; text-transform: uppercase; }
|
|
142
|
-
.field { display: grid; gap: 5px; }
|
|
143
|
-
.field span { color: var(--muted); font-size: 11px; font-weight: 900; text-transform: uppercase; letter-spacing: .08em; }
|
|
144
|
-
.settings-trigger { white-space: nowrap; }
|
|
145
|
-
.settings-backdrop {
|
|
146
|
-
position: fixed;
|
|
147
|
-
inset: 0;
|
|
148
|
-
z-index: 40;
|
|
149
|
-
display: none;
|
|
150
|
-
padding: 18px;
|
|
151
|
-
background: rgba(24, 23, 20, .28);
|
|
152
|
-
}
|
|
153
|
-
.settings-backdrop.open { display: grid; place-items: center; }
|
|
154
|
-
.settings-dialog {
|
|
155
|
-
display: grid;
|
|
156
|
-
grid-template-rows: auto 1fr;
|
|
157
|
-
width: min(1120px, 100%);
|
|
158
|
-
max-height: min(860px, calc(100vh - 36px));
|
|
159
|
-
border: 1px solid var(--ink);
|
|
160
|
-
background: var(--panel);
|
|
161
|
-
box-shadow: var(--shadow);
|
|
162
|
-
overflow: hidden;
|
|
163
|
-
}
|
|
164
|
-
.settings-head {
|
|
165
|
-
display: flex;
|
|
166
|
-
justify-content: space-between;
|
|
167
|
-
gap: 14px;
|
|
168
|
-
padding: 16px;
|
|
169
|
-
border-bottom: 1px solid var(--line);
|
|
170
|
-
background: #fffdf8;
|
|
171
|
-
}
|
|
172
|
-
.settings-head h2 { margin: 0; font-size: 24px; letter-spacing: 0; }
|
|
173
|
-
.settings-body {
|
|
174
|
-
display: grid;
|
|
175
|
-
grid-template-columns: 210px minmax(0, 1fr);
|
|
176
|
-
min-height: 0;
|
|
177
|
-
overflow: hidden;
|
|
178
|
-
}
|
|
179
|
-
.settings-nav {
|
|
180
|
-
display: grid;
|
|
181
|
-
align-content: start;
|
|
182
|
-
gap: 8px;
|
|
183
|
-
padding: 14px;
|
|
184
|
-
border-right: 1px solid var(--line);
|
|
185
|
-
background: var(--panel-2);
|
|
186
|
-
overflow: auto;
|
|
187
|
-
}
|
|
188
|
-
.settings-tab {
|
|
189
|
-
width: 100%;
|
|
190
|
-
background: transparent;
|
|
191
|
-
color: var(--ink);
|
|
192
|
-
border-color: var(--line);
|
|
193
|
-
text-align: left;
|
|
194
|
-
}
|
|
195
|
-
.settings-tab.active { background: var(--ink); color: #fffaf1; border-color: var(--ink); }
|
|
196
|
-
.settings-content { overflow: auto; padding: 16px; }
|
|
197
|
-
.settings-pane { display: none; gap: 12px; }
|
|
198
|
-
.settings-pane.active { display: grid; }
|
|
199
|
-
.settings-section-head {
|
|
200
|
-
display: flex;
|
|
201
|
-
align-items: start;
|
|
202
|
-
justify-content: space-between;
|
|
203
|
-
gap: 12px;
|
|
204
|
-
padding-bottom: 10px;
|
|
205
|
-
border-bottom: 1px solid var(--line);
|
|
206
|
-
}
|
|
207
|
-
.settings-section-head h3 { margin: 0; font-size: 18px; letter-spacing: 0; }
|
|
208
|
-
.settings-section-head .meta { max-width: 620px; }
|
|
209
|
-
.setup-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }
|
|
210
|
-
.setup-panel { display: grid; gap: 8px; padding: 12px; border: 1px solid var(--line); background: #fffdf8; }
|
|
211
|
-
.setup-panel.collapsed { display: none; }
|
|
212
|
-
.setup-panel .section-title { color: var(--ink); }
|
|
213
|
-
.setup-actions { display: grid; grid-template-columns: 1fr auto; gap: 8px; }
|
|
214
|
-
.summary-panel { display: grid; gap: 8px; padding: 12px; border: 1px solid var(--line); background: var(--active-soft); }
|
|
215
|
-
.settings-list { display: grid; gap: 8px; }
|
|
216
|
-
.settings-row {
|
|
217
|
-
display: grid;
|
|
218
|
-
grid-template-columns: minmax(0, 1fr) auto;
|
|
219
|
-
gap: 10px;
|
|
220
|
-
align-items: start;
|
|
221
|
-
padding: 11px;
|
|
222
|
-
border: 1px solid var(--line);
|
|
223
|
-
background: #fffdf8;
|
|
224
|
-
}
|
|
225
|
-
.settings-row strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
226
|
-
.settings-row .meta { overflow-wrap: anywhere; word-break: break-word; }
|
|
227
|
-
.command-wrap { display: grid; gap: 7px; }
|
|
228
|
-
.command-wrap textarea { font-family: var(--mono); font-size: 11px; min-height: 104px; }
|
|
229
|
-
.secret-note { color: var(--faint); font-size: 11px; line-height: 1.4; }
|
|
230
|
-
.empty, .error, .readonly-note { border: 1px dashed var(--line); padding: 18px; text-align: center; background: rgba(255, 253, 248, .65); }
|
|
231
|
-
.error { display: none; color: var(--danger); border-color: rgba(163, 58, 43, .35); background: #fff1ee; }
|
|
232
|
-
.readonly-note { color: var(--muted); line-height: 1.5; }
|
|
233
|
-
.mention-wrap { position: relative; }
|
|
234
|
-
.mention-menu {
|
|
235
|
-
position: absolute;
|
|
236
|
-
left: 0;
|
|
237
|
-
right: 0;
|
|
238
|
-
bottom: calc(100% + 6px);
|
|
239
|
-
z-index: 20;
|
|
240
|
-
display: none;
|
|
241
|
-
max-height: 220px;
|
|
242
|
-
overflow: auto;
|
|
243
|
-
border: 1px solid var(--ink);
|
|
244
|
-
background: #fffdf8;
|
|
245
|
-
box-shadow: var(--shadow);
|
|
246
|
-
}
|
|
247
|
-
.mention-option {
|
|
248
|
-
display: grid;
|
|
249
|
-
grid-template-columns: minmax(0, 1fr) auto;
|
|
250
|
-
gap: 8px;
|
|
251
|
-
width: 100%;
|
|
252
|
-
min-height: 36px;
|
|
253
|
-
padding: 8px 10px;
|
|
254
|
-
border: 0;
|
|
255
|
-
border-bottom: 1px solid var(--line);
|
|
256
|
-
background: #fffdf8;
|
|
257
|
-
color: var(--ink);
|
|
258
|
-
text-align: left;
|
|
259
|
-
}
|
|
260
|
-
.mention-option:hover, .mention-option.active { background: #fff7d9; }
|
|
261
|
-
.mention-option span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
262
|
-
.toast {
|
|
263
|
-
position: fixed;
|
|
264
|
-
left: 50%;
|
|
265
|
-
bottom: 18px;
|
|
266
|
-
transform: translateX(-50%);
|
|
267
|
-
display: none;
|
|
268
|
-
max-width: min(560px, calc(100vw - 28px));
|
|
269
|
-
border: 1px solid var(--ink);
|
|
270
|
-
background: var(--ink);
|
|
271
|
-
color: #fffaf1;
|
|
272
|
-
padding: 11px 13px;
|
|
273
|
-
font-size: 13px;
|
|
274
|
-
box-shadow: var(--shadow);
|
|
275
|
-
}
|
|
276
|
-
@media (max-width: 1120px) {
|
|
277
|
-
.app { grid-template-columns: 280px minmax(0, 1fr); }
|
|
278
|
-
.inspector { grid-column: 1 / -1; grid-template-rows: auto auto; }
|
|
279
|
-
.inspector .agents { max-height: 320px; }
|
|
280
|
-
.settings-body, .setup-grid, .settings-row { grid-template-columns: 1fr; }
|
|
281
|
-
.settings-nav { grid-template-columns: repeat(2, minmax(0, 1fr)); border-right: 0; border-bottom: 1px solid var(--line); }
|
|
282
|
-
}
|
|
283
|
-
@media (max-width: 760px) {
|
|
284
|
-
.app { width: 100%; height: 100vh; margin: 0; grid-template-columns: 1fr; overflow: auto; }
|
|
285
|
-
.sidebar, .inspector { max-height: none; }
|
|
286
|
-
.chat { height: 100vh; min-height: 0; }
|
|
287
|
-
.composer-controls, .split, .form-row, .setup-actions { grid-template-columns: 1fr; }
|
|
288
|
-
.message { max-width: 100%; }
|
|
289
|
-
}
|
|
290
|
-
</style>
|
|
291
|
-
</head>
|
|
292
|
-
<body>
|
|
293
|
-
<main class="app">
|
|
294
|
-
<aside class="panel sidebar">
|
|
295
|
-
<header class="brand">
|
|
296
|
-
<h1>Pal<br>Rooms</h1>
|
|
297
|
-
<p>Web chat for local rooms, agent invitations, and lightweight agent management.</p>
|
|
298
|
-
</header>
|
|
299
|
-
<form id="room-form" class="create-room">
|
|
300
|
-
<p class="section-title">Create Room</p>
|
|
301
|
-
<div class="form-row">
|
|
302
|
-
<input id="room-name" name="name" placeholder="general" autocomplete="off" required>
|
|
303
|
-
<button class="icon" title="Create room" aria-label="Create room">+</button>
|
|
304
|
-
</div>
|
|
305
|
-
<select id="room-kind" name="kind" aria-label="Room kind">
|
|
306
|
-
<option value="group">Group room</option>
|
|
307
|
-
<option value="dm">DM room</option>
|
|
308
|
-
</select>
|
|
309
|
-
</form>
|
|
310
|
-
<div id="rooms" class="rooms"></div>
|
|
311
|
-
</aside>
|
|
312
|
-
|
|
313
|
-
<section class="panel chat">
|
|
314
|
-
<header class="panel-head">
|
|
315
|
-
<div>
|
|
316
|
-
<h2 id="room-title">Select a room</h2>
|
|
317
|
-
<div id="room-meta" class="meta">No active room</div>
|
|
318
|
-
</div>
|
|
319
|
-
<button id="refresh" class="secondary icon" title="Refresh" aria-label="Refresh">↻</button>
|
|
320
|
-
</header>
|
|
321
|
-
<div id="messages" class="messages"></div>
|
|
322
|
-
<form id="message-form" class="composer">
|
|
323
|
-
<div class="mention-wrap">
|
|
324
|
-
<div id="mention-menu" class="mention-menu"></div>
|
|
325
|
-
<textarea id="message-content" placeholder="Type a message. Use @ to mention room members." required></textarea>
|
|
326
|
-
</div>
|
|
327
|
-
<div class="composer-controls">
|
|
328
|
-
<input id="message-sender" value="owner" autocomplete="off" aria-label="Sender">
|
|
329
|
-
<button id="send-message">Send</button>
|
|
330
|
-
</div>
|
|
331
|
-
</form>
|
|
332
|
-
</section>
|
|
333
|
-
|
|
334
|
-
<aside class="panel inspector">
|
|
335
|
-
<header class="panel-head">
|
|
336
|
-
<div>
|
|
337
|
-
<h2>Agents</h2>
|
|
338
|
-
<div class="meta">Use runtime=codex for executable demo agents.</div>
|
|
339
|
-
</div>
|
|
340
|
-
<button id="open-settings" class="secondary settings-trigger" type="button">Settings</button>
|
|
341
|
-
</header>
|
|
342
|
-
<div class="toolbox">
|
|
343
|
-
<form id="invite-form" class="split">
|
|
344
|
-
<select id="invite-agent" aria-label="Agent to invite"></select>
|
|
345
|
-
<select id="invite-mode" aria-label="Delivery mode">
|
|
346
|
-
<option value="mentions">Mentions</option>
|
|
347
|
-
<option value="all">All</option>
|
|
348
|
-
<option value="periodic">Periodic</option>
|
|
349
|
-
<option value="muted">Muted</option>
|
|
350
|
-
<option value="off">Off</option>
|
|
351
|
-
</select>
|
|
352
|
-
<button>Invite</button>
|
|
353
|
-
</form>
|
|
354
|
-
<div id="members" class="members"></div>
|
|
355
|
-
</div>
|
|
356
|
-
<div id="agents" class="agents"></div>
|
|
357
|
-
</aside>
|
|
358
|
-
</main>
|
|
359
|
-
<section id="settings-backdrop" class="settings-backdrop" aria-hidden="true">
|
|
360
|
-
<div class="settings-dialog" role="dialog" aria-modal="true" aria-labelledby="settings-title">
|
|
361
|
-
<header class="settings-head">
|
|
362
|
-
<div>
|
|
363
|
-
<h2 id="settings-title">Settings</h2>
|
|
364
|
-
<div class="meta">Configure access, agents, computers, and Feishu/Lark from one place.</div>
|
|
365
|
-
</div>
|
|
366
|
-
<button id="close-settings" class="secondary icon" type="button" aria-label="Close settings">×</button>
|
|
367
|
-
</header>
|
|
368
|
-
<div class="settings-body">
|
|
369
|
-
<nav class="settings-nav" aria-label="Settings sections">
|
|
370
|
-
<button class="settings-tab active" type="button" data-settings-tab="access">Access</button>
|
|
371
|
-
<button class="settings-tab" type="button" data-settings-tab="agents">Agents</button>
|
|
372
|
-
<button class="settings-tab" type="button" data-settings-tab="computers">Computers</button>
|
|
373
|
-
<button class="settings-tab" type="button" data-settings-tab="lark">Lark</button>
|
|
374
|
-
</nav>
|
|
375
|
-
<div class="settings-content">
|
|
376
|
-
<section id="settings-access" class="settings-pane active">
|
|
377
|
-
<div class="summary-panel">
|
|
378
|
-
<p class="section-title">Server Access</p>
|
|
379
|
-
<div id="server-access" class="meta">Listening locally. Tailscale address is detected on load.</div>
|
|
380
|
-
</div>
|
|
381
|
-
<div class="settings-section-head">
|
|
382
|
-
<div>
|
|
383
|
-
<h3>Lark Users</h3>
|
|
384
|
-
<div class="meta">Only listed Lark user IDs may trigger inbound bot handling.</div>
|
|
385
|
-
</div>
|
|
386
|
-
<button class="secondary" type="button" data-add-panel="lark-user-form">Add User</button>
|
|
387
|
-
</div>
|
|
388
|
-
<div class="setup-grid">
|
|
389
|
-
<div id="settings-lark-user-list" class="settings-list"></div>
|
|
390
|
-
<form id="lark-user-form" class="setup-panel collapsed">
|
|
391
|
-
<p class="section-title">Authorized Lark User</p>
|
|
392
|
-
<label class="field"><span>User ID</span><input id="lark-user-id" placeholder="on_xxx union id" autocomplete="off" required></label>
|
|
393
|
-
<label class="field"><span>Display name</span><input id="lark-user-name" placeholder="Optional" autocomplete="off"></label>
|
|
394
|
-
<div class="setup-actions">
|
|
395
|
-
<button>Save user</button>
|
|
396
|
-
<button class="secondary" type="button" data-cancel-panel="lark-user-form">Cancel</button>
|
|
397
|
-
</div>
|
|
398
|
-
</form>
|
|
399
|
-
</div>
|
|
400
|
-
</section>
|
|
401
|
-
<section id="settings-agents" class="settings-pane">
|
|
402
|
-
<div class="settings-section-head">
|
|
403
|
-
<div>
|
|
404
|
-
<h3>Agents</h3>
|
|
405
|
-
<div class="meta">Manage logical agents and assign them to an available computer.</div>
|
|
406
|
-
</div>
|
|
407
|
-
<button class="secondary" type="button" data-add-panel="agent-form">Add Agent</button>
|
|
408
|
-
</div>
|
|
409
|
-
<div class="setup-grid">
|
|
410
|
-
<div id="settings-agent-list" class="settings-list"></div>
|
|
411
|
-
<form id="agent-form" class="setup-panel collapsed">
|
|
412
|
-
<p class="section-title">Agent Onboard</p>
|
|
413
|
-
<label class="field"><span>Key</span><input id="agent-key" placeholder="codex" autocomplete="off" required></label>
|
|
414
|
-
<label class="field"><span>Name</span><input id="agent-name" placeholder="Codex" autocomplete="off" required></label>
|
|
415
|
-
<label class="field"><span>Runtime</span><select id="agent-runtime"><option value="codex">codex</option><option value="neeko">neeko</option><option value="coco">coco</option><option value="coco-stream-json">coco-stream-json</option></select></label>
|
|
416
|
-
<label class="field"><span>Computer</span><select id="agent-computer"><option value="">No assignment</option></select></label>
|
|
417
|
-
<label class="field"><span>Description</span><input id="agent-desc" placeholder="Optional" autocomplete="off"></label>
|
|
418
|
-
<div class="setup-actions">
|
|
419
|
-
<button>Onboard agent</button>
|
|
420
|
-
<button class="secondary" type="button" data-cancel-panel="agent-form">Cancel</button>
|
|
421
|
-
</div>
|
|
422
|
-
</form>
|
|
423
|
-
</div>
|
|
424
|
-
</section>
|
|
425
|
-
<section id="settings-computers" class="settings-pane">
|
|
426
|
-
<div class="settings-section-head">
|
|
427
|
-
<div>
|
|
428
|
-
<h3>Computers</h3>
|
|
429
|
-
<div class="meta">Provision daemon credentials and see connected machines.</div>
|
|
430
|
-
</div>
|
|
431
|
-
<button class="secondary" type="button" data-add-panel="computer-form">Add Computer</button>
|
|
432
|
-
</div>
|
|
433
|
-
<div class="setup-grid">
|
|
434
|
-
<div id="settings-computer-list" class="settings-list"></div>
|
|
435
|
-
<form id="computer-form" class="setup-panel collapsed">
|
|
436
|
-
<p class="section-title">Computer Onboard</p>
|
|
437
|
-
<label class="field"><span>Name</span><input id="computer-name" placeholder="Local computer" autocomplete="off"></label>
|
|
438
|
-
<label class="field"><span>Server URL</span><input id="computer-server" autocomplete="off"></label>
|
|
439
|
-
<label class="field"><span>Daemon package</span><input id="computer-package" value="@controlflow-ai/daemon@latest" autocomplete="off"></label>
|
|
440
|
-
<div class="setup-actions">
|
|
441
|
-
<button>Generate command</button>
|
|
442
|
-
<button id="copy-command" class="secondary" type="button">Copy</button>
|
|
443
|
-
</div>
|
|
444
|
-
<button class="secondary" type="button" data-cancel-panel="computer-form">Cancel</button>
|
|
445
|
-
<div class="command-wrap">
|
|
446
|
-
<textarea id="computer-command" readonly rows="4" placeholder="Daemon command"></textarea>
|
|
447
|
-
</div>
|
|
448
|
-
</form>
|
|
449
|
-
</div>
|
|
450
|
-
</section>
|
|
451
|
-
<section id="settings-lark" class="settings-pane">
|
|
452
|
-
<div class="settings-section-head">
|
|
453
|
-
<div>
|
|
454
|
-
<h3>Lark</h3>
|
|
455
|
-
<div class="meta">Bind Feishu/Lark bot credentials to a Pal agent. Secrets stay in the local runtime profile.</div>
|
|
456
|
-
</div>
|
|
457
|
-
<button class="secondary" type="button" data-add-panel="lark-form">Add Lark Bot</button>
|
|
458
|
-
</div>
|
|
459
|
-
<div class="setup-grid">
|
|
460
|
-
<div id="settings-lark-list" class="settings-list"></div>
|
|
461
|
-
<form id="lark-form" class="setup-panel collapsed">
|
|
462
|
-
<p class="section-title">Lark Setup</p>
|
|
463
|
-
<label class="field"><span>Bind agent</span><select id="lark-agent"></select></label>
|
|
464
|
-
<label class="field"><span>Label</span><input id="lark-label" placeholder="Team bot" autocomplete="off"></label>
|
|
465
|
-
<label class="field"><span>App ID</span><input id="lark-app-id" placeholder="cli_xxx" autocomplete="off" required></label>
|
|
466
|
-
<label class="field"><span>App Secret</span><input id="lark-app-secret" type="password" autocomplete="off" required></label>
|
|
467
|
-
<div class="secret-note">Secret is sent only to this local Pal server, validated with Feishu, and stored in the local Lark config file.</div>
|
|
468
|
-
<div class="setup-actions">
|
|
469
|
-
<button>Save Lark bot</button>
|
|
470
|
-
<button class="secondary" type="button" data-cancel-panel="lark-form">Cancel</button>
|
|
471
|
-
</div>
|
|
472
|
-
</form>
|
|
473
|
-
</div>
|
|
474
|
-
</section>
|
|
475
|
-
</div>
|
|
476
|
-
</div>
|
|
477
|
-
</div>
|
|
478
|
-
</section>
|
|
479
|
-
<div id="error" class="error" role="alert"></div>
|
|
480
|
-
<div id="toast" class="toast" role="status"></div>
|
|
481
|
-
<script>
|
|
482
|
-
const state = { rooms: [], agents: [], computers: [], lark: null, larkUsers: [], serverAccess: null, members: [], mentionables: [], messages: [], selectedRoomId: null, mentionIndex: 0 };
|
|
483
|
-
const root = (id) => document.getElementById(id);
|
|
484
|
-
const escapeHtml = (value) => String(value ?? '').replace(/[&<>'"]/g, (char) => ({ '&':'&', '<':'<', '>':'>', "'":''', '"':'"' }[char]));
|
|
485
|
-
async function api(path, options) {
|
|
486
|
-
const response = await fetch(path, options);
|
|
487
|
-
const payload = await response.json().catch(() => ({}));
|
|
488
|
-
if (!response.ok || payload.ok === false) throw new Error(payload.message || payload.code || 'Request failed');
|
|
489
|
-
return payload.data || {};
|
|
490
|
-
}
|
|
491
|
-
function showToast(text) {
|
|
492
|
-
const el = root('toast');
|
|
493
|
-
el.textContent = text;
|
|
494
|
-
el.style.display = 'block';
|
|
495
|
-
clearTimeout(showToast.timer);
|
|
496
|
-
showToast.timer = setTimeout(() => { el.style.display = 'none'; }, 2600);
|
|
497
|
-
}
|
|
498
|
-
function showError(error) {
|
|
499
|
-
const el = root('error');
|
|
500
|
-
el.textContent = error instanceof Error ? error.message : String(error);
|
|
501
|
-
el.style.display = 'block';
|
|
502
|
-
clearTimeout(showError.timer);
|
|
503
|
-
showError.timer = setTimeout(() => { el.style.display = 'none'; }, 5000);
|
|
504
|
-
}
|
|
505
|
-
function pill(value, cls) {
|
|
506
|
-
return '<span class="pill ' + (cls || '') + '">' + escapeHtml(value) + '</span>';
|
|
507
|
-
}
|
|
508
|
-
function empty(text) {
|
|
509
|
-
return '<div class="empty">' + escapeHtml(text) + '</div>';
|
|
510
|
-
}
|
|
511
|
-
function activeRoom() {
|
|
512
|
-
return state.rooms.find((room) => room.id === state.selectedRoomId) || state.rooms[0] || null;
|
|
513
|
-
}
|
|
514
|
-
function roomLabel(room) {
|
|
515
|
-
return room ? (room.display_name || room.name) : '';
|
|
516
|
-
}
|
|
517
|
-
function canWriteRoom(room) {
|
|
518
|
-
return room && room.provider === 'web';
|
|
519
|
-
}
|
|
520
|
-
function mentionLabel(member) {
|
|
521
|
-
return member.display_name || member.participant_id;
|
|
522
|
-
}
|
|
523
|
-
function mentionToken(member) {
|
|
524
|
-
return member.kind === 'agent' ? member.participant_id : mentionLabel(member).replace(/\\s+/g, '');
|
|
525
|
-
}
|
|
526
|
-
function renderRooms() {
|
|
527
|
-
root('rooms').innerHTML = state.rooms.length ? state.rooms.map((room) => (
|
|
528
|
-
'<article class="room ' + (room.id === state.selectedRoomId ? 'active' : '') + '" onclick="selectRoom(\\'' + room.id + '\\')">' +
|
|
529
|
-
'<div class="room-title"><span title="' + escapeHtml(room.name) + '">#' + escapeHtml(roomLabel(room)) + '</span>' + pill(room.kind, room.kind === 'dm' ? 'blue' : 'good') + '</div>' +
|
|
530
|
-
'<div class="pills">' + pill(room.provider) + (canWriteRoom(room) ? pill('writable', 'good') : pill('read-only')) + pill((room.message_count || 0) + ' messages') + '</div>' +
|
|
531
|
-
'<div class="meta">' + escapeHtml(room.id) + '</div></article>'
|
|
532
|
-
)).join('') : empty('No rooms yet. Create one above.');
|
|
533
|
-
}
|
|
534
|
-
function renderMessages() {
|
|
535
|
-
const room = activeRoom();
|
|
536
|
-
root('room-title').textContent = room ? '#' + roomLabel(room) : 'Select a room';
|
|
537
|
-
root('room-title').title = room?.name || '';
|
|
538
|
-
root('room-meta').textContent = room ? room.id + ' · ' + room.kind + ' · ' + room.provider + (canWriteRoom(room) ? '' : ' · read-only from Web') : 'No active room';
|
|
539
|
-
root('message-form').style.display = room ? (canWriteRoom(room) ? 'grid' : 'block') : 'none';
|
|
540
|
-
root('message-form').className = canWriteRoom(room) ? 'composer' : 'composer readonly';
|
|
541
|
-
if (room && !canWriteRoom(room)) {
|
|
542
|
-
root('message-form').innerHTML = '<div class="readonly-note">This room is owned by ' + escapeHtml(room.provider) + '. Web human sending is disabled because Pal cannot send as your Feishu user account. Agent replies still go through the provider channel.</div>';
|
|
543
|
-
} else if (room && !root('message-content')) {
|
|
544
|
-
root('message-form').innerHTML = '<div class="mention-wrap"><div id="mention-menu" class="mention-menu"></div><textarea id="message-content" placeholder="Type a message. Use @ to mention room members." required></textarea></div><div class="composer-controls"><input id="message-sender" value="owner" autocomplete="off" aria-label="Sender"><button id="send-message">Send</button></div>';
|
|
545
|
-
bindComposer();
|
|
546
|
-
}
|
|
547
|
-
root('messages').innerHTML = state.messages.length ? state.messages.map((message) => {
|
|
548
|
-
const senderInput = root('message-sender');
|
|
549
|
-
const kind = senderInput && message.sender === senderInput.value.trim() ? ' mine' : (state.agents.some((agent) => agent.agent_key === message.sender) ? ' agent' : '');
|
|
550
|
-
const mentions = (message.mentions || []).map((mention) => pill('@' + mention, 'blue')).join('');
|
|
551
|
-
return '<article class="message' + kind + '"><div class="message-head">' + pill('@' + message.sender, kind.includes('agent') ? 'good' : '') +
|
|
552
|
-
(message.recipient ? pill('to @' + message.recipient) : '') + pill('#' + message.id) + mentions + '</div>' +
|
|
553
|
-
'<div class="message-body">' + escapeHtml(message.content) + '</div><div class="meta">' + escapeHtml(message.created_at) + '</div></article>';
|
|
554
|
-
}).join('') : empty(room ? 'No messages in this room yet.' : 'Create or select a room.');
|
|
555
|
-
root('messages').scrollTop = root('messages').scrollHeight;
|
|
556
|
-
}
|
|
557
|
-
function renderAgents() {
|
|
558
|
-
root('invite-agent').innerHTML = state.agents.length ? state.agents.map((agent) => '<option value="' + escapeHtml(agent.agent_key) + '">' + escapeHtml(agent.display_name) + ' · ' + escapeHtml(agent.agent_key) + '</option>').join('') : '<option value="">No agents</option>';
|
|
559
|
-
root('lark-agent').innerHTML = state.agents.length ? state.agents.map((agent) => '<option value="' + escapeHtml(agent.agent_key) + '">' + escapeHtml(agent.display_name) + ' · ' + escapeHtml(agent.agent_key) + '</option>').join('') : '<option value="">Onboard an agent first</option>';
|
|
560
|
-
root('agents').innerHTML = state.agents.length ? state.agents.map((agent) => (
|
|
561
|
-
'<article class="agent"><div class="agent-title"><span>' + escapeHtml(agent.display_name) + '</span>' + pill(agent.runtime || 'no runtime', agent.runtime === 'codex' ? 'good' : '') + '</div>' +
|
|
562
|
-
'<div class="pills">' + pill(agent.agent_key) + pill(agent.id) + '</div>' +
|
|
563
|
-
'<div class="meta">' + escapeHtml(agent.description || 'No description') + '</div></article>'
|
|
564
|
-
)).join('') : empty('No agents yet. Create codex to start.');
|
|
565
|
-
root('settings-agent-list').innerHTML = state.agents.length
|
|
566
|
-
? state.agents.map((agent) => '<article class="settings-row"><div><strong>' + escapeHtml(agent.display_name) + '</strong><div class="meta">' + escapeHtml(agent.description || 'No description') + '</div><div class="pills">' + pill(agent.agent_key, 'blue') + pill(agent.runtime || 'no runtime', agent.runtime === 'codex' ? 'good' : '') + '</div></div></article>').join('')
|
|
567
|
-
: empty('No agents yet.');
|
|
568
|
-
}
|
|
569
|
-
function renderComputers() {
|
|
570
|
-
root('agent-computer').innerHTML = '<option value="">No assignment</option>' + state.computers.map((computer) => '<option value="' + escapeHtml(computer.id) + '">' + escapeHtml(computer.name) + ' · ' + escapeHtml(computer.id) + '</option>').join('');
|
|
571
|
-
root('settings-computer-list').innerHTML = state.computers.length
|
|
572
|
-
? state.computers.map((computer) => '<article class="settings-row"><div><strong>' + escapeHtml(computer.name) + '</strong><div class="meta">' + escapeHtml(computer.id) + '</div><div class="pills">' + pill(computer.status, computer.status === 'online' ? 'good' : '') + (computer.last_seen_at ? pill('last seen ' + computer.last_seen_at) : '') + '</div></div></article>').join('')
|
|
573
|
-
: empty('No computers yet.');
|
|
574
|
-
}
|
|
575
|
-
function renderLarkConfig() {
|
|
576
|
-
const el = root('settings-lark-list');
|
|
577
|
-
if (!el) return;
|
|
578
|
-
const bots = state.lark?.bots || [];
|
|
579
|
-
el.innerHTML = bots.length
|
|
580
|
-
? bots.map((bot) => '<article class="settings-row"><div><strong>' + escapeHtml(bot.label || bot.appId) + '</strong><div class="meta">' + escapeHtml(bot.appId) + '</div><div class="pills">' + pill('@' + (bot.agent || '-'), 'blue') + pill(bot.botOpenId ? 'open_id resolved' : 'open_id missing', bot.botOpenId ? 'good' : '') + pill(bot.hasSecret ? 'secret stored' : 'secret missing') + '</div></div></article>').join('') + '<div class="meta">Config: ' + escapeHtml(state.lark.path || '') + '</div>'
|
|
581
|
-
: empty('No Lark bots configured yet.');
|
|
582
|
-
}
|
|
583
|
-
function renderLarkUsers() {
|
|
584
|
-
const el = root('settings-lark-user-list');
|
|
585
|
-
if (!el) return;
|
|
586
|
-
el.innerHTML = state.larkUsers.length
|
|
587
|
-
? state.larkUsers.map((user) => '<article class="settings-row"><div><strong>' + escapeHtml(user.display_name || user.user_id) + '</strong><div class="meta">' + escapeHtml(user.user_id) + '</div><div class="pills">' + pill('authorized', 'good') + '</div></div><button class="secondary" type="button" data-delete-lark-user="' + escapeHtml(user.user_id) + '">Delete</button></article>').join('')
|
|
588
|
-
: empty('No authorized Lark users. Inbound bot messages will be ignored.');
|
|
589
|
-
[...el.querySelectorAll('[data-delete-lark-user]')].forEach((button) => {
|
|
590
|
-
button.addEventListener('click', async () => {
|
|
591
|
-
const userId = button.dataset.deleteLarkUser;
|
|
592
|
-
if (!userId) return;
|
|
593
|
-
const deleted = await api('/api/lark/authorized-users/' + encodeURIComponent(userId), { method: 'DELETE' }).catch((error) => { showError(error); return null; });
|
|
594
|
-
if (!deleted) return;
|
|
595
|
-
await loadLarkUsers();
|
|
596
|
-
showToast('Lark user removed');
|
|
597
|
-
});
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
function renderServerAccess() {
|
|
601
|
-
const access = state.serverAccess;
|
|
602
|
-
const el = root('server-access');
|
|
603
|
-
if (!el) return;
|
|
604
|
-
if (!access) {
|
|
605
|
-
el.textContent = 'Listening locally. Tailscale address is detected on load.';
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
el.textContent = access.tailscaleUrl
|
|
609
|
-
? 'Listening on local ' + access.localUrl + ' and Tailscale ' + access.tailscaleUrl
|
|
610
|
-
: 'Listening on local ' + access.localUrl + '. No Tailscale interface detected.';
|
|
611
|
-
}
|
|
612
|
-
function renderMembers() {
|
|
613
|
-
root('members').innerHTML = state.members.length ? state.members.map((member) => (
|
|
614
|
-
'<article class="member"><div class="member-title"><span>' + escapeHtml(member.display_name || member.participant_id) + '</span>' + pill(member.kind, member.kind === 'agent' ? 'good' : '') + '</div>' +
|
|
615
|
-
'<div class="pills">' + pill(member.source) + pill(member.status) + '</div></article>'
|
|
616
|
-
)).join('') : empty('No known members in this room.');
|
|
617
|
-
}
|
|
618
|
-
function openSettings(tab) {
|
|
619
|
-
root('settings-backdrop').classList.add('open');
|
|
620
|
-
root('settings-backdrop').setAttribute('aria-hidden', 'false');
|
|
621
|
-
selectSettingsTab(tab || 'access');
|
|
622
|
-
}
|
|
623
|
-
function closeSettings() {
|
|
624
|
-
root('settings-backdrop').classList.remove('open');
|
|
625
|
-
root('settings-backdrop').setAttribute('aria-hidden', 'true');
|
|
626
|
-
}
|
|
627
|
-
function selectSettingsTab(tab) {
|
|
628
|
-
document.querySelectorAll('.settings-tab').forEach((button) => button.classList.toggle('active', button.dataset.settingsTab === tab));
|
|
629
|
-
document.querySelectorAll('.settings-pane').forEach((pane) => pane.classList.toggle('active', pane.id === 'settings-' + tab));
|
|
630
|
-
}
|
|
631
|
-
function showPanel(id) {
|
|
632
|
-
root(id)?.classList.remove('collapsed');
|
|
633
|
-
}
|
|
634
|
-
function hidePanel(id) {
|
|
635
|
-
root(id)?.classList.add('collapsed');
|
|
636
|
-
}
|
|
637
|
-
function currentMentionQuery() {
|
|
638
|
-
const input = root('message-content');
|
|
639
|
-
if (!input) return null;
|
|
640
|
-
const cursor = input.selectionStart ?? input.value.length;
|
|
641
|
-
const prefix = input.value.slice(0, cursor);
|
|
642
|
-
const match = prefix.match(/(^|\\s)@([\\w.\\-\\u4e00-\\u9fff]*)$/);
|
|
643
|
-
if (!match) return null;
|
|
644
|
-
return { query: match[2].toLowerCase(), start: cursor - match[2].length - 1, end: cursor };
|
|
645
|
-
}
|
|
646
|
-
function renderMentionMenu() {
|
|
647
|
-
const menu = root('mention-menu');
|
|
648
|
-
if (!menu) return;
|
|
649
|
-
const active = currentMentionQuery();
|
|
650
|
-
if (!active) {
|
|
651
|
-
menu.style.display = 'none';
|
|
652
|
-
menu.innerHTML = '';
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
const options = state.mentionables.filter((member) => {
|
|
656
|
-
const haystack = (mentionLabel(member) + ' ' + member.participant_id).toLowerCase();
|
|
657
|
-
return haystack.includes(active.query);
|
|
658
|
-
}).slice(0, 8);
|
|
659
|
-
if (!options.length) {
|
|
660
|
-
menu.style.display = 'none';
|
|
661
|
-
menu.innerHTML = '';
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
state.mentionIndex = Math.min(state.mentionIndex, options.length - 1);
|
|
665
|
-
menu.innerHTML = options.map((member, index) => '<button type="button" class="mention-option ' + (index === state.mentionIndex ? 'active' : '') + '" data-mention="' + escapeHtml(member.participant_id) + '"><span>@' + escapeHtml(mentionLabel(member)) + '</span>' + pill(member.kind, member.kind === 'agent' ? 'good' : '') + '</button>').join('');
|
|
666
|
-
menu.style.display = 'block';
|
|
667
|
-
[...menu.querySelectorAll('.mention-option')].forEach((button, index) => {
|
|
668
|
-
button.addEventListener('mousedown', (event) => {
|
|
669
|
-
event.preventDefault();
|
|
670
|
-
insertMention(options[index]);
|
|
671
|
-
});
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
function insertMention(member) {
|
|
675
|
-
const input = root('message-content');
|
|
676
|
-
const active = currentMentionQuery();
|
|
677
|
-
if (!input || !active) return;
|
|
678
|
-
const token = '@' + mentionToken(member) + ' ';
|
|
679
|
-
input.value = input.value.slice(0, active.start) + token + input.value.slice(active.end);
|
|
680
|
-
const cursor = active.start + token.length;
|
|
681
|
-
input.focus();
|
|
682
|
-
input.setSelectionRange(cursor, cursor);
|
|
683
|
-
renderMentionMenu();
|
|
684
|
-
}
|
|
685
|
-
function bindComposer() {
|
|
686
|
-
const input = root('message-content');
|
|
687
|
-
const form = root('message-form');
|
|
688
|
-
if (!input || input.dataset.bound === '1') return;
|
|
689
|
-
input.dataset.bound = '1';
|
|
690
|
-
input.addEventListener('input', () => { state.mentionIndex = 0; renderMentionMenu(); });
|
|
691
|
-
input.addEventListener('keydown', (event) => {
|
|
692
|
-
const menu = root('mention-menu');
|
|
693
|
-
if (!menu || menu.style.display !== 'block') return;
|
|
694
|
-
const count = menu.querySelectorAll('.mention-option').length;
|
|
695
|
-
if (event.key === 'ArrowDown') {
|
|
696
|
-
event.preventDefault();
|
|
697
|
-
state.mentionIndex = (state.mentionIndex + 1) % count;
|
|
698
|
-
renderMentionMenu();
|
|
699
|
-
} else if (event.key === 'ArrowUp') {
|
|
700
|
-
event.preventDefault();
|
|
701
|
-
state.mentionIndex = (state.mentionIndex + count - 1) % count;
|
|
702
|
-
renderMentionMenu();
|
|
703
|
-
} else if (event.key === 'Enter' && !event.shiftKey) {
|
|
704
|
-
const button = menu.querySelectorAll('.mention-option')[state.mentionIndex];
|
|
705
|
-
if (button) {
|
|
706
|
-
event.preventDefault();
|
|
707
|
-
button.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
|
|
708
|
-
}
|
|
709
|
-
} else if (event.key === 'Escape') {
|
|
710
|
-
menu.style.display = 'none';
|
|
711
|
-
}
|
|
712
|
-
});
|
|
713
|
-
form.addEventListener('submit', sendMessage);
|
|
714
|
-
}
|
|
715
|
-
async function loadRooms() {
|
|
716
|
-
const data = await api('/api/rooms');
|
|
717
|
-
state.rooms = data.rooms || [];
|
|
718
|
-
if (!state.selectedRoomId || !state.rooms.some((room) => room.id === state.selectedRoomId)) {
|
|
719
|
-
state.selectedRoomId = state.rooms[0]?.id || null;
|
|
720
|
-
}
|
|
721
|
-
renderRooms();
|
|
722
|
-
}
|
|
723
|
-
async function loadAgents() {
|
|
724
|
-
const data = await api('/api/agents');
|
|
725
|
-
state.agents = data.agents || [];
|
|
726
|
-
renderAgents();
|
|
727
|
-
}
|
|
728
|
-
async function loadComputers() {
|
|
729
|
-
const data = await api('/api/computers');
|
|
730
|
-
state.computers = data.computers || [];
|
|
731
|
-
renderComputers();
|
|
732
|
-
}
|
|
733
|
-
async function loadLarkConfig() {
|
|
734
|
-
const data = await api('/api/lark/config');
|
|
735
|
-
state.lark = data;
|
|
736
|
-
renderLarkConfig();
|
|
737
|
-
}
|
|
738
|
-
async function loadLarkUsers() {
|
|
739
|
-
const data = await api('/api/lark/authorized-users');
|
|
740
|
-
state.larkUsers = data.users || [];
|
|
741
|
-
renderLarkUsers();
|
|
742
|
-
}
|
|
743
|
-
async function loadServerAccess() {
|
|
744
|
-
const data = await api('/api/server/access');
|
|
745
|
-
state.serverAccess = data;
|
|
746
|
-
renderServerAccess();
|
|
747
|
-
}
|
|
748
|
-
async function loadMessages() {
|
|
749
|
-
const room = activeRoom();
|
|
750
|
-
if (!room) {
|
|
751
|
-
state.messages = [];
|
|
752
|
-
state.members = [];
|
|
753
|
-
state.mentionables = [];
|
|
754
|
-
renderMessages();
|
|
755
|
-
renderMembers();
|
|
756
|
-
return;
|
|
757
|
-
}
|
|
758
|
-
const [messages, members, mentionables] = await Promise.all([
|
|
759
|
-
api('/api/messages?chat_id=' + encodeURIComponent(room.id) + '&limit=100'),
|
|
760
|
-
api('/api/rooms/' + encodeURIComponent(room.id) + '/members'),
|
|
761
|
-
api('/api/rooms/' + encodeURIComponent(room.id) + '/mentionables'),
|
|
762
|
-
]);
|
|
763
|
-
state.messages = messages.messages || [];
|
|
764
|
-
state.members = members.participants || [];
|
|
765
|
-
state.mentionables = mentionables.participants || state.members;
|
|
766
|
-
renderMessages();
|
|
767
|
-
renderMembers();
|
|
768
|
-
}
|
|
769
|
-
async function refresh() {
|
|
770
|
-
await Promise.all([loadRooms(), loadAgents(), loadComputers(), loadLarkConfig(), loadLarkUsers(), loadServerAccess()]);
|
|
771
|
-
await loadMessages();
|
|
772
|
-
}
|
|
773
|
-
window.selectRoom = async (id) => {
|
|
774
|
-
state.selectedRoomId = id;
|
|
775
|
-
renderRooms();
|
|
776
|
-
await loadMessages().catch(showError);
|
|
777
|
-
};
|
|
778
|
-
root('refresh').addEventListener('click', () => refresh().catch(showError));
|
|
779
|
-
root('open-settings').addEventListener('click', () => openSettings('access'));
|
|
780
|
-
root('close-settings').addEventListener('click', closeSettings);
|
|
781
|
-
root('settings-backdrop').addEventListener('mousedown', (event) => {
|
|
782
|
-
if (event.target === root('settings-backdrop')) closeSettings();
|
|
783
|
-
});
|
|
784
|
-
document.addEventListener('keydown', (event) => {
|
|
785
|
-
if (event.key === 'Escape' && root('settings-backdrop').classList.contains('open')) closeSettings();
|
|
786
|
-
});
|
|
787
|
-
document.querySelectorAll('.settings-tab').forEach((button) => {
|
|
788
|
-
button.addEventListener('click', () => selectSettingsTab(button.dataset.settingsTab));
|
|
789
|
-
});
|
|
790
|
-
document.querySelectorAll('[data-add-panel]').forEach((button) => {
|
|
791
|
-
button.addEventListener('click', () => showPanel(button.dataset.addPanel));
|
|
792
|
-
});
|
|
793
|
-
document.querySelectorAll('[data-cancel-panel]').forEach((button) => {
|
|
794
|
-
button.addEventListener('click', () => hidePanel(button.dataset.cancelPanel));
|
|
795
|
-
});
|
|
796
|
-
root('room-form').addEventListener('submit', async (event) => {
|
|
797
|
-
event.preventDefault();
|
|
798
|
-
const name = root('room-name').value.trim();
|
|
799
|
-
if (!name) return;
|
|
800
|
-
const data = await api('/api/rooms', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name, kind: root('room-kind').value }) }).catch((error) => { showError(error); return null; });
|
|
801
|
-
if (!data) return;
|
|
802
|
-
root('room-name').value = '';
|
|
803
|
-
state.selectedRoomId = data.room.id;
|
|
804
|
-
await refresh();
|
|
805
|
-
showToast('Room created: #' + data.room.name);
|
|
806
|
-
});
|
|
807
|
-
root('agent-form').addEventListener('submit', async (event) => {
|
|
808
|
-
event.preventDefault();
|
|
809
|
-
const agent_key = root('agent-key').value.trim();
|
|
810
|
-
const display_name = root('agent-name').value.trim();
|
|
811
|
-
const runtime = root('agent-runtime').value.trim() || 'codex';
|
|
812
|
-
const computer_id = root('agent-computer').value.trim() || undefined;
|
|
813
|
-
const description = root('agent-desc').value.trim() || null;
|
|
814
|
-
if (!agent_key || !display_name) return;
|
|
815
|
-
const saved = await api('/api/agents/onboard', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ agent_key, display_name, runtime, description, computer_id }) }).catch((error) => { showError(error); return null; });
|
|
816
|
-
if (!saved) return;
|
|
817
|
-
root('agent-key').value = '';
|
|
818
|
-
root('agent-name').value = '';
|
|
819
|
-
root('agent-runtime').value = 'codex';
|
|
820
|
-
root('agent-computer').value = '';
|
|
821
|
-
root('agent-desc').value = '';
|
|
822
|
-
await loadAgents();
|
|
823
|
-
hidePanel('agent-form');
|
|
824
|
-
showToast('Agent saved: @' + agent_key);
|
|
825
|
-
});
|
|
826
|
-
root('computer-form').addEventListener('submit', async (event) => {
|
|
827
|
-
event.preventDefault();
|
|
828
|
-
const name = root('computer-name').value.trim();
|
|
829
|
-
const server_url = root('computer-server').value.trim() || location.origin;
|
|
830
|
-
const package_name = root('computer-package').value.trim() || undefined;
|
|
831
|
-
const data = await api('/api/computers/provision', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: name || undefined, server_url, package_name }) }).catch((error) => { showError(error); return null; });
|
|
832
|
-
if (!data) return;
|
|
833
|
-
root('computer-command').value = data.command;
|
|
834
|
-
await loadComputers();
|
|
835
|
-
showToast('Computer provisioned: ' + data.computer.id);
|
|
836
|
-
});
|
|
837
|
-
root('copy-command').addEventListener('click', async () => {
|
|
838
|
-
const command = root('computer-command').value;
|
|
839
|
-
if (!command) return;
|
|
840
|
-
await navigator.clipboard?.writeText(command).catch(() => null);
|
|
841
|
-
showToast('Daemon command copied');
|
|
842
|
-
});
|
|
843
|
-
root('computer-server').value = location.origin;
|
|
844
|
-
root('lark-user-form').addEventListener('submit', async (event) => {
|
|
845
|
-
event.preventDefault();
|
|
846
|
-
const user_id = root('lark-user-id').value.trim();
|
|
847
|
-
const display_name = root('lark-user-name').value.trim() || null;
|
|
848
|
-
if (!user_id) return;
|
|
849
|
-
const saved = await api('/api/lark/authorized-users', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ user_id, display_name }) }).catch((error) => { showError(error); return null; });
|
|
850
|
-
if (!saved) return;
|
|
851
|
-
root('lark-user-id').value = '';
|
|
852
|
-
root('lark-user-name').value = '';
|
|
853
|
-
await loadLarkUsers();
|
|
854
|
-
hidePanel('lark-user-form');
|
|
855
|
-
showToast('Lark user authorized');
|
|
856
|
-
});
|
|
857
|
-
root('lark-form').addEventListener('submit', async (event) => {
|
|
858
|
-
event.preventDefault();
|
|
859
|
-
const agent = root('lark-agent').value;
|
|
860
|
-
const app_id = root('lark-app-id').value.trim();
|
|
861
|
-
const app_secret = root('lark-app-secret').value.trim();
|
|
862
|
-
const label = root('lark-label').value.trim() || undefined;
|
|
863
|
-
if (!agent || !app_id || !app_secret) return;
|
|
864
|
-
const data = await api('/api/lark/setup', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ agent, app_id, app_secret, label }) }).catch((error) => { showError(error); return null; });
|
|
865
|
-
if (!data) return;
|
|
866
|
-
root('lark-app-secret').value = '';
|
|
867
|
-
await loadLarkConfig();
|
|
868
|
-
const reload = await api('/api/lark/reload', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' }).catch((error) => ({ reloadError: error.message }));
|
|
869
|
-
if (!reload.reloadError) hidePanel('lark-form');
|
|
870
|
-
showToast(reload.reloadError ? 'Lark saved, reload failed: ' + reload.reloadError : 'Lark bot saved and reloaded: ' + data.appId);
|
|
871
|
-
});
|
|
872
|
-
root('invite-form').addEventListener('submit', async (event) => {
|
|
873
|
-
event.preventDefault();
|
|
874
|
-
const room = activeRoom();
|
|
875
|
-
const agent = root('invite-agent').value;
|
|
876
|
-
if (!room || !agent) return;
|
|
877
|
-
const mode = root('invite-mode').value;
|
|
878
|
-
const data = await api('/api/rooms/' + encodeURIComponent(room.id) + '/agents', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ agent, mode }) }).catch((error) => { showError(error); return null; });
|
|
879
|
-
if (!data) return;
|
|
880
|
-
await loadMessages();
|
|
881
|
-
showToast('@' + agent + ' invited with ' + data.subscription.mode + ' mode');
|
|
882
|
-
});
|
|
883
|
-
async function sendMessage(event) {
|
|
884
|
-
event.preventDefault();
|
|
885
|
-
const room = activeRoom();
|
|
886
|
-
if (!canWriteRoom(room)) return;
|
|
887
|
-
const content = root('message-content')?.value.trim();
|
|
888
|
-
const sender = root('message-sender')?.value.trim();
|
|
889
|
-
if (!room || !content || !sender) return;
|
|
890
|
-
const mentions = Array.from(new Set((content.match(/@[\\w.\\-\\u4e00-\\u9fff]+/g) || []).map((item) => item.slice(1))));
|
|
891
|
-
const data = await api('/api/messages', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ room_id: room.id, sender, content, mentions }) }).catch((error) => { showError(error); return null; });
|
|
892
|
-
if (!data) return;
|
|
893
|
-
root('message-content').value = '';
|
|
894
|
-
await loadMessages();
|
|
895
|
-
await loadRooms();
|
|
896
|
-
renderRooms();
|
|
897
|
-
showToast('Message sent · deliveries=' + ((data.deliveries || []).length));
|
|
898
|
-
}
|
|
899
|
-
bindComposer();
|
|
900
|
-
refresh().catch(showError);
|
|
901
|
-
</script>
|
|
902
|
-
</body>
|
|
903
|
-
</html>`;
|
|
904
|
-
}
|