@songsid/agend 0.0.1

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.
Files changed (232) hide show
  1. package/README.md +210 -0
  2. package/README.zh-TW.md +134 -0
  3. package/dist/access-path.d.ts +10 -0
  4. package/dist/access-path.js +32 -0
  5. package/dist/access-path.js.map +1 -0
  6. package/dist/adapter-world.d.ts +25 -0
  7. package/dist/adapter-world.js +41 -0
  8. package/dist/adapter-world.js.map +1 -0
  9. package/dist/agent-cli-instructions.md +50 -0
  10. package/dist/agent-cli.d.ts +2 -0
  11. package/dist/agent-cli.js +200 -0
  12. package/dist/agent-cli.js.map +1 -0
  13. package/dist/agent-endpoint.d.ts +25 -0
  14. package/dist/agent-endpoint.js +162 -0
  15. package/dist/agent-endpoint.js.map +1 -0
  16. package/dist/backend/antigravity.d.ts +17 -0
  17. package/dist/backend/antigravity.js +98 -0
  18. package/dist/backend/antigravity.js.map +1 -0
  19. package/dist/backend/claude-code.d.ts +23 -0
  20. package/dist/backend/claude-code.js +171 -0
  21. package/dist/backend/claude-code.js.map +1 -0
  22. package/dist/backend/codex.d.ts +18 -0
  23. package/dist/backend/codex.js +160 -0
  24. package/dist/backend/codex.js.map +1 -0
  25. package/dist/backend/factory.d.ts +2 -0
  26. package/dist/backend/factory.js +28 -0
  27. package/dist/backend/factory.js.map +1 -0
  28. package/dist/backend/gemini-cli.d.ts +17 -0
  29. package/dist/backend/gemini-cli.js +163 -0
  30. package/dist/backend/gemini-cli.js.map +1 -0
  31. package/dist/backend/index.d.ts +7 -0
  32. package/dist/backend/index.js +7 -0
  33. package/dist/backend/index.js.map +1 -0
  34. package/dist/backend/kiro.d.ts +17 -0
  35. package/dist/backend/kiro.js +147 -0
  36. package/dist/backend/kiro.js.map +1 -0
  37. package/dist/backend/marker-utils.d.ts +13 -0
  38. package/dist/backend/marker-utils.js +64 -0
  39. package/dist/backend/marker-utils.js.map +1 -0
  40. package/dist/backend/mock.d.ts +25 -0
  41. package/dist/backend/mock.js +85 -0
  42. package/dist/backend/mock.js.map +1 -0
  43. package/dist/backend/opencode.d.ts +16 -0
  44. package/dist/backend/opencode.js +136 -0
  45. package/dist/backend/opencode.js.map +1 -0
  46. package/dist/backend/types.d.ts +86 -0
  47. package/dist/backend/types.js +33 -0
  48. package/dist/backend/types.js.map +1 -0
  49. package/dist/channel/access-manager.d.ts +18 -0
  50. package/dist/channel/access-manager.js +153 -0
  51. package/dist/channel/access-manager.js.map +1 -0
  52. package/dist/channel/adapters/telegram.d.ts +63 -0
  53. package/dist/channel/adapters/telegram.js +646 -0
  54. package/dist/channel/adapters/telegram.js.map +1 -0
  55. package/dist/channel/attachment-handler.d.ts +15 -0
  56. package/dist/channel/attachment-handler.js +88 -0
  57. package/dist/channel/attachment-handler.js.map +1 -0
  58. package/dist/channel/factory.d.ts +12 -0
  59. package/dist/channel/factory.js +67 -0
  60. package/dist/channel/factory.js.map +1 -0
  61. package/dist/channel/ipc-bridge.d.ts +26 -0
  62. package/dist/channel/ipc-bridge.js +220 -0
  63. package/dist/channel/ipc-bridge.js.map +1 -0
  64. package/dist/channel/mcp-server.d.ts +10 -0
  65. package/dist/channel/mcp-server.js +288 -0
  66. package/dist/channel/mcp-server.js.map +1 -0
  67. package/dist/channel/mcp-tools.d.ts +17 -0
  68. package/dist/channel/mcp-tools.js +110 -0
  69. package/dist/channel/mcp-tools.js.map +1 -0
  70. package/dist/channel/message-bus.d.ts +17 -0
  71. package/dist/channel/message-bus.js +86 -0
  72. package/dist/channel/message-bus.js.map +1 -0
  73. package/dist/channel/message-queue.d.ts +39 -0
  74. package/dist/channel/message-queue.js +253 -0
  75. package/dist/channel/message-queue.js.map +1 -0
  76. package/dist/channel/tool-router.d.ts +6 -0
  77. package/dist/channel/tool-router.js +75 -0
  78. package/dist/channel/tool-router.js.map +1 -0
  79. package/dist/channel/tool-tracker.d.ts +13 -0
  80. package/dist/channel/tool-tracker.js +58 -0
  81. package/dist/channel/tool-tracker.js.map +1 -0
  82. package/dist/channel/types.d.ts +118 -0
  83. package/dist/channel/types.js +2 -0
  84. package/dist/channel/types.js.map +1 -0
  85. package/dist/chat-export.d.ts +4 -0
  86. package/dist/chat-export.js +91 -0
  87. package/dist/chat-export.js.map +1 -0
  88. package/dist/classic-channel-manager.d.ts +59 -0
  89. package/dist/classic-channel-manager.js +193 -0
  90. package/dist/classic-channel-manager.js.map +1 -0
  91. package/dist/cli.d.ts +2 -0
  92. package/dist/cli.js +1833 -0
  93. package/dist/cli.js.map +1 -0
  94. package/dist/config.d.ts +9 -0
  95. package/dist/config.js +118 -0
  96. package/dist/config.js.map +1 -0
  97. package/dist/context-guardian.d.ts +26 -0
  98. package/dist/context-guardian.js +73 -0
  99. package/dist/context-guardian.js.map +1 -0
  100. package/dist/cost-guard.d.ts +36 -0
  101. package/dist/cost-guard.js +147 -0
  102. package/dist/cost-guard.js.map +1 -0
  103. package/dist/daemon-entry.d.ts +1 -0
  104. package/dist/daemon-entry.js +29 -0
  105. package/dist/daemon-entry.js.map +1 -0
  106. package/dist/daemon.d.ts +152 -0
  107. package/dist/daemon.js +1714 -0
  108. package/dist/daemon.js.map +1 -0
  109. package/dist/daily-summary.d.ts +13 -0
  110. package/dist/daily-summary.js +55 -0
  111. package/dist/daily-summary.js.map +1 -0
  112. package/dist/event-log.d.ts +36 -0
  113. package/dist/event-log.js +100 -0
  114. package/dist/event-log.js.map +1 -0
  115. package/dist/export-import.d.ts +2 -0
  116. package/dist/export-import.js +162 -0
  117. package/dist/export-import.js.map +1 -0
  118. package/dist/fleet-context.d.ts +61 -0
  119. package/dist/fleet-context.js +4 -0
  120. package/dist/fleet-context.js.map +1 -0
  121. package/dist/fleet-dashboard-html.d.ts +6 -0
  122. package/dist/fleet-dashboard-html.js +443 -0
  123. package/dist/fleet-dashboard-html.js.map +1 -0
  124. package/dist/fleet-health-server.d.ts +35 -0
  125. package/dist/fleet-health-server.js +290 -0
  126. package/dist/fleet-health-server.js.map +1 -0
  127. package/dist/fleet-instructions.d.ts +5 -0
  128. package/dist/fleet-instructions.js +161 -0
  129. package/dist/fleet-instructions.js.map +1 -0
  130. package/dist/fleet-manager.d.ts +212 -0
  131. package/dist/fleet-manager.js +3655 -0
  132. package/dist/fleet-manager.js.map +1 -0
  133. package/dist/fleet-rpc-handlers.d.ts +42 -0
  134. package/dist/fleet-rpc-handlers.js +356 -0
  135. package/dist/fleet-rpc-handlers.js.map +1 -0
  136. package/dist/fleet-system-prompt.d.ts +11 -0
  137. package/dist/fleet-system-prompt.js +61 -0
  138. package/dist/fleet-system-prompt.js.map +1 -0
  139. package/dist/general-knowledge/skills.md +177 -0
  140. package/dist/hang-detector.d.ts +16 -0
  141. package/dist/hang-detector.js +53 -0
  142. package/dist/hang-detector.js.map +1 -0
  143. package/dist/index.d.ts +8 -0
  144. package/dist/index.js +6 -0
  145. package/dist/index.js.map +1 -0
  146. package/dist/instance-lifecycle.d.ts +90 -0
  147. package/dist/instance-lifecycle.js +592 -0
  148. package/dist/instance-lifecycle.js.map +1 -0
  149. package/dist/instructions.d.ts +15 -0
  150. package/dist/instructions.js +90 -0
  151. package/dist/instructions.js.map +1 -0
  152. package/dist/logger.d.ts +7 -0
  153. package/dist/logger.js +84 -0
  154. package/dist/logger.js.map +1 -0
  155. package/dist/outbound-handlers.d.ts +51 -0
  156. package/dist/outbound-handlers.js +739 -0
  157. package/dist/outbound-handlers.js.map +1 -0
  158. package/dist/outbound-schemas.d.ts +238 -0
  159. package/dist/outbound-schemas.js +248 -0
  160. package/dist/outbound-schemas.js.map +1 -0
  161. package/dist/paths.d.ts +10 -0
  162. package/dist/paths.js +42 -0
  163. package/dist/paths.js.map +1 -0
  164. package/dist/plugin/agend/.claude-plugin/plugin.json +5 -0
  165. package/dist/quickstart.d.ts +1 -0
  166. package/dist/quickstart.js +595 -0
  167. package/dist/quickstart.js.map +1 -0
  168. package/dist/routing-engine.d.ts +22 -0
  169. package/dist/routing-engine.js +44 -0
  170. package/dist/routing-engine.js.map +1 -0
  171. package/dist/safe-async.d.ts +6 -0
  172. package/dist/safe-async.js +20 -0
  173. package/dist/safe-async.js.map +1 -0
  174. package/dist/scheduler/db.d.ts +37 -0
  175. package/dist/scheduler/db.js +360 -0
  176. package/dist/scheduler/db.js.map +1 -0
  177. package/dist/scheduler/db.test.d.ts +1 -0
  178. package/dist/scheduler/db.test.js +92 -0
  179. package/dist/scheduler/db.test.js.map +1 -0
  180. package/dist/scheduler/index.d.ts +4 -0
  181. package/dist/scheduler/index.js +4 -0
  182. package/dist/scheduler/index.js.map +1 -0
  183. package/dist/scheduler/scheduler.d.ts +44 -0
  184. package/dist/scheduler/scheduler.js +197 -0
  185. package/dist/scheduler/scheduler.js.map +1 -0
  186. package/dist/scheduler/scheduler.test.d.ts +1 -0
  187. package/dist/scheduler/scheduler.test.js +119 -0
  188. package/dist/scheduler/scheduler.test.js.map +1 -0
  189. package/dist/scheduler/types.d.ts +107 -0
  190. package/dist/scheduler/types.js +7 -0
  191. package/dist/scheduler/types.js.map +1 -0
  192. package/dist/service-installer.d.ts +17 -0
  193. package/dist/service-installer.js +182 -0
  194. package/dist/service-installer.js.map +1 -0
  195. package/dist/setup-wizard.d.ts +48 -0
  196. package/dist/setup-wizard.js +701 -0
  197. package/dist/setup-wizard.js.map +1 -0
  198. package/dist/statusline-watcher.d.ts +34 -0
  199. package/dist/statusline-watcher.js +73 -0
  200. package/dist/statusline-watcher.js.map +1 -0
  201. package/dist/stt.d.ts +10 -0
  202. package/dist/stt.js +33 -0
  203. package/dist/stt.js.map +1 -0
  204. package/dist/tmux-control.d.ts +52 -0
  205. package/dist/tmux-control.js +207 -0
  206. package/dist/tmux-control.js.map +1 -0
  207. package/dist/tmux-manager.d.ts +44 -0
  208. package/dist/tmux-manager.js +218 -0
  209. package/dist/tmux-manager.js.map +1 -0
  210. package/dist/topic-archiver.d.ts +40 -0
  211. package/dist/topic-archiver.js +103 -0
  212. package/dist/topic-archiver.js.map +1 -0
  213. package/dist/topic-commands.d.ts +28 -0
  214. package/dist/topic-commands.js +359 -0
  215. package/dist/topic-commands.js.map +1 -0
  216. package/dist/transcript-monitor.d.ts +23 -0
  217. package/dist/transcript-monitor.js +164 -0
  218. package/dist/transcript-monitor.js.map +1 -0
  219. package/dist/types.d.ts +211 -0
  220. package/dist/types.js +2 -0
  221. package/dist/types.js.map +1 -0
  222. package/dist/ui/dashboard.html +719 -0
  223. package/dist/web-api.d.ts +101 -0
  224. package/dist/web-api.js +648 -0
  225. package/dist/web-api.js.map +1 -0
  226. package/dist/webhook-emitter.d.ts +15 -0
  227. package/dist/webhook-emitter.js +41 -0
  228. package/dist/webhook-emitter.js.map +1 -0
  229. package/dist/workflow-templates/default.md +35 -0
  230. package/package.json +76 -0
  231. package/templates/launchd.plist.ejs +31 -0
  232. package/templates/systemd.service.ejs +16 -0
