agentainer 0.1.7 → 2.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 (98) hide show
  1. package/README.md +248 -677
  2. package/agentainer +16 -18
  3. package/agentainer.example.yaml +86 -0
  4. package/bin/agentainer.js +9 -8
  5. package/examples/academic-coauthor.yaml +123 -0
  6. package/examples/accessibility-audit.yaml +152 -0
  7. package/examples/affiliate-product-reviews.yaml +106 -0
  8. package/examples/api-design.yaml +157 -0
  9. package/examples/app-store-optimization.yaml +108 -0
  10. package/examples/brainstorm.yaml +27 -128
  11. package/examples/brand-voice-style-guide.yaml +109 -0
  12. package/examples/bug-hunt.yaml +51 -96
  13. package/examples/candidate-screen.yaml +122 -0
  14. package/examples/case-study-writer.yaml +100 -0
  15. package/examples/changelog-release-notes.yaml +114 -0
  16. package/examples/chatbot-builder.yaml +138 -0
  17. package/examples/code-review.yaml +73 -0
  18. package/examples/comparison-guide-writer.yaml +106 -0
  19. package/examples/competitive-intel.yaml +126 -0
  20. package/examples/content-studio.yaml +91 -0
  21. package/examples/course-creator.yaml +133 -0
  22. package/examples/customer-support-triage.yaml +118 -0
  23. package/examples/daily-briefing.yaml +119 -0
  24. package/examples/data-pipeline-builder.yaml +135 -0
  25. package/examples/debate.yaml +16 -90
  26. package/examples/design-system.yaml +138 -0
  27. package/examples/ebook-generator.yaml +90 -0
  28. package/examples/ecommerce-listing-optimizer.yaml +126 -0
  29. package/examples/email-newsletter.yaml +103 -0
  30. package/examples/faq-knowledge-sync.yaml +107 -0
  31. package/examples/game-design.yaml +122 -0
  32. package/examples/glossary-term-writer.yaml +103 -0
  33. package/examples/incident-response.yaml +52 -109
  34. package/examples/knowledge-base.yaml +115 -0
  35. package/examples/landing-page-converter.yaml +103 -0
  36. package/examples/legal-contract-review.yaml +118 -0
  37. package/examples/linkedin-ghostwriter.yaml +93 -0
  38. package/examples/localization.yaml +56 -123
  39. package/examples/meeting-notes.yaml +111 -0
  40. package/examples/migration-planner.yaml +127 -0
  41. package/examples/onboarding-buddy.yaml +111 -0
  42. package/examples/performance-audit.yaml +123 -0
  43. package/examples/podcast-production.yaml +117 -0
  44. package/examples/postmortem.yaml +119 -0
  45. package/examples/pr-review-gate.yaml +123 -0
  46. package/examples/press-release-wire.yaml +96 -0
  47. package/examples/product-spec.yaml +107 -0
  48. package/examples/prompt-engineering-lab.yaml +109 -0
  49. package/examples/quickstart.yaml +48 -0
  50. package/examples/rag-builder.yaml +145 -0
  51. package/examples/refactor-planner.yaml +127 -0
  52. package/examples/research.yaml +25 -0
  53. package/examples/resume-tailor.yaml +116 -0
  54. package/examples/rfp-response.yaml +124 -0
  55. package/examples/sales-coach.yaml +123 -0
  56. package/examples/security-audit.yaml +120 -0
  57. package/examples/seo-audit-and-fix.yaml +138 -0
  58. package/examples/seo-content-factory.yaml +103 -0
  59. package/examples/social-media.yaml +103 -0
  60. package/examples/software-company.yaml +71 -128
  61. package/examples/startup-validator.yaml +115 -0
  62. package/examples/tdd-pingpong.yaml +36 -68
  63. package/examples/technical-documentation.yaml +112 -0
  64. package/examples/test-factory.yaml +114 -0
  65. package/examples/tutorial-howto-creator.yaml +111 -0
  66. package/examples/twitter-x-thread-factory.yaml +91 -0
  67. package/examples/white-paper-research.yaml +96 -0
  68. package/examples/writers-room.yaml +49 -111
  69. package/examples/youtube-script-studio.yaml +107 -0
  70. package/hooks/claude_stop.sh +5 -3
  71. package/hooks/codex_notify.sh +4 -3
  72. package/lib/cli.py +933 -0
  73. package/lib/config.py +267 -308
  74. package/lib/hooks.py +246 -0
  75. package/lib/lock.py +75 -0
  76. package/lib/log.py +64 -0
  77. package/lib/mail.py +699 -0
  78. package/lib/minyaml.py +1 -39
  79. package/lib/reconcile.py +544 -0
  80. package/lib/sessions.py +223 -0
  81. package/lib/supervisor.py +216 -0
  82. package/lib/telegram.py +372 -0
  83. package/lib/tmux.py +355 -0
  84. package/lib/turn.py +167 -0
  85. package/lib/ui.py +1219 -0
  86. package/llms.txt +145 -429
  87. package/package.json +9 -7
  88. package/scripts/check-deps.js +18 -61
  89. package/ui/app.js +1136 -0
  90. package/ui/index.html +404 -0
  91. package/agents.example.yaml +0 -257
  92. package/examples/code-review-broadcast.yaml +0 -109
  93. package/examples/existing-repo.yaml +0 -74
  94. package/examples/multi-language-broadcast.yaml +0 -127
  95. package/examples/ping-pong.yaml +0 -89
  96. package/examples/red-team.yaml +0 -117
  97. package/examples/research-swarm.yaml +0 -129
  98. package/lib/swarm.py +0 -2461