@@ -0,0 +1,719 @@
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
+ <meta name="referrer" content="no-referrer">
7
+ <title>AgEnD Dashboard</title>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800&family=DM+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
10
+ <style>
11
+ :root {
12
+ --bg: #080b12; --surface: rgba(22,27,34,0.7); --raised: rgba(28,33,41,0.8);
13
+ --border: rgba(48,54,61,0.6); --border-bright: rgba(48,54,61,0.9);
14
+ --text: #f0f6fc; --text-sec: #8b949e; --text-muted: #484f58;
15
+ --accent: #2AABEE; --accent-dim: rgba(42,171,238,0.08); --accent-glow: rgba(42,171,238,0.2);
16
+ --success: #56d364; --error: #f85149; --warn: #d29922;
17
+ --glass: rgba(22,27,34,0.55); --glass-border: rgba(255,255,255,0.06);
18
+ }
19
+ * { margin: 0; padding: 0; box-sizing: border-box; }
20
+ html { font-size: 14px; }
21
+ body {
22
+ background: var(--bg); color: var(--text);
23
+ font-family: 'DM Sans', system-ui, sans-serif;
24
+ display: flex; height: 100vh;
25
+ background-image:
26
+ radial-gradient(ellipse 120% 80% at 10% 20%, rgba(42,171,238,0.04), transparent 50%),
27
+ radial-gradient(ellipse 80% 60% at 90% 80%, rgba(86,211,100,0.03), transparent 50%);
28
+ }
29
+
30
+ /* ── Sidebar ─────────────────────────────────────────── */
31
+ .sidebar {
32
+ width: 280px; flex-shrink: 0; display: flex; flex-direction: column;
33
+ background: var(--glass); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
34
+ border-right: 1px solid var(--glass-border);
35
+ }
36
+ .sidebar-header {
37
+ padding: 24px 20px 20px;
38
+ background: linear-gradient(135deg, rgba(42,171,238,0.08) 0%, transparent 60%);
39
+ border-bottom: 1px solid var(--glass-border);
40
+ }
41
+ .sidebar-header h1 {
42
+ font-family: 'Outfit', sans-serif; font-size: 22px; font-weight: 800;
43
+ letter-spacing: -0.03em; color: var(--text);
44
+ background: linear-gradient(135deg, var(--text) 60%, var(--accent));
45
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
46
+ }
47
+ .sidebar-header .uptime {
48
+ font-size: 11px; color: var(--text-muted); margin-top: 6px;
49
+ font-family: 'IBM Plex Mono', monospace; letter-spacing: 0.02em;
50
+ }
51
+ .nav-section {
52
+ font-family: 'Outfit', sans-serif; font-size: 10px; font-weight: 700;
53
+ letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-muted);
54
+ padding: 16px 20px 8px;
55
+ }
56
+ .fleet-entry {
57
+ margin: 0 12px; padding: 10px 12px; cursor: pointer;
58
+ border-radius: 10px; display: flex; align-items: center; gap: 10px;
59
+ font-family: 'Outfit', sans-serif; font-weight: 600; font-size: 13px;
60
+ color: var(--accent); transition: all 0.15s;
61
+ border: 1px solid transparent;
62
+ }
63
+ .fleet-entry:hover { background: var(--accent-dim); }
64
+ .fleet-entry.active { background: var(--accent-dim); border-color: rgba(42,171,238,0.2); box-shadow: 0 0 20px rgba(42,171,238,0.06); }
65
+ .instance-list { flex: 1; overflow-y: auto; padding: 4px 0; }
66
+ .instance-item {
67
+ margin: 2px 12px; padding: 10px 12px; cursor: pointer;
68
+ border-radius: 10px; display: flex; align-items: center; gap: 10px;
69
+ transition: all 0.15s; border: 1px solid transparent;
70
+ }
71
+ .instance-item:hover { background: var(--raised); }
72
+ .instance-item.active { background: var(--accent-dim); border-color: rgba(42,171,238,0.15); }
73
+ .dot {
74
+ width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
75
+ transition: box-shadow 0.3s;
76
+ }
77
+ .dot.running { background: var(--success); box-shadow: 0 0 8px rgba(86,211,100,0.5); }
78
+ .dot.stopped { background: var(--error); opacity: 0.7; }
79
+ .dot.crashed { background: var(--warn); }
80
+ .inst-name { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
81
+ .inst-meta { font-size: 11px; color: var(--text-muted); font-family: 'IBM Plex Mono', monospace; }
82
+ .sidebar-footer { padding: 16px; border-top: 1px solid var(--glass-border); }
83
+
84
+ /* ── Main ────────────────────────────────────────────── */
85
+ .main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
86
+ .no-sel { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px; color: var(--text-muted); }
87
+ .no-sel svg { opacity: 0.15; }
88
+ .no-sel span { font-family: 'Outfit', sans-serif; font-size: 15px; font-weight: 500; letter-spacing: -0.01em; }
89
+
90
+ /* ── Tabs ─────────────────────────────────────────────── */
91
+ .tabs {
92
+ display: flex; gap: 2px; padding: 8px 16px;
93
+ background: var(--glass); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
94
+ border-bottom: 1px solid var(--glass-border);
95
+ }
96
+ .tab {
97
+ padding: 8px 16px; cursor: pointer; border-radius: 8px;
98
+ font-family: 'Outfit', sans-serif; font-size: 13px; font-weight: 500;
99
+ color: var(--text-muted); transition: all 0.15s;
100
+ }
101
+ .tab:hover { color: var(--text-sec); background: rgba(255,255,255,0.03); }
102
+ .tab.active { color: var(--accent); background: var(--accent-dim); }
103
+
104
+ /* ── Action buttons ──────────────────────────────────── */
105
+ .actions { display: flex; gap: 8px; padding: 12px 16px; border-bottom: 1px solid var(--glass-border); }
106
+ .btn {
107
+ padding: 7px 14px; border-radius: 8px; font-size: 12px; font-weight: 600;
108
+ cursor: pointer; border: 1px solid var(--border); font-family: 'DM Sans', sans-serif;
109
+ transition: all 0.15s; display: inline-flex; align-items: center; gap: 5px;
110
+ }
111
+ .btn:active:not(:disabled) { transform: scale(0.96); }
112
+ .btn:disabled { opacity: 0.35; cursor: default; }
113
+ .btn-accent { background: var(--accent); color: #fff; border-color: transparent; }
114
+ .btn-accent:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(42,171,238,0.3); }
115
+ .btn-green { background: rgba(35,134,54,0.9); color: #fff; border-color: transparent; }
116
+ .btn-green:hover:not(:disabled) { box-shadow: 0 4px 16px rgba(35,134,54,0.3); }
117
+ .btn-warn { background: rgba(158,106,3,0.9); color: #fff; border-color: transparent; }
118
+ .btn-warn:hover:not(:disabled) { box-shadow: 0 4px 16px rgba(158,106,3,0.3); }
119
+ .btn-red { background: rgba(218,54,51,0.9); color: #fff; border-color: transparent; }
120
+ .btn-red:hover:not(:disabled) { box-shadow: 0 4px 16px rgba(218,54,51,0.3); }
121
+ .btn-ghost { background: transparent; color: var(--text-sec); }
122
+ .btn-ghost:hover:not(:disabled) { color: var(--text); background: var(--raised); }
123
+
124
+ /* ── Toast ───────────────────────────────────────────── */
125
+ .toast {
126
+ position: fixed; bottom: 24px; right: 24px; padding: 12px 20px;
127
+ border-radius: 12px; font-size: 13px; font-weight: 500; z-index: 100;
128
+ backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
129
+ animation: toastIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
130
+ border: 1px solid var(--glass-border);
131
+ }
132
+ .toast.ok { background: rgba(35,134,54,0.85); color: #fff; }
133
+ .toast.err { background: rgba(218,54,51,0.85); color: #fff; }
134
+ @keyframes toastIn { from { transform: translateY(16px) scale(0.95); opacity: 0; } to { transform: translateY(0) scale(1); opacity: 1; } }
135
+
136
+ /* ── Chat ─────────────────────────────────────────────── */
137
+ .chat-view { flex: 1; display: flex; flex-direction: column; }
138
+ .messages { flex: 1; overflow-y: auto; padding: 20px 24px; display: flex; flex-direction: column; gap: 10px; }
139
+ .msg {
140
+ max-width: 78%; padding: 12px 16px; border-radius: 16px;
141
+ font-size: 13px; line-height: 1.65; white-space: pre-wrap; word-break: break-word;
142
+ }
143
+ .msg.inbound {
144
+ align-self: flex-end; border-bottom-right-radius: 4px;
145
+ background: linear-gradient(135deg, rgba(42,171,238,0.12), rgba(42,171,238,0.06));
146
+ border: 1px solid rgba(42,171,238,0.1);
147
+ }
148
+ .msg.outbound {
149
+ align-self: flex-start; border-bottom-left-radius: 4px;
150
+ background: var(--surface); border: 1px solid var(--glass-border);
151
+ border-left: 2px solid var(--success);
152
+ }
153
+ .msg .sender { font-size: 11px; font-weight: 600; margin-bottom: 4px; font-family: 'Outfit', sans-serif; }
154
+ .msg .sender.user { color: var(--accent); }
155
+ .msg .sender.agent { color: var(--success); }
156
+ .msg .time { font-size: 10px; color: var(--text-muted); float: right; margin-left: 12px; font-family: 'IBM Plex Mono', monospace; }
157
+ .msg-empty { color: var(--text-muted); text-align: center; margin-top: 80px; font-size: 13px; line-height: 2; }
158
+ .msg-empty svg { display: block; margin: 0 auto 12px; opacity: 0.15; }
159
+ .input-bar { padding: 16px 20px; border-top: 1px solid var(--glass-border); display: flex; gap: 8px; }
160
+ .input-bar input {
161
+ flex: 1; background: rgba(255,255,255,0.04); color: var(--text);
162
+ border: 1px solid var(--border); border-radius: 12px; padding: 11px 16px;
163
+ font-size: 13px; font-family: 'DM Sans', sans-serif; outline: none; transition: all 0.15s;
164
+ }
165
+ .input-bar input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
166
+
167
+ /* ── Detail cards ────────────────────────────────────── */
168
+ .detail-view { flex: 1; overflow-y: auto; padding: 24px; display: none; }
169
+ .card {
170
+ background: var(--surface); border: 1px solid var(--glass-border);
171
+ border-radius: 16px; padding: 20px 24px; margin-bottom: 16px;
172
+ backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
173
+ transition: border-color 0.2s;
174
+ }
175
+ .card:hover { border-color: rgba(255,255,255,0.1); }
176
+ .card h3 {
177
+ font-family: 'Outfit', sans-serif; font-size: 10px; font-weight: 700;
178
+ letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent);
179
+ margin-bottom: 14px;
180
+ }
181
+ .detail-row { display: flex; align-items: center; gap: 10px; font-size: 13px; padding: 6px 0; }
182
+ .detail-label { color: var(--text-muted); min-width: 100px; font-family: 'IBM Plex Mono', monospace; font-size: 11px; }
183
+ .progress-bar { display: inline-block; width: 80px; height: 4px; background: rgba(255,255,255,0.06); border-radius: 2px; vertical-align: middle; margin-left: 8px; overflow: hidden; }
184
+ .progress-fill { height: 100%; border-radius: 2px; transition: width 0.4s cubic-bezier(0.4,0,0.2,1); }
185
+ .activity-item { font-size: 12px; padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.03); font-family: 'IBM Plex Mono', monospace; }
186
+ .act-time { color: var(--text-muted); margin-right: 8px; }
187
+ .act-event { color: var(--warn); margin-right: 4px; }
188
+
189
+ /* ── List items (tasks, schedules, teams) ─────────────── */
190
+ .task-view { flex: 1; overflow-y: auto; padding: 24px; display: none; }
191
+ .list-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
192
+ .list-header span { font-family: 'Outfit', sans-serif; font-weight: 600; font-size: 15px; }
193
+ .task-item {
194
+ background: var(--surface); border: 1px solid var(--glass-border);
195
+ border-radius: 12px; padding: 14px 18px; margin-bottom: 10px;
196
+ display: flex; align-items: center; gap: 12px;
197
+ backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
198
+ transition: all 0.15s;
199
+ }
200
+ .task-item:hover { border-color: rgba(255,255,255,0.08); transform: translateY(-1px); }
201
+ .task-status {
202
+ font-size: 10px; padding: 3px 10px; border-radius: 999px; font-weight: 600;
203
+ font-family: 'IBM Plex Mono', monospace; text-transform: uppercase; letter-spacing: 0.06em;
204
+ }
205
+ .task-status.open { background: var(--accent-dim); color: var(--accent); }
206
+ .task-status.claimed { background: rgba(210,153,34,0.12); color: var(--warn); }
207
+ .task-status.done { background: rgba(86,211,100,0.1); color: var(--success); }
208
+ .task-title { flex: 1; font-size: 13px; font-weight: 500; }
209
+ .task-assignee { font-size: 11px; color: var(--text-sec); }
210
+ .task-actions { display: flex; gap: 4px; }
211
+
212
+ /* ── Form overlay ────────────────────────────────────── */
213
+ .form-overlay {
214
+ position: fixed; inset: 0; background: rgba(0,0,0,0.65);
215
+ backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
216
+ display: flex; align-items: center; justify-content: center; z-index: 50;
217
+ animation: fadeIn 0.2s ease;
218
+ }
219
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
220
+ .form-card {
221
+ background: var(--glass); border: 1px solid var(--glass-border);
222
+ border-radius: 20px; padding: 32px; width: 480px; max-width: 92vw; max-height: 82vh;
223
+ overflow-y: auto; backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
224
+ box-shadow: 0 24px 80px rgba(0,0,0,0.4);
225
+ }
226
+ .form-card h2 {
227
+ font-family: 'Outfit', sans-serif; font-size: 20px; font-weight: 700;
228
+ margin-bottom: 24px; letter-spacing: -0.02em;
229
+ }
230
+ .form-field { margin-bottom: 16px; }
231
+ .form-field label {
232
+ display: block; font-size: 11px; font-weight: 600; color: var(--text-muted);
233
+ margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.08em;
234
+ font-family: 'Outfit', sans-serif;
235
+ }
236
+ .form-field input, .form-field select {
237
+ width: 100%; background: rgba(255,255,255,0.04); color: var(--text);
238
+ border: 1px solid var(--border); border-radius: 10px; padding: 10px 14px;
239
+ font-size: 13px; font-family: 'DM Sans', sans-serif; outline: none;
240
+ transition: all 0.15s;
241
+ }
242
+ .form-field input:focus, .form-field select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
243
+ .form-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 24px; }
244
+ .eye-btn { background: none; border: none; cursor: pointer; color: var(--text-muted); font-size: 14px; padding: 4px 6px; border-radius: 6px; transition: all 0.15s; }
245
+ .eye-btn:hover { color: var(--accent); }
246
+
247
+ @media (max-width: 700px) { .sidebar { width: 240px; } }
248
+ </style>
249
+ </head>
250
+ <body>
251
+
252
+ <div class="sidebar">
253
+ <div class="sidebar-header">
254
+ <h1>AgEnD</h1>
255
+ <div class="uptime" id="uptime">--</div>
256
+ </div>
257
+ <div style="padding:8px 12px 0">
258
+ <div class="fleet-entry" id="fleetEntry" onclick="selFleet()">
259
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
260
+ Fleet
261
+ </div>
262
+ </div>
263
+ <div class="nav-section">Instances</div>
264
+ <div class="instance-list" id="instanceList"></div>
265
+ <div class="sidebar-footer">
266
+ <button class="btn btn-accent" style="width:100%;justify-content:center" onclick="showCreateInstance()">+ New Instance</button>
267
+ </div>
268
+ </div>
269
+
270
+ <div class="main" id="mainArea">
271
+ <div class="no-sel">
272
+ <svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
273
+ <span>Select Fleet or an instance to begin</span>
274
+ </div>
275
+ </div>
276
+
277
+ <script>
278
+ const T = new URLSearchParams(location.search).get("token") || "";
279
+ let cur = null, curTab = "chat", mode = "none", instances = [];
280
+ // mode: "none" | "fleet" | "instance"
281
+ const msgs = {};
282
+
283
+ async function api(m, p, b) {
284
+ const o = { method: m, headers: { "Content-Type": "application/json" } };
285
+ if (b) o.body = JSON.stringify(b);
286
+ const r = await fetch(`${p}${p.includes("?") ? "&" : "?"}token=${T}`, o);
287
+ return r.json();
288
+ }
289
+ function esc(s) { const d = document.createElement("div"); d.textContent = s || ""; return d.innerHTML; }
290
+ function fmtUp(s) { return s < 60 ? `${s}s` : s < 3600 ? `${Math.floor(s/60)}m` : `${Math.floor(s/3600)}h ${Math.floor((s%3600)/60)}m`; }
291
+ function toast(msg, ok=true) {
292
+ const el = document.createElement("div"); el.className = `toast ${ok?"ok":"err"}`; el.textContent = msg;
293
+ document.body.appendChild(el); setTimeout(() => el.remove(), 3000);
294
+ }
295
+
296
+ const sse = new EventSource(`/ui/events?token=${T}`);
297
+ sse.addEventListener("status", e => {
298
+ const d = JSON.parse(e.data); instances = d.instances;
299
+ document.getElementById("uptime").textContent = fmtUp(d.uptime);
300
+ renderList();
301
+ if (cur) { const i = instances.find(x => x.name === cur); const a = document.getElementById("actions"); if (i && a) renderActions(i); }
302
+ });
303
+ sse.addEventListener("message", e => {
304
+ const m = JSON.parse(e.data);
305
+ if (!msgs[m.instance]) msgs[m.instance] = [];
306
+ msgs[m.instance].push(m);
307
+ if (msgs[m.instance].length > 500) msgs[m.instance].shift();
308
+ if (m.instance === cur && curTab === "chat") renderMsgs();
309
+ });
310
+ sse.onerror = () => { document.getElementById("uptime").textContent = "disconnected"; };
311
+
312
+ function renderList() {
313
+ document.getElementById("fleetEntry").className = `fleet-entry${mode==="fleet"?" active":""}`;
314
+ const el = document.getElementById("instanceList");
315
+ el.innerHTML = instances.map(i => `<div class="instance-item${i.name===cur&&mode==="instance"?" active":""}" data-n="${esc(i.name)}">
316
+ <div class="dot ${i.status}" title="${i.status}"></div><div style="flex:1;min-width:0">
317
+ <div class="inst-name">${esc(i.name)}</div>
318
+ <div class="inst-meta">${i.cost>0?`$${i.cost.toFixed(2)}`:""}${i.context_pct>0?` ctx:${Math.round(i.context_pct)}%`:""}</div>
319
+ </div></div>`).join("");
320
+ el.querySelectorAll(".instance-item").forEach(x => x.addEventListener("click", () => sel(x.dataset.n)));
321
+ }
322
+
323
+ function selFleet() { mode = "fleet"; cur = null; curTab = "tasks"; renderList(); renderMain(); }
324
+ function sel(name) { mode = "instance"; cur = name; curTab = "chat"; renderMain(); renderList(); }
325
+
326
+ function renderMain() {
327
+ const main = document.getElementById("mainArea");
328
+ if (mode === "fleet") return renderFleetMain(main);
329
+ if (mode === "instance" && cur) return renderInstanceMain(main);
330
+ main.innerHTML = '<div class="no-sel"><svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg><span>Select Fleet or an instance to begin</span></div>';
331
+ }
332
+
333
+ function renderFleetMain(main) {
334
+ const tabs = ["tasks","schedules","teams","config"];
335
+ if (!tabs.includes(curTab)) curTab = "tasks";
336
+ main.innerHTML = `
337
+ <div class="tabs">${tabs.map(t => `<div class="tab${curTab===t?" active":""}" data-t="${t}">${t[0].toUpperCase()+t.slice(1)}</div>`).join("")}</div>
338
+ <div class="task-view" id="taskView" style="display:${curTab==="tasks"?"block":"none"}"><div style="color:var(--text-muted);text-align:center;padding:24px">Loading...</div></div>
339
+ <div class="task-view" id="schedView" style="display:${curTab==="schedules"?"block":"none"}"><div style="color:var(--text-muted);text-align:center;padding:24px">Loading...</div></div>
340
+ <div class="task-view" id="teamView" style="display:${curTab==="teams"?"block":"none"}"><div style="color:var(--text-muted);text-align:center;padding:24px">Loading...</div></div>
341
+ <div class="detail-view" id="configView" style="display:${curTab==="config"?"block":"none"}"><div style="color:var(--text-muted);text-align:center;padding:24px">Loading...</div></div>`;
342
+ main.querySelectorAll(".tab").forEach(t => t.addEventListener("click", () => { curTab = t.dataset.t; renderMain(); }));
343
+ if (curTab === "tasks") loadTasks();
344
+ else if (curTab === "schedules") loadSchedules();
345
+ else if (curTab === "teams") loadTeams();
346
+ else if (curTab === "config") loadConfig();
347
+ }
348
+
349
+ function renderInstanceMain(main) {
350
+ const tabs = ["chat","detail"];
351
+ if (!tabs.includes(curTab)) curTab = "chat";
352
+ main.innerHTML = `
353
+ <div class="tabs">${tabs.map(t => `<div class="tab${curTab===t?" active":""}" data-t="${t}">${t[0].toUpperCase()+t.slice(1)}</div>`).join("")}</div>
354
+ <div class="actions" id="actions"></div>
355
+ <div class="chat-view" id="chatView" style="display:${curTab==="chat"?"flex":"none"}">
356
+ <div class="messages" id="messages"></div>
357
+ <div class="input-bar"><input id="msgIn" placeholder="Send a message..." autocomplete="off"><button class="btn btn-accent" onclick="sendMsg()">Send</button></div>
358
+ </div>
359
+ <div class="detail-view" id="detailView" style="display:${curTab==="detail"?"block":"none"}"><div style="color:var(--text-muted);text-align:center;padding:24px">Loading...</div></div>`;
360
+ main.querySelectorAll(".tab").forEach(t => t.addEventListener("click", () => { curTab = t.dataset.t; renderMain(); }));
361
+ const i = instances.find(x => x.name === cur); renderActions(i);
362
+ if (curTab === "chat") { renderMsgs(); const inp = document.getElementById("msgIn"); inp.addEventListener("keydown", e => { if (e.key==="Enter"&&!e.shiftKey&&!e.isComposing){e.preventDefault();sendMsg();} }); inp.focus(); }
363
+ else if (curTab === "detail") loadDetail();
364
+ }
365
+
366
+ function renderActions(i) {
367
+ const el = document.getElementById("actions"); if (!el || !i) return;
368
+ const r = i.status === "running";
369
+ el.innerHTML = `${r
370
+ ?`<button class="btn btn-warn" onclick="doAction('stop')"><svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="1"/></svg> Stop</button><button class="btn btn-warn" onclick="doAction('restart')"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M1 4v6h6M23 20v-6h-6"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/></svg> Restart</button>`
371
+ :`<button class="btn btn-green" onclick="doAction('start')"><svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg> Start</button>`}
372
+ <button class="btn btn-red" onclick="doAction('delete')"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg> Delete</button>`;
373
+ }
374
+ async function doAction(a) {
375
+ if (!cur) return;
376
+ if (a === "delete") {
377
+ const c = prompt(`Type "delete ${cur}" to confirm:`);
378
+ if (c !== `delete ${cur}`) return;
379
+ const r = await api("POST", `/ui/instances/${encodeURIComponent(cur)}/delete`, { confirm: c });
380
+ if (r.error) { toast(r.error, false); return; }
381
+ toast(`${cur} deleted`); cur = null;
382
+ document.getElementById("mainArea").innerHTML = '<div class="no-sel"><span>Instance deleted</span></div>';
383
+ return;
384
+ }
385
+ const ep = `/ui/${a}/${encodeURIComponent(cur)}`;
386
+ const r = await api("POST", ep);
387
+ if (r.error) toast(r.error, false);
388
+ else toast(`${cur} ${a}ed`);
389
+ }
390
+
391
+ function renderMsgs() {
392
+ const el = document.getElementById("messages"); if (!el) return;
393
+ const m = msgs[cur] || [];
394
+ if (!m.length) { el.innerHTML = '<div class="msg-empty"><svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>No messages yet<br><span style="font-size:12px;color:var(--text-muted)">Send a message to start the conversation</span></div>'; return; }
395
+ el.innerHTML = m.map(x => {
396
+ const u = x.sender==="web-user"||(!x.sender.startsWith("agend")&&x.sender!==cur&&x.sender!=="general");
397
+ const t = x.ts ? new Date(x.ts).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"}) : "";
398
+ return `<div class="msg ${u?"inbound":"outbound"}"><div class="sender ${u?"user":"agent"}">${esc(x.sender)}<span class="time">${t}</span></div>${esc(x.text)}</div>`;
399
+ }).join("");
400
+ el.scrollTop = el.scrollHeight;
401
+ }
402
+ async function sendMsg() {
403
+ const inp = document.getElementById("msgIn"), txt = inp.value.trim();
404
+ if (!txt||!cur) return; inp.value = "";
405
+ await api("POST", "/ui/send", { instance: cur, message: txt });
406
+ }
407
+
408
+ async function loadDetail() {
409
+ const el = document.getElementById("detailView"); if (!el||!cur) return;
410
+ try {
411
+ const d = await api("GET", `/ui/instance/${encodeURIComponent(cur)}`);
412
+ const sl = d.statusline||{}, cost=sl.cost?.total_cost_usd??0, ctx=sl.context_window?.used_percentage??0;
413
+ const model=sl.model?.display_name??"--", rl5=sl.rate_limits?.five_hour?.used_percentage??0, rl7=sl.rate_limits?.seven_day?.used_percentage??0;
414
+ let h = `<div class="card"><h3>Instance</h3>
415
+ <div class="detail-row"><span class="detail-label">Name</span><span>${esc(d.name)}</span></div>
416
+ <div class="detail-row"><span class="detail-label">Status</span><span>${esc(d.status)}</span></div>
417
+ <div class="detail-row"><span class="detail-label">Display</span><span>${esc(d.display_name||"--")}</span></div>
418
+ <div class="detail-row"><span class="detail-label">Description</span><span>${esc(d.description||"--")}</span></div>
419
+ <div class="detail-row"><span class="detail-label">Directory</span><span style="font-family:'IBM Plex Mono',monospace;font-size:12px">${esc(d.working_directory)}</span></div></div>
420
+ <div class="card"><h3>Runtime</h3>
421
+ <div class="detail-row"><span class="detail-label">Model</span><span>${esc(model)}</span></div>
422
+ <div class="detail-row"><span class="detail-label">Cost</span><span>$${cost.toFixed(2)} <span style="color:var(--text-muted);font-size:11px">(session accumulated)</span></span></div>
423
+ <div class="detail-row"><span class="detail-label">Context</span><span>${Math.round(ctx)}% <span class="progress-bar"><span class="progress-fill" style="width:${Math.min(ctx,100)}%;background:${ctx>80?'var(--warn)':'var(--accent)'}"></span></span></span></div>
424
+ <div class="detail-row"><span class="detail-label">Rate 5h</span><span>${Math.round(rl5)}% <span class="progress-bar"><span class="progress-fill" style="width:${Math.min(rl5,100)}%;background:${rl5>90?'var(--error)':rl5>70?'var(--warn)':'var(--accent)'}"></span></span></span></div>
425
+ <div class="detail-row"><span class="detail-label">Rate 7d</span><span>${Math.round(rl7)}% <span class="progress-bar"><span class="progress-fill" style="width:${Math.min(rl7,100)}%;background:${rl7>90?'var(--error)':rl7>70?'var(--warn)':'var(--accent)'}"></span></span></span></div></div>`;
426
+ if (d.recent_activity?.length) {
427
+ h += `<div class="card"><h3>Recent Activity</h3>`;
428
+ for (const a of d.recent_activity) { const t=a.timestamp?a.timestamp.slice(11,16):""; h+=`<div class="activity-item"><span class="act-time">${t}</span><span class="act-event">${esc(a.event)}</span>${esc(a.summary||"")}</div>`; }
429
+ h += `</div>`;
430
+ }
431
+ el.innerHTML = h;
432
+ } catch { el.innerHTML = `<div style="color:var(--error);padding:24px">Failed to load</div>`; }
433
+ }
434
+
435
+ async function loadTasks() {
436
+ const el = document.getElementById("taskView"); if (!el) return;
437
+ try {
438
+ const d = await api("GET", "/ui/tasks");
439
+ const tasks = d.tasks || [];
440
+ let h = `<div class="list-header"><span>${tasks.length} tasks</span>
441
+ <button class="btn btn-accent" onclick="showCreateTask()">+ New Task</button></div>`;
442
+ if (!tasks.length) h += `<div class="msg-empty"><svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>No tasks yet<br><span style="font-size:12px">Create one to coordinate fleet work</span></div>`;
443
+ else for (const t of tasks) {
444
+ h += `<div class="task-item">
445
+ <span class="task-status ${t.status}">${t.status}</span>
446
+ <span class="task-title">${esc(t.title)}</span>
447
+ <span class="task-assignee">${t.assignee?`<span style="cursor:pointer;color:var(--accent)" onclick="sel('${esc(t.assignee)}')">${esc(t.assignee)}</span>`:""}</span>
448
+ <div class="task-actions">
449
+ ${t.status==="open"?`<button class="btn btn-ghost" onclick="taskAct('${t.id}','claim')">Claim</button>`:""}
450
+ ${t.status==="claimed"?`<button class="btn btn-ghost" onclick="taskAct('${t.id}','complete')">Done</button>`:""}
451
+ </div></div>`;
452
+ }
453
+ el.innerHTML = h;
454
+ } catch { el.innerHTML = `<div style="color:var(--error);padding:24px">Failed to load</div>`; }
455
+ }
456
+ async function taskAct(id, action) {
457
+ const r = await api("POST", `/ui/tasks/${id}`, { action });
458
+ if (r.error) toast(r.error, false); else { toast(`Task ${action}ed`); loadTasks(); }
459
+ }
460
+
461
+ async function showCreateInstance() {
462
+ const ov = document.createElement("div"); ov.className = "form-overlay";
463
+ let backendOpts = '<option value="">Use fleet default</option>';
464
+ try {
465
+ const bd = await api("GET", "/ui/backends");
466
+ const first = (bd.backends || []).find(b => b.installed);
467
+ backendOpts += (bd.backends || []).map(b => {
468
+ const label = b.installed ? b.name : `${b.name} (not installed)`;
469
+ return `<option value="${b.name}" ${b.installed ? "" : "disabled"} ${b.name === first?.name ? "selected" : ""}>${label}</option>`;
470
+ }).join("");
471
+ } catch { backendOpts += ["claude-code","codex","gemini-cli","opencode","kiro-cli"].map(b => `<option value="${b}">${b}</option>`).join(""); }
472
+ ov.innerHTML = `<div class="form-card"><h2>Create Instance</h2>
473
+ <div class="form-field"><label>Directory</label><input id="ci-dir" placeholder="Optional — auto-creates workspace if empty"></div>
474
+ <div class="form-field"><label id="ci-topic-label">Topic Name</label><input id="ci-topic" placeholder="Auto from directory name"></div>
475
+ <div class="form-field"><label>Description</label><input id="ci-desc" placeholder="What this instance does"></div>
476
+ <div class="form-field"><label>Backend</label><select id="ci-backend">${backendOpts}</select></div>
477
+ <div class="form-field"><label>Model</label><input id="ci-model" placeholder="e.g. sonnet, opus, gemini-2.5-pro"></div>
478
+ <div class="form-field"><label>Branch (git worktree)</label><input id="ci-branch" placeholder="e.g. feature-x"></div>
479
+ <div class="form-field"><label>Tags</label><input id="ci-tags" placeholder="comma-separated, e.g. dev, review"></div>
480
+ <div class="form-field"><label>System Prompt</label><textarea id="ci-prompt" rows="3" placeholder="Custom instructions for this instance"></textarea></div>
481
+ <div class="form-actions"><button class="btn btn-ghost" onclick="this.closest('.form-overlay').remove()">Cancel</button>
482
+ <button class="btn btn-accent" id="ci-go">Create</button></div></div>`;
483
+ document.body.appendChild(ov);
484
+ ov.addEventListener("click", e => { if (e.target === ov) ov.remove(); });
485
+ ov.querySelector("#ci-dir").addEventListener("input", () => {
486
+ const hasDir = ov.querySelector("#ci-dir").value.trim();
487
+ ov.querySelector("#ci-topic-label").textContent = hasDir ? "Topic Name" : "Topic Name *";
488
+ });
489
+ ov.querySelector("#ci-go").addEventListener("click", async () => {
490
+ const dir = document.getElementById("ci-dir").value.trim();
491
+ const topic = document.getElementById("ci-topic").value.trim();
492
+ if (!dir && !topic) { toast("Topic Name is required when Directory is empty", false); return; }
493
+ const btn = ov.querySelector("#ci-go"); btn.disabled = true; btn.textContent = "Creating...";
494
+ const body = {};
495
+ if (dir) body.directory = dir;
496
+ const v = id => document.getElementById(id).value.trim();
497
+ if (v("ci-topic")) body.topic_name = v("ci-topic");
498
+ if (v("ci-desc")) body.description = v("ci-desc");
499
+ const backend = document.getElementById("ci-backend").value; if (backend) body.backend = backend;
500
+ if (v("ci-model")) body.model = v("ci-model");
501
+ if (v("ci-branch")) body.branch = v("ci-branch");
502
+ const tags = v("ci-tags"); if (tags) body.tags = tags.split(",").map(t => t.trim()).filter(Boolean);
503
+ if (v("ci-prompt")) body.systemPrompt = v("ci-prompt");
504
+ const r = await api("POST", "/ui/instances", body);
505
+ if (r.error) { toast(r.error, false); btn.disabled = false; btn.textContent = "Create"; }
506
+ else { toast("Instance created"); ov.remove(); }
507
+ });
508
+ }
509
+
510
+ function showCreateTask() {
511
+ const ov = document.createElement("div"); ov.className = "form-overlay";
512
+ const opts = instances.map(i => `<option value="${esc(i.name)}">${esc(i.name)}</option>`).join("");
513
+ ov.innerHTML = `<div class="form-card"><h2>Create Task</h2>
514
+ <div class="form-field"><label>Title *</label><input id="ct-title" placeholder="Task title"></div>
515
+ <div class="form-field"><label>Description</label><input id="ct-desc" placeholder="Details"></div>
516
+ <div class="form-field"><label>Priority</label><select id="ct-pri"><option value="normal">Normal</option><option value="low">Low</option><option value="high">High</option><option value="urgent">Urgent</option></select></div>
517
+ <div class="form-field"><label>Assignee</label><select id="ct-assign"><option value="">Unassigned</option>${opts}</select></div>
518
+ <div class="form-actions"><button class="btn btn-ghost" onclick="this.closest('.form-overlay').remove()">Cancel</button>
519
+ <button class="btn btn-accent" id="ct-go">Create</button></div></div>`;
520
+ document.body.appendChild(ov);
521
+ ov.addEventListener("click", e => { if (e.target === ov) ov.remove(); });
522
+ ov.querySelector("#ct-go").addEventListener("click", async () => {
523
+ const title = document.getElementById("ct-title").value.trim();
524
+ if (!title) { toast("Title required", false); return; }
525
+ const btn = ov.querySelector("#ct-go"); btn.disabled = true; btn.textContent = "Creating...";
526
+ const body = { title, priority: document.getElementById("ct-pri").value };
527
+ const desc = document.getElementById("ct-desc").value.trim(); if (desc) body.description = desc;
528
+ const asgn = document.getElementById("ct-assign").value; if (asgn) body.assignee = asgn;
529
+ const r = await api("POST", "/ui/tasks", body);
530
+ if (r.error) { toast(r.error, false); btn.disabled = false; btn.textContent = "Create"; }
531
+ else { toast("Task created"); ov.remove(); loadTasks(); }
532
+ });
533
+ }
534
+
535
+ // ── Schedules ────────────────────────────────────────────
536
+ async function loadSchedules() {
537
+ const el = document.getElementById("schedView"); if (!el) return;
538
+ try {
539
+ const d = await api("GET", "/ui/schedules");
540
+ const scheds = d.schedules || [];
541
+ let h = `<div class="list-header"><span>${scheds.length} schedules</span>
542
+ <button class="btn btn-accent" onclick="showCreateSchedule()">+ New Schedule</button></div>`;
543
+ if (!scheds.length) h += `<div class="msg-empty"><svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>No schedules yet<br><span style="font-size:12px">Automate recurring tasks with cron</span></div>`;
544
+ else for (const s of scheds) {
545
+ h += `<div class="task-item">
546
+ <span class="task-status ${s.enabled?"open":"done"}">${s.enabled?"on":"off"}</span>
547
+ <span class="task-title">${esc(s.label||cronDesc(s.cron))}</span>
548
+ <span class="task-assignee" style="font-family:'IBM Plex Mono',monospace" title="${esc(cronDesc(s.cron))}">${esc(s.cron)}</span>
549
+ <span class="task-assignee" style="cursor:pointer;color:var(--accent)" onclick="sel('${esc(s.target)}')">${esc(s.target)}</span>
550
+ <div class="task-actions">
551
+ <button class="btn btn-ghost btn-red" onclick="delSchedule('${s.id}')">Delete</button>
552
+ </div></div>`;
553
+ }
554
+ el.innerHTML = h;
555
+ } catch { el.innerHTML = `<div style="color:var(--error);padding:24px">Failed to load</div>`; }
556
+ }
557
+ async function delSchedule(id) {
558
+ if (!confirm("Delete this schedule?")) return;
559
+ const r = await api("DELETE", `/ui/schedules/${id}`);
560
+ if (r.error) toast(r.error, false); else { toast("Schedule deleted"); loadSchedules(); }
561
+ }
562
+ function showCreateSchedule() {
563
+ const ov = document.createElement("div"); ov.className = "form-overlay";
564
+ const tgtOpts = instances.map(i => `<option value="${esc(i.name)}">${esc(i.name)}</option>`).join("");
565
+ ov.innerHTML = `<div class="form-card"><h2>Create Schedule</h2>
566
+ <div class="form-field"><label>Cron Expression *</label><input id="cs-cron" placeholder="0 9 * * 1-5"></div>
567
+ <div class="form-field"><label>Message *</label><input id="cs-msg" placeholder="What to send"></div>
568
+ <div class="form-field"><label>Target Instance *</label><select id="cs-target">${tgtOpts}</select></div>
569
+ <div class="form-field"><label>Label</label><input id="cs-label" placeholder="Daily standup"></div>
570
+ <div class="form-actions"><button class="btn btn-ghost" onclick="this.closest('.form-overlay').remove()">Cancel</button>
571
+ <button class="btn btn-accent" id="cs-go">Create</button></div></div>`;
572
+ document.body.appendChild(ov);
573
+ ov.addEventListener("click", e => { if (e.target === ov) ov.remove(); });
574
+ ov.querySelector("#cs-go").addEventListener("click", async () => {
575
+ const cron = document.getElementById("cs-cron").value.trim();
576
+ const msg = document.getElementById("cs-msg").value.trim();
577
+ const target = document.getElementById("cs-target").value;
578
+ if (!cron || !msg || !target) { toast("Cron, message, and target required", false); return; }
579
+ const btn = ov.querySelector("#cs-go"); btn.disabled = true; btn.textContent = "Creating...";
580
+ const body = { cron, message: msg, target, source: "web-user", reply_chat_id: "", reply_thread_id: "" };
581
+ const label = document.getElementById("cs-label").value.trim(); if (label) body.label = label;
582
+ const r = await api("POST", "/ui/schedules", body);
583
+ if (r.error) { toast(r.error, false); btn.disabled = false; btn.textContent = "Create"; }
584
+ else { toast("Schedule created"); ov.remove(); loadSchedules(); }
585
+ });
586
+ }
587
+
588
+ // ── Teams ────────────────────────────────────────────────
589
+ async function loadTeams() {
590
+ const el = document.getElementById("teamView"); if (!el) return;
591
+ try {
592
+ const d = await api("GET", "/ui/teams");
593
+ const teams = d.teams || {};
594
+ const entries = Object.entries(teams);
595
+ let h = `<div class="list-header"><span>${entries.length} teams</span>
596
+ <button class="btn btn-accent" onclick="showCreateTeam()">+ New Team</button></div>`;
597
+ if (!entries.length) h += `<div class="msg-empty"><svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>No teams yet<br><span style="font-size:12px">Group instances for coordinated work</span></div>`;
598
+ else for (const [name, team] of entries) {
599
+ h += `<div class="task-item">
600
+ <span class="task-title" style="font-weight:600">${esc(name)}</span>
601
+ <span class="task-assignee">${esc((team.members||[]).join(", "))}</span>
602
+ <div class="task-actions">
603
+ <button class="btn btn-ghost btn-red" onclick="delTeam('${esc(name)}')">Delete</button>
604
+ </div></div>`;
605
+ }
606
+ el.innerHTML = h;
607
+ } catch { el.innerHTML = `<div style="color:var(--error);padding:24px">Failed to load</div>`; }
608
+ }
609
+ async function delTeam(name) {
610
+ if (!confirm(`Delete team "${name}"?`)) return;
611
+ const r = await api("DELETE", `/ui/teams/${encodeURIComponent(name)}`);
612
+ if (r.error) toast(r.error, false); else { toast("Team deleted"); loadTeams(); }
613
+ }
614
+ function showCreateTeam() {
615
+ const ov = document.createElement("div"); ov.className = "form-overlay";
616
+ const memOpts = instances.map(i => `<label style="display:flex;gap:6px;align-items:center;font-size:13px;padding:3px 0"><input type="checkbox" value="${esc(i.name)}">${esc(i.name)}</label>`).join("");
617
+ ov.innerHTML = `<div class="form-card"><h2>Create Team</h2>
618
+ <div class="form-field"><label>Team Name *</label><input id="ct2-name" placeholder="team-name"></div>
619
+ <div class="form-field"><label>Description</label><input id="ct2-desc" placeholder="What this team does"></div>
620
+ <div class="form-field"><label>Members *</label><div id="ct2-members" style="max-height:150px;overflow-y:auto">${memOpts}</div></div>
621
+ <div class="form-actions"><button class="btn btn-ghost" onclick="this.closest('.form-overlay').remove()">Cancel</button>
622
+ <button class="btn btn-accent" id="ct2-go">Create</button></div></div>`;
623
+ document.body.appendChild(ov);
624
+ ov.addEventListener("click", e => { if (e.target === ov) ov.remove(); });
625
+ ov.querySelector("#ct2-go").addEventListener("click", async () => {
626
+ const name = document.getElementById("ct2-name").value.trim();
627
+ const members = [...document.querySelectorAll("#ct2-members input:checked")].map(c => c.value);
628
+ if (!name || !members.length) { toast("Name and at least one member required", false); return; }
629
+ const btn = ov.querySelector("#ct2-go"); btn.disabled = true; btn.textContent = "Creating...";
630
+ const body = { name, members };
631
+ const desc = document.getElementById("ct2-desc").value.trim(); if (desc) body.description = desc;
632
+ const r = await api("POST", "/ui/teams", body);
633
+ if (r.error) { toast(r.error, false); btn.disabled = false; btn.textContent = "Create"; }
634
+ else { toast("Team created"); ov.remove(); loadTeams(); }
635
+ });
636
+ }
637
+
638
+ // ── Cron description ─────────────────────────────────────
639
+ const DAYS = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
640
+ function cronDesc(expr) {
641
+ if (!expr) return "";
642
+ const p = expr.trim().split(/\s+/);
643
+ if (p.length < 5) return expr;
644
+ const [min, hr, dom, mon, dow] = p;
645
+ const time = (hr !== "*" && min !== "*") ? `at ${hr.padStart(2,"0")}:${min.padStart(2,"0")}` : "";
646
+ if (min === "*" && hr === "*") return "Every minute";
647
+ if (min.startsWith("*/")) return `Every ${min.slice(2)} minutes`;
648
+ if (hr.startsWith("*/")) return `Every ${hr.slice(2)} hours`;
649
+ if (dom === "*" && mon === "*" && dow === "*") return `Daily ${time}`;
650
+ if (dom === "*" && mon === "*" && dow === "1-5") return `Weekdays ${time}`;
651
+ if (dom === "*" && mon === "*" && dow !== "*") {
652
+ const days = dow.split(",").map(d => { const n = parseInt(d); return isNaN(n) ? d : (DAYS[n] || d); });
653
+ return `${days.join(", ")} ${time}`;
654
+ }
655
+ if (dom !== "*" && mon === "*") return `${dom}${dom==="1"?"st":dom==="2"?"nd":dom==="3"?"rd":"th"} of month ${time}`;
656
+ return expr;
657
+ }
658
+
659
+ // ── Config form ──────────────────────────────────────────
660
+ let cfgData = null;
661
+ async function loadConfig() {
662
+ const el = document.getElementById("configView"); if (!el) return;
663
+ try {
664
+ cfgData = await api("GET", "/ui/config");
665
+ const ch = cfgData.channel || {};
666
+ const acc = ch.access || {};
667
+ const defs = cfgData.defaults || {};
668
+ const roots = cfgData.project_roots || [];
669
+ el.innerHTML = `
670
+ <div class="card"><h3>Channel</h3>
671
+ <div class="form-field"><label>Type</label><input id="cfg-type" value="${esc(ch.type||"")}" disabled style="opacity:0.5"></div>
672
+ <div class="form-field"><label>Bot Token Env</label><input id="cfg-token-env" value="${esc(ch.bot_token_env||"AGEND_BOT_TOKEN")}" disabled style="opacity:0.5"></div>
673
+ <div class="form-field"><label>Group ID</label>
674
+ <div style="display:flex;gap:4px;align-items:center">
675
+ <input id="cfg-gid" type="password" value="${ch.group_id||""}" style="flex:1">
676
+ <button class="eye-btn" onclick="togglePw('cfg-gid')">&#128065;</button>
677
+ </div></div>
678
+ </div>
679
+ <div class="card"><h3>Access Control</h3>
680
+ <div class="form-field"><label>Mode</label>
681
+ <select id="cfg-acc-mode"><option value="locked" ${acc.mode==="locked"?"selected":""}>Locked</option><option value="open" ${acc.mode==="open"?"selected":""}>Open</option></select></div>
682
+ <div class="form-field"><label>Allowed Users</label>
683
+ <div style="display:flex;gap:4px;align-items:center">
684
+ <input id="cfg-users" type="password" value="${(acc.allowed_users||[]).join(", ")}" style="flex:1">
685
+ <button class="eye-btn" onclick="togglePw('cfg-users')">&#128065;</button>
686
+ </div>
687
+ <div style="font-size:11px;color:var(--text-muted);margin-top:4px">Comma-separated user IDs</div></div>
688
+ </div>
689
+ <div class="card"><h3>Defaults</h3>
690
+ <div class="form-field"><label>Backend</label>
691
+ <select id="cfg-backend">
692
+ ${["claude-code","codex","gemini-cli","opencode","kiro-cli"].map(b => `<option value="${b}" ${defs.backend===b?"selected":""}>${b}</option>`).join("")}
693
+ </select></div>
694
+ </div>
695
+ <div class="card"><h3>Project Roots</h3>
696
+ <div id="cfg-roots">${roots.map((r,i) => `<div style="display:flex;gap:4px;margin-bottom:6px"><input class="cfg-root" value="${esc(r)}" style="flex:1"><button class="btn btn-ghost" style="padding:4px 8px" onclick="this.parentElement.remove()">&#10005;</button></div>`).join("")}</div>
697
+ <button class="btn btn-ghost" onclick="addRoot()">+ Add Root</button>
698
+ </div>
699
+ <div style="display:flex;gap:10px;align-items:center;margin-top:16px">
700
+ <button class="btn btn-accent" onclick="saveConfig()">Save Changes</button>
701
+ <span style="font-size:11px;color:var(--text-muted)">Saving will reformat fleet.yaml (comments will be lost)</span>
702
+ </div>`;
703
+ } catch { el.innerHTML = `<div style="color:var(--error);padding:24px">Failed to load config</div>`; }
704
+ }
705
+ function togglePw(id) { const el = document.getElementById(id); el.type = el.type === "password" ? "text" : "password"; }
706
+ function addRoot() { const el = document.getElementById("cfg-roots"); el.insertAdjacentHTML("beforeend", `<div style="display:flex;gap:4px;margin-bottom:6px"><input class="cfg-root" value="" placeholder="/path/to/projects" style="flex:1"><button class="btn btn-ghost" style="padding:4px 8px" onclick="this.parentElement.remove()">&#10005;</button></div>`); }
707
+ async function saveConfig() {
708
+ const body = {
709
+ channel: { group_id: document.getElementById("cfg-gid").value.trim(), access: { mode: document.getElementById("cfg-acc-mode").value, allowed_users: document.getElementById("cfg-users").value.split(",").map(s => s.trim()).filter(Boolean) } },
710
+ defaults: { backend: document.getElementById("cfg-backend").value },
711
+ project_roots: [...document.querySelectorAll(".cfg-root")].map(i => i.value.trim()).filter(Boolean),
712
+ };
713
+ const r = await api("POST", "/ui/config", body);
714
+ if (r.error) toast(r.error, false);
715
+ else { toast("Config saved" + (r.needs_restart ? " (restart fleet to apply channel changes)" : "")); }
716
+ }
717
+ </script>
718
+ </body>
719
+ </html>