package/ui/index.html ADDED
@@ -0,0 +1,404 @@
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, viewport-fit=cover" />
6
+ <meta name="color-scheme" content="light dark" />
7
+ <title>Agentainer</title>
8
+ <style>
9
+ /* ---- design tokens (light + dark) ------------------------------------ */
10
+ :root {
11
+ color-scheme: light dark;
12
+ --bg: #f4f6f9;
13
+ --surface: #ffffff;
14
+ --surface-2: #eef1f5;
15
+ --border: #e2e6ec;
16
+ --text: #171a1f;
17
+ --muted: #667085;
18
+ --accent: #2563eb;
19
+ --accent-fg: #ffffff;
20
+ --accent-soft: #e6efff;
21
+ --ok-bg: #e3f6e9; --ok-fg: #157a3c;
22
+ --busy-bg: #fdf0d5; --busy-fg: #9a6b00;
23
+ --no-bg: #fde2e1; --no-fg: #b4231d;
24
+ --shadow: 0 1px 2px rgba(16,24,40,.06), 0 1px 3px rgba(16,24,40,.10);
25
+ --shadow-lg: 0 8px 28px rgba(16,24,40,.18);
26
+ --radius: 14px;
27
+ --radius-sm: 10px;
28
+ }
29
+ @media (prefers-color-scheme: dark) {
30
+ :root {
31
+ --bg: #0d0f13;
32
+ --surface: #161a21;
33
+ --surface-2: #1e232c;
34
+ --border: #262c37;
35
+ --text: #e7eaee;
36
+ --muted: #8b93a1;
37
+ --accent: #4f8cff;
38
+ --accent-fg: #0b1220;
39
+ --accent-soft: #17233b;
40
+ --ok-bg: #16351f; --ok-fg: #7ee29a;
41
+ --busy-bg: #3a3416; --busy-fg: #ffd97a;
42
+ --no-bg: #3a1d1d; --no-fg: #ff9b9b;
43
+ --shadow: 0 1px 2px rgba(0,0,0,.4);
44
+ --shadow-lg: 0 12px 40px rgba(0,0,0,.55);
45
+ }
46
+ }
47
+
48
+ * { box-sizing: border-box; }
49
+ /* The `hidden` attribute must win over class-level `display` rules (e.g. the
50
+ login card's `.center { display: grid }`), or hiding it visually no-ops. */
51
+ [hidden] { display: none !important; }
52
+ html, body { height: 100%; }
53
+ body {
54
+ margin: 0;
55
+ font: 15px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
56
+ background: var(--bg);
57
+ color: var(--text);
58
+ -webkit-font-smoothing: antialiased;
59
+ overflow-x: hidden;
60
+ }
61
+ button, input, textarea, select { font: inherit; color: inherit; }
62
+ button { cursor: pointer; }
63
+ a { color: var(--accent); }
64
+
65
+ /* ---- app bar --------------------------------------------------------- */
66
+ header#bar {
67
+ position: sticky; top: 0; z-index: 20;
68
+ display: flex; align-items: center; gap: .75rem;
69
+ padding: .6rem max(.9rem, env(safe-area-inset-left)) .6rem max(.9rem, env(safe-area-inset-left));
70
+ background: color-mix(in srgb, var(--surface) 88%, transparent);
71
+ backdrop-filter: saturate(1.4) blur(10px);
72
+ border-bottom: 1px solid var(--border);
73
+ }
74
+ .brand { display: flex; align-items: center; gap: .55rem; font-weight: 700; letter-spacing: -.01em; }
75
+ .brand .dot {
76
+ width: 26px; height: 26px; border-radius: 8px; flex: 0 0 auto;
77
+ background: linear-gradient(135deg, var(--accent), #7c3aed);
78
+ display: grid; place-items: center; color: #fff; font-size: 15px;
79
+ box-shadow: var(--shadow);
80
+ }
81
+ .brand small { display: block; font-weight: 500; color: var(--muted); font-size: .72rem; margin-top: -2px; }
82
+ header#bar .spacer { flex: 1 1 auto; }
83
+ .navbtn {
84
+ background: transparent; border: 1px solid transparent; color: var(--muted);
85
+ padding: .4rem .7rem; border-radius: 999px; font-weight: 600; font-size: .9rem;
86
+ }
87
+ .navbtn.active { color: var(--text); background: var(--surface-2); border-color: var(--border); }
88
+ .navbtn:hover { color: var(--text); }
89
+ .navbtn { position: relative; }
90
+
91
+ /* availability switch */
92
+ .avail { display: inline-flex; align-items: center; gap: .5rem; }
93
+ .avail .lbl { font-size: .82rem; color: var(--muted); white-space: nowrap; }
94
+ .switch { position: relative; width: 42px; height: 24px; flex: 0 0 auto; }
95
+ .switch input { opacity: 0; width: 0; height: 0; position: absolute; }
96
+ .switch .track {
97
+ position: absolute; inset: 0; border-radius: 999px; background: var(--surface-2);
98
+ border: 1px solid var(--border); transition: background .18s;
99
+ }
100
+ .switch .knob {
101
+ position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; border-radius: 50%;
102
+ background: #fff; box-shadow: var(--shadow); transition: transform .18s;
103
+ }
104
+ .switch input:checked + .track { background: var(--ok-fg); border-color: var(--ok-fg); }
105
+ .switch input:checked + .track + .knob { transform: translateX(18px); }
106
+
107
+ /* ---- layout ---------------------------------------------------------- */
108
+ main { max-width: 1080px; margin: 0 auto; padding: 1rem max(.9rem, env(safe-area-inset-left)) 3rem; }
109
+ .muted { color: var(--muted); }
110
+ .banner { color: var(--no-fg); min-height: 1.1rem; margin: .2rem 0; font-size: .9rem; }
111
+ .center { min-height: 60vh; display: grid; place-items: center; }
112
+
113
+ .card {
114
+ background: var(--surface); border: 1px solid var(--border);
115
+ border-radius: var(--radius); box-shadow: var(--shadow);
116
+ }
117
+
118
+ /* login */
119
+ #login .card { padding: 1.5rem; width: min(420px, 92vw); }
120
+ #login h2 { margin: .2rem 0 .3rem; }
121
+ input.field, textarea.field, select.field {
122
+ width: 100%; background: var(--surface-2); border: 1px solid var(--border);
123
+ border-radius: var(--radius-sm); padding: .6rem .7rem;
124
+ }
125
+ input.field:focus, textarea.field:focus, select.field:focus {
126
+ outline: 2px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 0;
127
+ border-color: var(--accent);
128
+ }
129
+ .btn {
130
+ background: var(--accent); color: var(--accent-fg); border: 1px solid transparent;
131
+ padding: .55rem .9rem; border-radius: var(--radius-sm); font-weight: 650;
132
+ box-shadow: var(--shadow);
133
+ }
134
+ .btn:hover { filter: brightness(1.06); }
135
+ .btn.ghost { background: var(--surface-2); color: var(--text); border-color: var(--border); box-shadow: none; }
136
+ .btn.danger { background: var(--no-bg); color: var(--no-fg); box-shadow: none; }
137
+ .btn.sm { padding: .35rem .6rem; font-size: .85rem; }
138
+
139
+ /* pills */
140
+ .pill { display: inline-flex; align-items: center; gap: .3rem; padding: .12rem .5rem; border-radius: 999px; font-size: .74rem; font-weight: 600; }
141
+ .pill.ok { background: var(--ok-bg); color: var(--ok-fg); }
142
+ .pill.busy { background: var(--busy-bg); color: var(--busy-fg); }
143
+ .pill.no { background: var(--no-bg); color: var(--no-fg); }
144
+ .pill.mute { background: var(--surface-2); color: var(--muted); }
145
+ /* truthful state pills */
146
+ .pill.st-working { background: var(--ok-bg); color: var(--ok-fg); }
147
+ .pill.st-waiting { background: var(--surface-2); color: var(--muted); }
148
+ .pill.st-attention { background: var(--busy-bg); color: var(--busy-fg); }
149
+ .pill.st-stalled { background: var(--no-bg); color: var(--no-fg); }
150
+ .pill.st-stopped { background: var(--surface-2); color: var(--muted); opacity: .7; }
151
+ .dotpulse { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
152
+ .pill.st-working .dotpulse { animation: dotpulse 1.2s ease-in-out infinite; }
153
+ @keyframes dotpulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: .3; transform: scale(.6); } }
154
+ @media (prefers-reduced-motion: reduce) { .pill.st-working .dotpulse { animation: none; } }
155
+ .pill.upbtn { border: 1px solid var(--accent); background: transparent; color: var(--accent); cursor: pointer; font-family: inherit; }
156
+ .pill.upbtn:hover:not(:disabled) { background: var(--accent); color: #fff; }
157
+ .pill.upbtn:disabled, .pill.downbtn:disabled { opacity: .6; cursor: default; }
158
+ .pill.downbtn { border: 1px solid var(--no-fg); background: transparent; color: var(--no-fg); cursor: pointer; font-family: inherit; }
159
+ .pill.downbtn:hover:not(:disabled) { background: var(--no-fg); color: #fff; }
160
+ /* stalled-recovery quick actions (Esc / Restart) on a card */
161
+ .pill.recover { border: 1px solid var(--border); background: var(--surface-2); color: var(--text); cursor: pointer; font-family: inherit; }
162
+ .pill.recover:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
163
+ .pill.recover:disabled { opacity: .6; cursor: default; }
164
+
165
+ /* connection-honesty indicator in the app bar */
166
+ .connind {
167
+ display: inline-flex; align-items: center; gap: .35rem; white-space: nowrap;
168
+ font-size: .76rem; font-weight: 650; padding: .2rem .55rem; border-radius: 999px;
169
+ border: 1px solid var(--border); background: var(--surface-2);
170
+ font-variant-numeric: tabular-nums;
171
+ }
172
+ .connind.live { color: var(--ok-fg); border-color: color-mix(in srgb, var(--ok-fg) 40%, var(--border)); }
173
+ .connind.stale { color: var(--busy-fg); border-color: color-mix(in srgb, var(--busy-fg) 45%, var(--border)); }
174
+
175
+ /* avatar */
176
+ .avatar {
177
+ width: 40px; height: 40px; border-radius: 12px; flex: 0 0 auto; display: grid; place-items: center;
178
+ font-weight: 700; color: #fff; font-size: .95rem;
179
+ }
180
+ .avatar.sm { width: 32px; height: 32px; border-radius: 10px; font-size: .82rem; }
181
+
182
+ /* ---- agents overview grid ------------------------------------------- */
183
+ .grid { display: grid; gap: .9rem; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
184
+ .agentcard { padding: 1rem; display: flex; flex-direction: column; gap: .7rem; cursor: pointer; transition: transform .12s, box-shadow .12s, border-color .12s; }
185
+ .agentcard:hover { transform: translateY(-2px); box-shadow: var(--shadow-lg); border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); }
186
+ .agentcard .top { display: flex; align-items: center; gap: .7rem; }
187
+ .agentcard .name { font-weight: 700; }
188
+ .agentcard .role { color: var(--muted); font-size: .86rem; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; min-height: 2.6em; }
189
+ .agentcard .meta { display: flex; flex-wrap: wrap; gap: .4rem; }
190
+ .sectiontitle { display: flex; align-items: center; justify-content: space-between; gap: .6rem; margin: .3rem 0 .8rem; flex-wrap: wrap; }
191
+ .sectiontitle h2 { margin: 0; font-size: 1.05rem; }
192
+ .agents-tools { display: flex; align-items: center; gap: .8rem; flex-wrap: wrap; }
193
+ .tgl { display: inline-flex; align-items: center; gap: .3rem; font-size: .82rem; color: var(--muted); cursor: pointer; white-space: nowrap; }
194
+ .tgl input { margin: 0; }
195
+ .rateline { margin-left: auto; font-size: .74rem; color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
196
+
197
+ /* onboarding: template picker (empty state) */
198
+ .tpl-intro { margin: .2rem 0 .7rem; }
199
+ .tplgrid { display: grid; gap: .8rem; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }
200
+ .tpl { text-align: left; padding: .9rem; display: flex; flex-direction: column; gap: .3rem; cursor: pointer; transition: transform .12s, box-shadow .12s, border-color .12s; }
201
+ .tpl:hover:not(:disabled) { transform: translateY(-2px); box-shadow: var(--shadow-lg); border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); }
202
+ .tpl:disabled { opacity: .6; cursor: default; }
203
+ .tpl-title { font-weight: 700; }
204
+ .tpl-sum { font-size: .84rem; }
205
+ .tpl-meta { font-size: .74rem; }
206
+
207
+ /* ---- mail app -------------------------------------------------------- */
208
+ .mailhead { display: flex; align-items: center; gap: .6rem; margin-bottom: .8rem; flex-wrap: wrap; }
209
+ .mailhead .who { display: flex; align-items: center; gap: .6rem; }
210
+ .mailhead h2 { margin: 0; font-size: 1.05rem; }
211
+ .mail { display: grid; grid-template-columns: 300px 1fr; gap: .9rem; align-items: start; }
212
+ .contacts { padding: .4rem; overflow: hidden; }
213
+ .contact {
214
+ display: flex; gap: .6rem; align-items: center; padding: .55rem .55rem; border-radius: var(--radius-sm);
215
+ cursor: pointer; border: 1px solid transparent;
216
+ }
217
+ .contact:hover { background: var(--surface-2); }
218
+ .contact.active { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 35%, var(--border)); }
219
+ .contact .info { min-width: 0; flex: 1 1 auto; }
220
+ .contact .cn { display: flex; justify-content: space-between; gap: .4rem; align-items: baseline; }
221
+ .contact .cn b { font-weight: 650; }
222
+ .contact .cn .t { color: var(--muted); font-size: .72rem; white-space: nowrap; }
223
+ .contact .prev { color: var(--muted); font-size: .82rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
224
+ .badge { background: var(--accent); color: var(--accent-fg); border-radius: 999px; font-size: .7rem; font-weight: 700; padding: .05rem .42rem; }
225
+
226
+ .thread { display: flex; flex-direction: column; min-height: 60vh; }
227
+ .thread .scroll { padding: 1rem; display: flex; flex-direction: column; gap: .7rem; overflow-y: auto; flex: 1 1 auto; }
228
+ .msg { max-width: 78%; padding: .55rem .75rem; border-radius: 14px; box-shadow: var(--shadow); word-wrap: break-word; overflow-wrap: anywhere; }
229
+ .msg .m-head { font-size: .68rem; color: var(--muted); margin-bottom: .2rem; display: flex; gap: .5rem; }
230
+ /* rendered markdown inside a bubble */
231
+ .msg .m-body > :first-child { margin-top: 0; }
232
+ .msg .m-body > :last-child { margin-bottom: 0; }
233
+ .msg p { margin: 0 0 .45rem; }
234
+ .msg .md-h { margin: .4rem 0 .3rem; font-size: 1rem; line-height: 1.3; }
235
+ .msg ul, .msg ol { margin: .2rem 0 .45rem; padding-left: 1.25rem; }
236
+ .msg li { margin: .1rem 0; }
237
+ .msg code { background: rgba(127,127,127,.18); padding: .05rem .3rem; border-radius: 5px; font: .85em ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
238
+ .msg pre.md-pre { background: rgba(127,127,127,.16); padding: .5rem .6rem; border-radius: 8px; overflow-x: auto; margin: .35rem 0; }
239
+ .msg pre.md-pre code { background: none; padding: 0; }
240
+ .msg blockquote { margin: .3rem 0; padding-left: .6rem; border-left: 3px solid rgba(127,127,127,.4); opacity: .9; }
241
+ .msg a { color: inherit; text-decoration: underline; }
242
+ .msg.out code, .msg.out pre.md-pre { background: rgba(255,255,255,.2); }
243
+ .m-more { display: block; margin-top: .3rem; padding: 0; background: none; border: none; cursor: pointer; font: inherit; font-size: .72rem; color: var(--accent); text-decoration: underline; opacity: .85; }
244
+ .m-more:hover { opacity: 1; }
245
+ .msg.out .m-more { color: color-mix(in srgb, var(--accent-fg) 85%, transparent); }
246
+ .m-status { font-size: .66rem; margin-top: .25rem; text-align: right; color: var(--muted); letter-spacing: .01em; }
247
+ .msg.out .m-status { color: color-mix(in srgb, var(--accent-fg) 78%, transparent); }
248
+ .m-status.s-read { color: var(--ok-fg); }
249
+ .msg.out .m-status.s-read { color: color-mix(in srgb, var(--accent-fg) 92%, var(--ok-fg)); }
250
+ .msg.in { align-self: flex-start; background: var(--surface-2); border-bottom-left-radius: 5px; }
251
+ .msg.out { align-self: flex-end; background: var(--accent); color: var(--accent-fg); border-bottom-right-radius: 5px; }
252
+ .msg.out .m-head { color: color-mix(in srgb, var(--accent-fg) 75%, transparent); }
253
+ .msg.system { align-self: center; background: transparent; border: 1px dashed var(--border); color: var(--muted); font-size: .85rem; box-shadow: none; max-width: 90%; }
254
+ .empty { color: var(--muted); text-align: center; padding: 2rem 1rem; }
255
+
256
+ .compose { border-top: 1px solid var(--border); padding: .7rem; display: flex; gap: .5rem; align-items: flex-end; }
257
+ .compose textarea { min-height: 44px; max-height: 40vh; resize: vertical; }
258
+ .composenote { padding: .7rem; border-top: 1px solid var(--border); color: var(--muted); font-size: .85rem; }
259
+
260
+ /* terminal panel */
261
+ .terminal { margin-top: .9rem; padding: .8rem; }
262
+ .terminal .thead { display: flex; align-items: center; justify-content: space-between; margin-bottom: .5rem; gap: .5rem; }
263
+ pre.pane {
264
+ background: #0b0d11; color: #cdd6e4; border: 1px solid var(--border); border-radius: var(--radius-sm);
265
+ padding: .7rem; max-height: 42vh; overflow: auto; white-space: pre; font: 12.5px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
266
+ }
267
+ pre.pane.wrap { white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; }
268
+ .terminal .thead .wrapchk { margin-right: auto; display: flex; align-items: center; gap: .35rem; font-size: .8rem; color: var(--muted); cursor: pointer; white-space: nowrap; }
269
+ .terminal .thead .wrapchk input { margin: 0; }
270
+ .keyrow { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .5rem; }
271
+ .keybtn {
272
+ font: 600 .78rem/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
273
+ padding: .34rem .55rem; border: 1px solid var(--border); border-radius: var(--radius-sm);
274
+ background: var(--surface-2); color: var(--text); cursor: pointer; min-width: 2.1rem;
275
+ }
276
+ .keybtn:hover { background: var(--surface); border-color: var(--accent); }
277
+ .keybtn:active { transform: translateY(1px); }
278
+ .typerow { display: flex; gap: .5rem; margin-top: .5rem; }
279
+
280
+ /* ---- settings -------------------------------------------------------- */
281
+ .settings { display: flex; flex-direction: column; gap: 1rem; }
282
+ .formgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: .8rem; }
283
+ .fld { display: flex; flex-direction: column; gap: .3rem; }
284
+ .fld label { font-size: .8rem; color: var(--muted); font-weight: 600; }
285
+ .panel { padding: 1rem; }
286
+ .panel h3 { margin: 0 0 .2rem; }
287
+ .agentrow { display: flex; align-items: center; gap: .7rem; padding: .6rem .2rem; border-bottom: 1px solid var(--border); }
288
+ .agentrow:last-child { border-bottom: none; }
289
+ .agentrow .info { flex: 1 1 auto; min-width: 0; }
290
+ .row { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; }
291
+ .rowend { display: flex; gap: .5rem; justify-content: flex-end; margin-top: .6rem; }
292
+
293
+ .toast {
294
+ position: fixed; left: 50%; bottom: 1.3rem; transform: translateX(-50%);
295
+ background: var(--text); color: var(--bg); padding: .55rem .9rem; border-radius: 999px;
296
+ box-shadow: var(--shadow-lg); font-size: .88rem; z-index: 50; opacity: 0; transition: opacity .2s, transform .2s;
297
+ pointer-events: none; max-width: 90vw;
298
+ }
299
+ .toast.show { opacity: 1; transform: translateX(-50%) translateY(-4px); }
300
+
301
+ /* modal */
302
+ .modal-back { position: fixed; inset: 0; background: rgba(6,8,12,.55); display: grid; place-items: center; z-index: 40; padding: 1rem; }
303
+ .modal { width: min(560px, 96vw); max-height: 90vh; overflow: auto; padding: 1.2rem; }
304
+ .modal h3 { margin-top: 0; }
305
+
306
+ /* segmented tabs (Mail / Terminal) */
307
+ .tabs { display: inline-flex; background: var(--surface-2); border: 1px solid var(--border); border-radius: 999px; padding: 2px; }
308
+ .tab { border: none; background: transparent; color: var(--muted); padding: .3rem .8rem; border-radius: 999px; font-weight: 600; font-size: .88rem; }
309
+ .tab.active { background: var(--surface); color: var(--text); box-shadow: var(--shadow); }
310
+
311
+ /* activity timeline */
312
+ .timeline { display: flex; flex-direction: column; }
313
+ .event { display: flex; align-items: center; gap: .5rem; padding: .4rem .6rem; border-bottom: 1px solid var(--border); font-size: .88rem; flex-wrap: wrap; }
314
+ .event:last-child { border-bottom: none; }
315
+ .event .t { color: var(--muted); font-size: .74rem; min-width: 3.2rem; font-variant-numeric: tabular-nums; }
316
+
317
+ /* topology graph */
318
+ .edge { stroke: currentColor; stroke-width: 1.6; opacity: .55; }
319
+ /* live flow: an edge into a node that currently holds unread mail */
320
+ .edge-live { stroke: var(--accent); opacity: .95; stroke-width: 2.4; stroke-dasharray: 5 4; animation: edgeflow .6s linear infinite; }
321
+ @keyframes edgeflow { to { stroke-dashoffset: -9; } }
322
+ /* status rings, colored by the agent's live state */
323
+ .gring { stroke-width: 2.5; }
324
+ .gring-working { stroke: var(--ok-fg); animation: ringpulse 1.4s ease-in-out infinite; }
325
+ .gring-attention { stroke: var(--busy-fg); }
326
+ .gring-stalled { stroke: var(--no-fg); }
327
+ .gring-stopped { stroke: var(--muted); }
328
+ @keyframes ringpulse { 0%, 100% { opacity: .9; } 50% { opacity: .3; } }
329
+ @media (prefers-reduced-motion: reduce) { .edge-live, .gring-working { animation: none; } }
330
+ .gnode-t { fill: #fff; font-weight: 700; font-size: 11px; }
331
+ .gnode-l { fill: var(--muted); font-size: 10px; }
332
+ .gnode { cursor: pointer; }
333
+ .gnode circle { transition: opacity .12s; }
334
+ .gnode:hover circle, .gnode:focus circle { opacity: .82; }
335
+ .gnode:hover .gnode-l, .gnode:focus .gnode-l { fill: var(--text); }
336
+ .gnode:focus { outline: none; }
337
+
338
+ .hide { display: none !important; }
339
+
340
+ /* ---- responsive ------------------------------------------------------ */
341
+ @media (max-width: 760px) {
342
+ .brand small { display: none; }
343
+ .avail .lbl { display: none; }
344
+ /* keep the app bar from overflowing on phones; the banner still reports
345
+ a dropped connection, so the chip is redundant here. */
346
+ .connind { display: none; }
347
+ .mail { grid-template-columns: 1fr; }
348
+ /* On mobile show either the contacts list OR the open thread. */
349
+ body[data-pane="thread"] .contacts { display: none; }
350
+ body[data-pane="list"] .thread-wrap { display: none; }
351
+ .msg { max-width: 88%; }
352
+ main { padding: .8rem .7rem 4rem; }
353
+ }
354
+ </style>
355
+ </head>
356
+ <body data-pane="list">
357
+ <header id="bar">
358
+ <div class="brand">
359
+ <span class="dot">◆</span>
360
+ <span>Agentainer<small id="swarmMeta">control plane</small></span>
361
+ </div>
362
+ <span class="spacer"></span>
363
+ <span class="connind" id="connind" hidden></span>
364
+ <div class="avail" id="availWrap" hidden>
365
+ <span class="lbl" id="availLbl">You: away</span>
366
+ <label class="switch" title="Receive mail from agents">
367
+ <input type="checkbox" id="availToggle" />
368
+ <span class="track"></span><span class="knob"></span>
369
+ </label>
370
+ </div>
371
+ <nav id="nav" hidden>
372
+ <button class="navbtn active" data-view="agents">Agents</button>
373
+ <button class="navbtn" data-view="activity">Activity</button>
374
+ <button class="navbtn" data-view="settings">Settings</button>
375
+ </nav>
376
+ </header>
377
+
378
+ <main>
379
+ <div class="banner" id="banner"></div>
380
+
381
+ <!-- login -->
382
+ <section id="login" class="center">
383
+ <div class="card">
384
+ <h2>Connect</h2>
385
+ <p class="muted" style="margin-top:0">Enter the access token printed when you run <code>agentainer up</code> (also shown by <code>agentainer serve</code>).</p>
386
+ <div class="fld">
387
+ <input id="token" class="field" type="password" placeholder="UI token" autocomplete="off" />
388
+ </div>
389
+ <div class="rowend">
390
+ <button class="btn" id="connect">Connect</button>
391
+ </div>
392
+ </div>
393
+ </section>
394
+
395
+ <!-- everything below is rendered by app.js -->
396
+ <section id="view" hidden></section>
397
+ </main>
398
+
399
+ <div id="toast" class="toast"></div>
400
+ <div id="modalRoot"></div>
401
+
402
+ <script src="app.js"></script>
403
+ </body>
404
+ </html>
@@ -1,257 +0,0 @@
1
- # =============================================================================
2
- # Agentainer example configuration
3
- #
4
- # cp agents.example.yaml agents.yaml
5
- # agentainer validate --show-prompts # check it without launching anything
6
- # agentainer up # launch the swarm
7
- #
8
- # Every key is documented inline. Anything marked (optional) can be omitted.
9
- # =============================================================================
10
-
11
- swarm:
12
- name: dev-swarm
13
-
14
- # Where agent working directories are created. One folder per agent, named
15
- # after the agent. Relative paths resolve against this file's directory.
16
- root: ./workspace
17
-
18
- # (optional) Create missing agent folders automatically. Default true.
19
- # Set to false to point agents at directories that must already exist, so a
20
- # typo in a path fails loudly instead of silently creating an empty folder.
21
- create_workdirs: true
22
-
23
- # (optional) Prefix for tmux session names. With `swarm-`, the agent named
24
- # `developer` lives in the tmux session `swarm-developer`.
25
- session_prefix: ""
26
-
27
- # (optional) Timing knobs, all in milliseconds.
28
- send_delay_ms: 150 # pause before pasting text into a pane
29
- enter_delay_ms: 250 # pause between pasting text and pressing Enter
30
- max_forward_hops: 3 # stops auto-forward loops (A -> B -> A -> ...)
31
-
32
- # (optional) How long to keep probing an agent's input box for a response
33
- # before giving up and typing the first prompt anyway. Some CLIs (Claude Code)
34
- # silently discard keystrokes for several seconds partway through startup, so
35
- # Agentainer waits for the input box to echo a throwaway token before typing.
36
- ready_timeout_ms: 60000
37
-
38
- # (optional) An agent is "busy" from when a message is submitted to it until its
39
- # next captured turn end; senders are refused meanwhile and told to --queue or
40
- # --wait. If a capture never fires (crashed CLI, bad hook) the agent would look
41
- # busy forever, so after this long it is treated as idle again, with a warning.
42
- busy_timeout_ms: 900000
43
-
44
- # (optional) How messages look on the wire.
45
- # tagged -- messages arrive inside <swarm-message from=".." id=".."> ... </swarm-message>,
46
- # and an agent sends one by writing <swarm-send to="..."> ... </swarm-send>
47
- # in its reply. No shell quoting, so multi-line bodies survive intact.
48
- # plain -- the older "[swarm] message from <sender>:" header.
49
- message_format: tagged
50
-
51
- # (optional) An agent that answers a question as plain prose sends nothing: only a
52
- # <swarm-send> block is delivered. When it owes a reply and its turn ends without
53
- # one, Agentainer messages it explaining how to send. This is how many times.
54
- max_reply_reminders: 1
55
-
56
- # (optional) Reattach agents to their previous conversations on `up`, using the ids
57
- # recorded in <root>/.swarm/sessions.yaml. Same as passing `up --resume` every time.
58
- # Useful after a reboot. `up --no-resume` overrides it.
59
- resume: false
60
-
61
- # (optional) Only used by agents with `capture: pane` (gemini, hermes).
62
- pane_idle_ms: 2500 # pane must be unchanged this long to count as "done"
63
- pane_poll_ms: 700 # how often the watcher samples the pane
64
- pane_scrollback: 400 # lines of scrollback to diff
65
-
66
- # (optional) tmux comfort settings for when you `agentainer attach <agent>`.
67
- # The default tmux scrollback (2000 lines) is too small to hold a long
68
- # multi-agent conversation, so Agentainer raises it before creating sessions.
69
- # mouse mode lets you wheel-scroll the backlog; press q to leave copy mode.
70
- # Both are applied to the tmux server, so they also affect your other sessions.
71
- tmux_history_limit: 50000 # lines of scrollback per agent pane (0 = leave tmux's default)
72
- tmux_mouse: true # enable mouse wheel scrolling in the panes
73
-
74
- # -----------------------------------------------------------------------------
75
- # defaults (optional) -- applied to every agent that does not override them.
76
- # -----------------------------------------------------------------------------
77
- defaults:
78
- type: claude
79
- append_agents_that_you_can_talk_to_prompt: true
80
- in_first_prompt_append_your_task_will_be_sent_in_the_next_prompt: false
81
- env:
82
- SWARM_DEMO: "1"
83
-
84
- # -----------------------------------------------------------------------------
85
- # agent_types (optional) -- override the built-in launch commands, or define
86
- # entirely new agent types. Built-ins are: claude, codex, gemini, hermes.
87
- #
88
- # capture: how Agentainer learns that an agent finished a turn.
89
- # hook -- the CLI can run an external program on turn completion.
90
- # Supported for claude (Stop hook) and codex (notify program).
91
- # pane -- no such facility: poll the tmux pane and diff it. Heuristic.
92
- # none -- do not capture at all.
93
- #
94
- # boot_delay_ms: a grace period before Agentainer starts probing the input
95
- # box. It does NOT need to cover the full startup time -- delivery of the
96
- # first prompt is verified and retried, so a small value is fine.
97
- # -----------------------------------------------------------------------------
98
- agent_types:
99
- claude:
100
- command: "claude --dangerously-skip-permissions"
101
- capture: hook
102
- boot_delay_ms: 3000
103
- codex:
104
- command: "codex --yolo"
105
- capture: hook
106
- boot_delay_ms: 3000
107
- gemini:
108
- command: "gemini --yolo"
109
- capture: pane
110
- boot_delay_ms: 4000
111
- hermes:
112
- command: "hermes"
113
- capture: pane
114
- boot_delay_ms: 3000
115
-
116
- # -----------------------------------------------------------------------------
117
- # agents -- one tmux session and one directory each.
118
- # -----------------------------------------------------------------------------
119
- agents:
120
-
121
- - name: orchestrator
122
- type: claude
123
- # (optional) Override the type's command, e.g. to pin a model.
124
- command: "claude --dangerously-skip-permissions --model opus"
125
-
126
- # Who this agent is allowed to message. Use "*" for "everyone else".
127
- # An agent that tries to message anyone not listed here is refused.
128
- can_talk_to: ["researcher", "developer", "reviewer"]
129
-
130
- first_prompt: |
131
- You are the ORCHESTRATOR of a small software team.
132
-
133
- Your job is to break the user's goal into concrete pieces of work, hand
134
- each piece to the right specialist, and assemble their answers into a
135
- final result. You do not write production code yourself.
136
-
137
- Team:
138
- - researcher: investigates prior art, APIs and docs
139
- - developer: writes the implementation
140
- - reviewer: critiques diffs and finds bugs
141
-
142
- Start by asking the researcher what they can find, then brief the developer.
143
-
144
- # This agent is told its task arrives in a follow-up prompt, so it will
145
- # acknowledge and wait rather than inventing work.
146
- in_first_prompt_append_your_task_will_be_sent_in_the_next_prompt: true
147
-
148
- - name: researcher
149
- type: gemini
150
- command: "gemini --yolo"
151
- can_talk_to: ["orchestrator", "developer"]
152
-
153
- # Whenever this agent finishes a turn, its final message is automatically
154
- # relayed to these agents. Every name here must also be in can_talk_to.
155
- # Leave it out (the default) to let the agent decide when to speak.
156
- forward_responses_to: ["orchestrator"]
157
-
158
- first_prompt: |
159
- You are the RESEARCHER. You investigate libraries, APIs, prior art and
160
- documentation, then report findings concisely with sources.
161
-
162
- You never write production code. When you have an answer, state it plainly:
163
- what exists, what it costs, and what you recommend.
164
-
165
- - name: developer
166
- type: codex
167
- command: "codex --yolo"
168
- can_talk_to: ["orchestrator", "reviewer"]
169
-
170
- # (optional) Per-agent environment variables, exported into its tmux session.
171
- env:
172
- GIT_AUTHOR_NAME: "swarm-developer"
173
-
174
- # (optional) Before typing the first prompt, type a throwaway token and wait
175
- # for the input box to echo it back. Defaults to true; turn it off only for
176
- # a command that is not an interactive TUI.
177
- ready_probe: true
178
-
179
- # (optional) Refuse incoming messages while this agent is mid-turn, so a second
180
- # agent is told "developer is busy, --queue it or --wait" instead of interrupting.
181
- # Needs a capture to know when a turn ends; forced off when capture: none.
182
- busy_check: true
183
-
184
- # (optional) Read <swarm-send> blocks out of this agent's replies and deliver
185
- # them. Needs a capture; forced off when capture: none or message_format: plain.
186
- parse_outbound_tags: true
187
-
188
- # (optional) Nudge this agent when it owes a reply but sent nothing, or when the
189
- # block it wrote could not be delivered. Off when its tags are not parsed.
190
- reply_reminder: true
191
-
192
- # (optional) How to reattach this agent to a previous conversation. `resume_args`
193
- # is appended to `command`; `resume_command` replaces it outright, which you need
194
- # when the command goes through an alias or wrapper. {session_id} is substituted.
195
- #
196
- # resume_args: "resume {session_id}" # -> codex --yolo resume <id>
197
- # resume_command: "bash -ic 'mywrapper --resume {session_id}'"
198
-
199
- first_prompt: |
200
- You are the DEVELOPER. You implement what the orchestrator asks for, in
201
- the working directory you were started in.
202
-
203
- Write real, runnable code. When you finish a unit of work, send the
204
- reviewer a short summary of what you changed and why, and ask for review.
205
-
206
- - name: reviewer
207
- type: claude
208
- command: "claude --dangerously-skip-permissions"
209
- can_talk_to: ["developer", "orchestrator"]
210
-
211
- # (optional) Run this agent somewhere specific instead of <root>/<name>.
212
- # Relative paths resolve against this file. `~` is expanded. The
213
- # placeholders {name}, {root}, {swarm} and {type} are substituted, so a
214
- # single `defaults.workdir` can serve every agent, e.g. "{root}/{name}-wt".
215
- #
216
- # Several agents may share one directory (they then see each other's edits;
217
- # Agentainer warns you). Pair `create_workdir: false` with an existing
218
- # project so a bad path is an error, not a new empty folder.
219
- #
220
- # workdir: ~/projects/acme-api
221
- # create_workdir: false
222
-
223
- # (optional) Read the prompt from a file instead of inlining it here.
224
- # first_prompt_file: ./prompts/reviewer.md
225
- first_prompt: |
226
- You are the REVIEWER. You read the developer's changes and hunt for real
227
- bugs: wrong logic, unhandled errors, race conditions, security problems.
228
-
229
- Be specific and cite file:line. If a change is fine, say so briefly rather
230
- than inventing nitpicks. Report your verdict back to the developer.
231
-
232
- - name: scribe
233
- type: hermes
234
- command: "hermes"
235
-
236
- # This agent listens only: nobody can be messaged by it, and its own
237
- # responses are not captured.
238
- can_talk_to: []
239
- capture: none
240
-
241
- # Turn off the auto-appended communication block, since it has no peers.
242
- append_agents_that_you_can_talk_to_prompt: false
243
-
244
- first_prompt: |
245
- You are the SCRIBE. Keep a running changelog of the project in NOTES.md.
246
-
247
- # -----------------------------------------------------------------------------
248
- # templates (optional) -- override the text Agentainer appends to first prompts.
249
- # Available placeholders: {agent} {swarm} {peers} {prefix} {inbox} {workdir}
250
- # -----------------------------------------------------------------------------
251
- #
252
- # templates:
253
- # comms: |
254
- # You are {agent}. You may message: {peers}.
255
- # Send with: swarm send --to <agent> "message"
256
- # task_notice: |
257
- # Stand by. Your real task arrives in the next message.