@pasko70/pibo 1.9.2 → 1.9.3

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 (120) hide show
  1. package/README.md +183 -183
  2. package/context/codex-base-prompt.md +148 -148
  3. package/context/compute-worker.md +23 -23
  4. package/context/pibo-compaction-prompt.md +100 -100
  5. package/context/pibo-native-tooling.md +18 -18
  6. package/context/pibo-system-prompt.md +77 -77
  7. package/dist/apps/chat/agent-store.js +248 -248
  8. package/dist/apps/chat/data/project-service.js +168 -168
  9. package/dist/apps/chat/data/read-state-service.js +18 -18
  10. package/dist/apps/chat/data/session-query-service.js +25 -25
  11. package/dist/apps/chat/data/timeline-query-service.js +14 -14
  12. package/dist/apps/chat/static-assets.js +854 -854
  13. package/dist/apps/chat/workflow-persistence.js +255 -255
  14. package/dist/apps/chat-ui/assets/{dist-5GM30SQK.js → dist-5WHjqNrY.js} +1 -1
  15. package/dist/apps/chat-ui/assets/{dist-Cge8JklW.js → dist-BJBNSAIH.js} +1 -1
  16. package/dist/apps/chat-ui/assets/{dist-C-5u2QIS.js → dist-C3liNPZJ.js} +1 -1
  17. package/dist/apps/chat-ui/assets/{dist-BWbWIOcD.js → dist-CW6FHEaw.js} +1 -1
  18. package/dist/apps/chat-ui/assets/{dist-CjYtD7ZT.js → dist-CaNlj6v6.js} +1 -1
  19. package/dist/apps/chat-ui/assets/{dist-BYKMZlI0.js → dist-Cne74s6i.js} +1 -1
  20. package/dist/apps/chat-ui/assets/{dist-9lsp1UpA.js → dist-DvKZv20v.js} +1 -1
  21. package/dist/apps/chat-ui/assets/{dist-DFhhiR8M.js → dist-FYH7vzrh.js} +1 -1
  22. package/dist/apps/chat-ui/assets/{dist-Etxmpyxg.js → dist-f5KioIwL.js} +1 -1
  23. package/dist/apps/chat-ui/assets/{dist-2KdPXbMT.js → dist-hWrpj7SE.js} +1 -1
  24. package/dist/apps/chat-ui/assets/{dist-Idz5kzy8.js → dist-jAuO8_bV.js} +1 -1
  25. package/dist/apps/chat-ui/assets/{index-BqD_bm7z.js → index-Bf29pJph.js} +29 -29
  26. package/dist/apps/chat-ui/index.html +17 -17
  27. package/dist/apps/chat-ui/manifest.webmanifest +25 -25
  28. package/dist/apps/chat-ui/sw.js +47 -47
  29. package/dist/apps/chat-vscode-web/assets/index-BAMxIaI_.js +41 -0
  30. package/dist/apps/chat-vscode-web/index.html +13 -13
  31. package/dist/apps/cli-ui/cliSessionsCommand.js +23 -23
  32. package/dist/apps/context-files-ui/index.html +11 -11
  33. package/dist/cli.js +39 -39
  34. package/dist/compute/cli.js +54 -54
  35. package/dist/core/session-router.js +5 -2
  36. package/dist/cron/cli.js +15 -15
  37. package/dist/cron/store.js +49 -49
  38. package/dist/data/cli.js +23 -23
  39. package/dist/data/event-log.js +23 -23
  40. package/dist/data/message-store.js +20 -20
  41. package/dist/data/navigation-store.js +9 -9
  42. package/dist/data/observation-store.js +4 -4
  43. package/dist/data/payload-store.js +17 -17
  44. package/dist/data/schema.js +431 -431
  45. package/dist/data/session-store.js +4 -4
  46. package/dist/data/telemetry-queries.js +54 -54
  47. package/dist/data/telemetry.js +197 -197
  48. package/dist/debug/events.js +12 -12
  49. package/dist/debug/failures.js +6 -6
  50. package/dist/debug/index.js +227 -227
  51. package/dist/debug/messages.js +6 -6
  52. package/dist/debug/pty.js +124 -124
  53. package/dist/debug/session.js +29 -29
  54. package/dist/debug/tools.js +5 -5
  55. package/dist/debug/web-snapshot-browser-scripts.js +294 -294
  56. package/dist/debug/web-streaming-browser-library.js +925 -925
  57. package/dist/debug/web-streaming-browser-scripts.js +232 -232
  58. package/dist/debug/web-streaming-provider-telemetry.js +4 -4
  59. package/dist/debug/web.js +93 -93
  60. package/dist/gateway/cli.js +19 -19
  61. package/dist/mcp/config-command.js +53 -53
  62. package/dist/mcp/index.js +21 -21
  63. package/dist/mcp/registry.js +11 -11
  64. package/dist/pi-packages/cli.js +11 -11
  65. package/dist/plugins/context-files-store.js +110 -110
  66. package/dist/plugins/context-files.js +4 -4
  67. package/dist/ralph/cli.js +18 -18
  68. package/dist/ralph/templates.js +140 -140
  69. package/dist/reliability/store.js +226 -226
  70. package/dist/session-ui/sessionActivity.js +30 -0
  71. package/dist/sessions/pibo-data-store.js +16 -16
  72. package/dist/sessions/sqlite-store.js +53 -53
  73. package/dist/setup/cli.js +58 -58
  74. package/dist/signals/projector.js +26 -11
  75. package/dist/signals/registry.js +41 -0
  76. package/dist/tools/agent-browser-wrapper.js +80 -80
  77. package/dist/tools/browser-use-cdp.js +12 -12
  78. package/dist/tools/browser-use-wrapper.js +762 -762
  79. package/dist/tools/guides.js +538 -538
  80. package/dist/tools/index.js +99 -99
  81. package/dist/tools/runtime/node-worker-source.js +205 -205
  82. package/dist/tools/runtime/python-worker-source.js +177 -177
  83. package/dist/vscode/cli.js +9 -9
  84. package/dist/web-annotations/cdp.js +900 -900
  85. package/dist/web-annotations/store.js +96 -96
  86. package/docs/README.md +23 -23
  87. package/docs/ops/install-developer-host.md +112 -112
  88. package/docs/ops/install-user-host.md +96 -96
  89. package/docs/ops/upgrade-user-to-developer-host.md +69 -69
  90. package/docs/ops/vscode-extension-release.md +160 -160
  91. package/package.json +95 -95
  92. package/skills/builtin/graphify/SKILL.md +52 -52
  93. package/skills/builtin/pi-agent-harness/SKILL.md +319 -319
  94. package/skills/builtin/pi-agent-harness/agents/openai.yaml +4 -4
  95. package/skills/builtin/pibo-docker-system/SKILL.md +170 -170
  96. package/skills/builtin/pibo-spec-writing/SKILL.md +330 -330
  97. package/skills/builtin/prd/SKILL.md +143 -143
  98. package/skills/builtin/ralph-loop/SKILL.md +359 -359
  99. package/skills/builtin/ralph-prd-json/SKILL.md +123 -123
  100. package/skills/builtin/skill-creator/LICENSE.txt +201 -201
  101. package/skills/builtin/skill-creator/SKILL.md +513 -513
  102. package/skills/builtin/skill-creator/agents/analyzer.md +274 -274
  103. package/skills/builtin/skill-creator/agents/comparator.md +202 -202
  104. package/skills/builtin/skill-creator/agents/grader.md +223 -223
  105. package/skills/builtin/skill-creator/assets/eval_review.html +146 -146
  106. package/skills/builtin/skill-creator/eval-viewer/generate_review.py +471 -471
  107. package/skills/builtin/skill-creator/eval-viewer/viewer.html +1325 -1325
  108. package/skills/builtin/skill-creator/references/schemas.md +430 -430
  109. package/skills/builtin/skill-creator/scripts/aggregate_benchmark.py +401 -401
  110. package/skills/builtin/skill-creator/scripts/generate_report.py +326 -326
  111. package/skills/builtin/skill-creator/scripts/improve_description.py +247 -247
  112. package/skills/builtin/skill-creator/scripts/package_skill.py +136 -136
  113. package/skills/builtin/skill-creator/scripts/quick_validate.py +102 -102
  114. package/skills/builtin/skill-creator/scripts/run_eval.py +310 -310
  115. package/skills/builtin/skill-creator/scripts/run_loop.py +328 -328
  116. package/skills/builtin/skill-creator/scripts/utils.py +47 -47
  117. package/skills/builtin/web-annotations/SKILL.md +93 -93
  118. package/src/mcp/LICENSE.mcp-cli +21 -21
  119. package/dist/apps/chat-vscode-web/assets/index-Cst9OUkC.js +0 -41
  120. package/dist/session-ui/activeTurn.js +0 -126
@@ -139,859 +139,859 @@ function compressedAssetBody(path, body, encoding) {
139
139
  return compressed;
140
140
  }
141
141
  export function createFallbackChatHtml() {
142
- return `<!doctype html>
143
- <html lang="de">
144
- <head>
145
- <meta charset="utf-8">
146
- <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
147
- <meta name="theme-color" content="#101d22">
148
- <meta name="apple-mobile-web-app-capable" content="yes">
149
- <meta name="apple-mobile-web-app-title" content="Pibo Chat">
150
- <link rel="manifest" href="/apps/chat/manifest.webmanifest">
151
- <link rel="apple-touch-icon" href="/apps/chat/assets/pwa-images/ios/180.png">
152
- <title>Pibo Web Chat</title>
153
- <style>
154
- :root { color-scheme: dark; font-family: "Public Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #101d22; color: #d8e3e7; }
155
- * { box-sizing: border-box; }
156
- body { margin: 0; height: 100vh; overflow: hidden; background: #101d22; }
157
- button, textarea { font: inherit; }
158
- button { border: 1px solid #334155; background: #151f24; color: #d8e3e7; border-radius: 3px; padding: 7px 10px; cursor: pointer; }
159
- button:hover { border-color: #11a4d4; color: #ffffff; }
160
- button.primary { background: #11a4d4; border-color: #11a4d4; color: #ffffff; }
161
- button.ghost { background: transparent; }
162
- .app { display: grid; grid-template-rows: 56px 1fr; height: 100vh; }
163
- .topbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 0 16px; background: #1a262b; border-bottom: 1px solid #1e293b; }
164
- .brand { display: flex; align-items: center; gap: 8px; font-size: 17px; font-weight: 800; letter-spacing: .08em; color: #e7f7fb; text-transform: uppercase; white-space: nowrap; }
165
- .fallback-badge { border: 1px solid #f59e0b; border-radius: 999px; padding: 2px 6px; color: #fbbf24; font-size: 9px; letter-spacing: .06em; }
166
- .tabs { display: flex; gap: 6px; }
167
- .tab { height: 32px; text-transform: uppercase; font-size: 12px; letter-spacing: .06em; }
168
- .tab.active { border-color: #11a4d4; color: #11a4d4; background: rgba(17,164,212,.08); }
169
- .userbar { display: flex; align-items: center; gap: 8px; min-width: 0; color: #94a3b8; font-size: 12px; }
170
- .workspace { display: grid; grid-template-columns: 300px minmax(0,1fr) 320px; min-height: 0; }
171
- .sidebar, .inspector { background: #1a262b; border-right: 1px solid #1e293b; min-height: 0; overflow: auto; }
172
- .inspector { border-right: 0; border-left: 1px solid #1e293b; background: #0e1116; }
173
- .panel-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 42px; padding: 10px 12px; border-bottom: 1px solid #1e293b; color: #cbd5e1; font-size: 12px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
174
- .panel-actions { display: flex; align-items: center; gap: 6px; }
175
- .icon-button { width: 28px; height: 28px; padding: 0; display: grid; place-items: center; line-height: 1; }
176
- .session-tree { padding: 8px; }
177
- .session-row { width: 100%; display: grid; grid-template-columns: 28px minmax(0,1fr); align-items: center; margin-bottom: 4px; border: 1px solid transparent; border-radius: 3px; background: transparent; padding: 0 8px 0 0; }
178
- .session-row.active { border-color: #11a4d4; background: rgba(17,164,212,.10); }
179
- .session-toggle, .session-select { border: 0; background: transparent; border-radius: 0; padding: 0; }
180
- .session-toggle { width: 28px; height: 32px; display: grid; place-items: center; color: #64748b; }
181
- .session-toggle:hover { color: #11a4d4; border-color: transparent; }
182
- .session-select { min-width: 0; display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 7px; align-items: center; text-align: left; padding: 7px 0; }
183
- .session-select:hover { border-color: transparent; }
184
- .session-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; color: #e2e8f0; }
185
- .session-id { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; color: #64748b; }
186
- .status-dot { width: 7px; height: 7px; border-radius: 999px; background: #64748b; }
187
- .status-dot.running { background: #0bda57; box-shadow: 0 0 10px rgba(11,218,87,.35); }
188
- .status-dot.error { background: #ef4444; }
189
- .main { min-height: 0; display: grid; grid-template-rows: auto 1fr auto; background: #101d22; }
190
- .session-head { min-height: 58px; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-bottom: 1px solid #1e293b; background: #151f24; }
191
- .session-name { margin: 0; font-size: 16px; line-height: 1.2; }
192
- .session-meta { margin-top: 4px; color: #64748b; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; }
193
- .trace-controls { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end; }
194
- .content { min-height: 0; overflow: auto; padding: 14px; }
195
- .trace-list { min-width: 620px; display: flex; flex-direction: column; gap: 9px; }
196
- /* Ported from pydantic-tracing SpanNode/JsonRenderer visual structure. */
197
- .span-card { border: 1px solid #334155; border-radius: 3px; background: #1a262b; overflow: hidden; }
198
- .span-card.ok { border-color: rgba(11,218,87,.30); }
199
- .span-card.running { border-color: rgba(17,164,212,.50); box-shadow: 0 0 10px rgba(17,164,212,.10); }
200
- .span-card.error { border-color: rgba(255,107,0,.50); }
201
- .span-card[data-span-type="tool.call"] { border-color: rgba(168,85,247,.52); }
202
- .span-card[data-span-type="tool.result"] { border-color: rgba(34,197,94,.42); }
203
- .span-card[data-span-type="model.reasoning"] { border-color: rgba(245,158,11,.48); }
204
- .span-card[data-span-type="agent.delegation"] { border-color: rgba(249,115,22,.58); }
205
- .span-card[data-span-type="user.prompt"] { border-color: rgba(6,182,212,.48); }
206
- .span-header { width: 100%; display: grid; grid-template-columns: 18px 22px minmax(0,1fr) auto; align-items: center; gap: 8px; padding: 9px 10px; background: #151f24; border: 0; text-align: left; }
207
- .span-card.ok > .span-header { background: rgba(11,218,87,.05); }
208
- .span-card.running > .span-header { background: rgba(17,164,212,.05); }
209
- .span-card.error > .span-header { background: rgba(255,107,0,.05); }
210
- .span-chevron { color: #64748b; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
211
- .span-icon { width: 18px; height: 18px; display: grid; place-items: center; border-radius: 999px; background: rgba(17,164,212,.20); color: #11a4d4; font: 11px ui-monospace, SFMono-Regular, Menlo, monospace; }
212
- .span-card[data-span-type="tool.call"] .span-icon { background: rgba(168,85,247,.20); color: #a855f7; }
213
- .span-card[data-span-type="tool.result"] .span-icon { background: rgba(34,197,94,.20); color: #22c55e; }
214
- .span-card[data-span-type="model.reasoning"] .span-icon { background: rgba(245,158,11,.20); color: #f59e0b; }
215
- .span-card[data-span-type="agent.delegation"] .span-icon { background: rgba(249,115,22,.20); color: #f97316; }
216
- .span-card[data-span-type="user.prompt"] .span-icon { background: rgba(6,182,212,.20); color: #06b6d4; }
217
- .span-type-label { color: #cbd5e1; font-size: 11px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase; }
218
- .span-title { color: #94a3b8; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
219
- .span-meta { display: flex; align-items: center; gap: 6px; }
220
- .span-body { display: none; border-top: 1px solid #1e293b; }
221
- .span-card.open > .span-body { display: block; }
222
- .span-actions { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px 10px; border-bottom: 1px solid #1e293b; background: rgba(15,23,42,.18); }
223
- .span-quote { padding: 14px; color: #cbd5e1; font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; }
224
- .span-code-well { padding: 12px 14px; background: #0e1116; border-bottom: 1px solid #1e293b; overflow: auto; }
225
- .span-function { font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; color: #22c55e; white-space: pre-wrap; }
226
- .syntax-keyword { color: #c084fc; }
227
- .syntax-name { color: #fde047; }
228
- .syntax-key { color: #60a5fa; }
229
- .syntax-string { color: #86efac; }
230
- .span-output-summary { display: flex; align-items: center; gap: 8px; min-width: 0; padding: 8px 14px; background: rgba(15,23,42,.28); border-bottom: 1px solid #1e293b; color: #94a3b8; font-size: 12px; }
231
- .span-output-summary code { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #cbd5e1; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
232
- .span-section { padding: 12px 14px; background: #0e1116; border-top: 1px solid rgba(51,65,85,.55); }
233
- .span-section-title { margin-bottom: 7px; color: #60a5fa; font-size: 10px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
234
- .span-section-title.output { color: #22c55e; }
235
- .reasoning-block { padding: 14px; background: rgba(245,158,11,.06); color: #cbd5e1; font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; }
236
- .reasoning-comment { color: rgba(245,158,11,.75); }
237
- .delegation-card { padding: 14px; background: rgba(249,115,22,.06); border-bottom: 1px solid rgba(249,115,22,.20); }
238
- .delegation-title { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; color: #f97316; font-size: 13px; font-weight: 700; }
239
- .delegation-query { color: #94a3b8; font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; }
240
- .json-renderer { margin: 0; max-height: 24rem; overflow: auto; white-space: pre-wrap; color: #dbeafe; font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; }
241
- .trace-node { border: 1px solid #334155; border-radius: 3px; background: #1a262b; overflow: hidden; }
242
- .trace-node[data-type="user.message"] { border-color: rgba(6,182,212,.42); }
243
- .trace-node[data-type="assistant.message"] { border-color: rgba(34,197,94,.35); }
244
- .trace-node[data-type="model.reasoning"] { border-color: rgba(245,158,11,.45); }
245
- .trace-node[data-type="tool.call"] { border-color: rgba(168,85,247,.45); }
246
- .trace-node[data-type="agent.delegation"] { border-color: rgba(249,115,22,.55); }
247
- .trace-node[data-type="agent.async"] { border-color: rgba(249,115,22,.55); }
248
- .trace-node[data-type="execution.command"] { border-color: rgba(17,164,212,.42); }
249
- .trace-node[data-type="error"] { border-color: rgba(239,68,68,.65); }
250
- .trace-header { width: 100%; display: grid; grid-template-columns: 22px 1fr auto; align-items: center; gap: 8px; padding: 9px 10px; background: #151f24; border: 0; text-align: left; }
251
- .trace-icon { width: 18px; height: 18px; display: grid; place-items: center; border-radius: 999px; background: rgba(17,164,212,.14); color: #11a4d4; font-size: 11px; }
252
- .trace-label { color: #cbd5e1; font-size: 11px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase; }
253
- .trace-summary { margin-top: 3px; color: #94a3b8; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
254
- .trace-actions { display: flex; align-items: center; gap: 6px; }
255
- .badge { border: 1px solid #334155; border-radius: 999px; padding: 2px 6px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; color: #94a3b8; }
256
- .badge.running { color: #0bda57; border-color: rgba(11,218,87,.35); }
257
- .badge.error { color: #ef4444; border-color: rgba(239,68,68,.45); }
258
- .trace-body { display: none; padding: 10px; border-top: 1px solid #1e293b; }
259
- .trace-node.open > .trace-body { display: block; }
260
- .payload { margin: 0; white-space: pre-wrap; overflow: auto; max-height: 360px; padding: 10px; border: 1px solid #1e293b; border-radius: 3px; background: #0e1116; color: #dbeafe; font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; }
261
- .children { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; padding-left: 12px; border-left: 1px solid #334155; }
262
- .composer-wrap { position: relative; padding: 10px 12px; border-top: 1px solid #1e293b; background: #151f24; }
263
- .composer { display: grid; grid-template-columns: 1fr auto; gap: 8px; }
264
- textarea { width: 100%; min-height: 48px; max-height: 150px; resize: vertical; border: 1px solid #334155; border-radius: 3px; padding: 10px; background: #0e1116; color: #e2e8f0; }
265
- .command-menu { position: absolute; left: 12px; bottom: 74px; width: min(520px, calc(100% - 24px)); max-height: 280px; overflow: auto; background: #0e1116; border: 1px solid #11a4d4; border-radius: 3px; box-shadow: 0 12px 30px rgba(0,0,0,.35); z-index: 20; }
266
- .command-item { display: grid; grid-template-columns: 120px 1fr; gap: 10px; width: 100%; padding: 9px 10px; border: 0; border-bottom: 1px solid #1e293b; text-align: left; background: transparent; }
267
- .command-item.active { background: rgba(17,164,212,.13); }
268
- .command-name { color: #11a4d4; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
269
- .command-desc { color: #94a3b8; font-size: 12px; }
270
- .inspector-log { padding: 10px; display: flex; flex-direction: column; gap: 8px; }
271
- .raw-event { border-left: 2px solid #11a4d4; padding: 8px; background: #151f24; }
272
- .raw-event-type { color: #11a4d4; font: 11px ui-monospace, SFMono-Regular, Menlo, monospace; margin-bottom: 5px; }
273
- .placeholder { padding: 18px; color: #94a3b8; }
274
- .modal-backdrop { position: fixed; inset: 0; display: grid; place-items: center; background: rgba(0,0,0,.62); z-index: 50; }
275
- .modal { width: min(420px, calc(100vw - 32px)); border: 1px solid #334155; border-radius: 4px; background: #1a262b; padding: 16px; }
276
- .modal h2 { margin: 0 0 8px; font-size: 15px; }
277
- .modal p { margin: 0 0 14px; color: #94a3b8; font-size: 13px; }
278
- .modal-actions { display: flex; justify-content: flex-end; gap: 8px; }
279
- .hidden { display: none !important; }
280
- @media (max-width: 980px) {
281
- .workspace { grid-template-columns: 240px minmax(0,1fr); }
282
- .inspector { display: none; }
283
- }
284
- @media (max-width: 720px) {
285
- body { overflow: auto; }
286
- .app { height: auto; min-height: 100vh; }
287
- .topbar { flex-wrap: wrap; height: auto; min-height: 56px; padding: 10px; }
288
- .workspace { grid-template-columns: 1fr; }
289
- .sidebar { max-height: 260px; }
290
- .main { min-height: 70vh; }
291
- .trace-list { min-width: 0; }
292
- .composer { grid-template-columns: 1fr; }
293
- }
294
- </style>
295
- </head>
296
- <body>
297
- <div class="app">
298
- <header class="topbar">
299
- <div class="brand">Pibo Chat <span class="fallback-badge" title="Built React Chat assets are unavailable">Fallback UI</span></div>
300
- <nav class="tabs">
301
- <button class="tab active" data-area="sessions">Sessions</button>
302
- <button class="tab" data-area="agents">Agents</button>
303
- <button class="tab" data-area="settings">Settings</button>
304
- </nav>
305
- <div class="userbar" id="user"></div>
306
- </header>
307
- <div class="workspace">
308
- <aside class="sidebar">
309
- <div class="panel-head">
310
- <span id="sidebar-title">Sessions</span>
311
- <div class="panel-actions">
312
- <button class="ghost icon-button" id="new-session" title="New Session" aria-label="New Session">+</button>
313
- <button class="ghost" id="refresh">Refresh</button>
314
- </div>
315
- </div>
316
- <div class="session-tree" id="session-tree"></div>
317
- </aside>
318
- <main class="main">
319
- <div class="session-head">
320
- <div>
321
- <h1 class="session-name" id="session-title">Loading</h1>
322
- <div class="session-meta" id="session-meta"></div>
323
- </div>
324
- <div class="trace-controls">
325
- <button class="ghost" id="thinking-toggle">Thinking Off</button>
326
- <button class="ghost" id="collapse-all">Collapse All</button>
327
- <button class="ghost" id="expand-one">Depth 1</button>
328
- <button class="ghost" id="expand-all">Expand All</button>
329
- </div>
330
- </div>
331
- <section class="content" id="content"></section>
332
- <div class="composer-wrap" id="composer-wrap">
333
- <div class="command-menu hidden" id="command-menu"></div>
334
- <form class="composer" id="composer">
335
- <textarea id="message" name="message" placeholder="Send Message (/ for commands or $ for skills)"></textarea>
336
- <button class="primary" type="submit">Send</button>
337
- </form>
338
- </div>
339
- </main>
340
- <aside class="inspector">
341
- <div class="panel-head"><span>Raw Events</span><button class="ghost" id="clear-local">Clear UI</button></div>
342
- <div class="inspector-log" id="raw-log"></div>
343
- </aside>
344
- </div>
345
- </div>
346
- <div class="modal-backdrop hidden" id="fork-modal">
347
- <div class="modal">
348
- <h2>Zur geforkten Session wechseln?</h2>
349
- <p>Der Fork wurde erstellt. Wenn du wechselst, wird die neue Session geladen.</p>
350
- <div class="modal-actions">
351
- <button id="fork-stay">Nein</button>
352
- <button class="primary" id="fork-switch">Ja</button>
353
- </div>
354
- </div>
355
- </div>
356
- <script>
357
- const userEl = document.querySelector("#user");
358
- const sessionTreeEl = document.querySelector("#session-tree");
359
- const sessionTitleEl = document.querySelector("#session-title");
360
- const sessionMetaEl = document.querySelector("#session-meta");
361
- const contentEl = document.querySelector("#content");
362
- const rawLogEl = document.querySelector("#raw-log");
363
- const composer = document.querySelector("#composer");
364
- const messageInput = document.querySelector("#message");
365
- const commandMenu = document.querySelector("#command-menu");
366
- const forkModal = document.querySelector("#fork-modal");
367
- const newSessionButton = document.querySelector("#new-session");
368
- let eventSource;
369
- let bootstrap;
370
- let selectedPiboSessionId;
371
- let area = "sessions";
372
- let pendingForkResult;
373
- let pendingForkComposerText;
374
- let showThinking = localStorage.getItem("pibo.chat.showThinking") === "true";
375
- let selectedAgentProfile = localStorage.getItem("pibo.chat.newSessionProfile") || "";
376
- let openNodes = new Set(JSON.parse(localStorage.getItem("pibo.chat.openNodes") || "[]"));
377
- let openSessionNodes = new Set();
378
- let commandIndex = 0;
379
-
380
- function saveOpenNodes() {
381
- localStorage.setItem("pibo.chat.openNodes", JSON.stringify(Array.from(openNodes)));
382
- }
383
- function escapeText(value) {
384
- return String(value == null ? "" : value).replace(/[&<>"']/g, function(ch) {
385
- return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[ch];
386
- });
387
- }
388
- function pretty(value) {
389
- if (value == null || value === "") return "";
390
- if (typeof value === "string") return value;
391
- try { return JSON.stringify(value, null, 2); } catch { return String(value); }
392
- }
393
- function short(value, n) {
394
- const text = String(value || "").replace(/\\s+/g, " ").trim();
395
- return text.length > n ? text.slice(0, n - 1) + "…" : text;
396
- }
397
- function nodeLabel(type) {
398
- return {
399
- "user.message": "User Message",
400
- "assistant.message": "Agent Message",
401
- "agent.turn": "Agent Turn",
402
- "model.reasoning": "Reasoning",
403
- "tool.call": "Tool Call",
404
- "tool.result": "Tool Result",
405
- "agent.delegation": "Agent Delegation",
406
- "agent.async": "Async Agent",
407
- "execution.command": "Execution Command",
408
- "yielded.run": "Yielded Run",
409
- "error": "Error"
410
- }[type] || type;
411
- }
412
- function nodeIcon(type) {
413
- return {
414
- "user.message": "U",
415
- "assistant.message": "A",
416
- "agent.turn": "R",
417
- "model.reasoning": "?",
418
- "tool.call": "T",
419
- "tool.result": "O",
420
- "agent.delegation": "D",
421
- "agent.async": "A",
422
- "execution.command": "$",
423
- "yielded.run": "Y",
424
- "error": "!"
425
- }[type] || "*";
426
- }
427
- function spanType(type) {
428
- return {
429
- "user.message": "user.prompt",
430
- "assistant.message": "model.response",
431
- "agent.turn": "agent.run",
432
- "model.reasoning": "model.reasoning",
433
- "tool.call": "tool.call",
434
- "tool.result": "tool.result",
435
- "agent.delegation": "agent.delegation",
436
- "agent.async": "agent.async",
437
- "execution.command": "tool.result",
438
- "yielded.run": "tool.call",
439
- "error": "tool.result"
440
- }[type] || "agent.run";
441
- }
442
- function spanLabel(type) {
443
- const mapped = spanType(type);
444
- return {
445
- "agent.run": "Agent Run",
446
- "tool.call": "Tool Call",
447
- "tool.result": "Tool Result",
448
- "model.response": "Model Response",
449
- "model.reasoning": "Reasoning",
450
- "agent.delegation": "Agent Delegation",
451
- "agent.async": "Async Agent",
452
- "user.prompt": "User Prompt"
453
- }[mapped] || mapped;
454
- }
455
- function spanStatusClass(status) {
456
- if (status === "error") return "error";
457
- if (status === "running") return "running";
458
- return "ok";
459
- }
460
- function renderJson(value) {
461
- return '<pre class="json-renderer">' + escapeText(pretty(value)) + '</pre>';
462
- }
463
- function functionSignature(name, args) {
464
- const input = args && typeof args === "object" && !Array.isArray(args) ? args : {};
465
- const params = Object.entries(input).map(function(entry) {
466
- const key = entry[0];
467
- const value = entry[1];
468
- const raw = typeof value === "string" ? "'" + short(value, 50) + "'" : short(pretty(value), 50);
469
- return '<span class="syntax-key">' + escapeText(key) + '</span>=<span class="syntax-string">' + escapeText(raw) + '</span>';
470
- }).join(", ");
471
- return '<code class="span-function"><span class="syntax-keyword">def</span> <span class="syntax-name">' + escapeText(name || "tool") + '</span>(' + params + ')</code>';
472
- }
473
- function renderSpanContent(node) {
474
- const actionHtml = [];
475
- if (node.type === "user.message" && node.entryId) actionHtml.push('<button class="ghost fork-button" data-entry-id="' + escapeText(node.entryId) + '">Fork From Here</button>');
476
- if (node.linkedPiboSessionId) actionHtml.push('<button class="ghost session-link" data-pibo-session-id="' + escapeText(node.linkedPiboSessionId) + '">Open Child Session</button>');
477
- const actions = actionHtml.length ? '<div class="span-actions">' + actionHtml.join("") + '</div>' : "";
478
-
479
- if (node.type === "user.message") {
480
- return actions + '<div class="span-quote">' + escapeText(node.output || node.summary || "") + '</div>';
481
- }
482
- if (node.type === "assistant.message") {
483
- return actions + '<div class="span-section"><div class="span-section-title output">Structured Output</div>' + renderJson(node.output || node.summary || "") + '</div>';
484
- }
485
- if (node.type === "model.reasoning") {
486
- return actions + '<div class="reasoning-block"><span class="reasoning-comment">// Model reasoning</span>\\n' + escapeText(node.output || node.summary || "") + '</div>';
487
- }
488
- if (node.type === "tool.call" || node.type === "yielded.run") {
489
- return actions +
490
- '<div class="span-code-well">' + functionSignature(node.title, node.input || {}) + '</div>' +
491
- (node.output !== undefined ? '<div class="span-output-summary"><span>Output:</span><code>' + escapeText(short(pretty(node.output), 120)) + '</code></div>' : "") +
492
- '<div class="span-section"><div class="span-section-title">Input</div>' + renderJson(node.input || {}) + '</div>' +
493
- (node.output !== undefined ? '<div class="span-section"><div class="span-section-title output">Output</div>' + renderJson(node.output) + '</div>' : "");
494
- }
495
- if (node.type === "tool.result" || node.type === "execution.command") {
496
- return actions + '<div class="span-section"><div class="span-section-title output">Output</div>' + renderJson(node.output || node.input || "") + '</div>';
497
- }
498
- if (node.type === "agent.delegation") {
499
- return actions +
500
- '<div class="delegation-card"><div class="delegation-title">Agent Delegation · ' + escapeText(node.title || "subagent") + '</div><div class="delegation-query">' + escapeText(short(node.summary || pretty(node.input), 220)) + '</div></div>' +
501
- '<div class="span-section"><div class="span-section-title">Input</div>' + renderJson(node.input || {}) + '</div>' +
502
- (node.output !== undefined ? '<div class="span-section"><div class="span-section-title output">Output</div>' + renderJson(node.output) + '</div>' : "");
503
- }
504
- if (node.type === "agent.async") {
505
- const input = node.input && typeof node.input === "object" ? node.input : {};
506
- const startedBy = input.startedBy || "pibo_run_start";
507
- const runId = input.runId || node.runId || "";
508
- return actions +
509
- '<div class="delegation-card"><div class="delegation-title">Async Agent · ' + escapeText(node.title || "subagent") + '</div><div class="delegation-query">Started by ' + escapeText(startedBy) + (runId ? ' · ' + escapeText(runId) : "") + '</div></div>' +
510
- '<div class="span-section"><div class="span-section-title">Input</div>' + renderJson(node.input || {}) + '</div>' +
511
- (node.output !== undefined ? '<div class="span-section"><div class="span-section-title output">Run</div>' + renderJson(node.output) + '</div>' : "");
512
- }
513
- if (node.type === "error") {
514
- return actions + '<div class="span-section"><div class="span-section-title">Error</div>' + renderJson(node.error || node.output || "") + '</div>';
515
- }
516
- return actions + '<div class="span-section">' + renderJson(node.output || node.input || node.summary || "") + '</div>';
517
- }
518
-
519
- async function signIn() {
520
- const response = await fetch("/api/auth/sign-in/social", {
521
- method: "POST",
522
- headers: { "content-type": "application/json" },
523
- body: JSON.stringify({ provider: "google", callbackURL: "/apps/chat", disableRedirect: true }),
524
- });
525
- const data = await response.json();
526
- if (!response.ok || !data.url) {
527
- renderSignedOut(data.message || data.error || "Could not start Google sign in.");
528
- return;
529
- }
530
- location.href = data.url;
531
- }
532
-
533
- async function clearAuthSession() {
534
- await fetch("/api/auth/sign-out", {
535
- method: "POST",
536
- credentials: "same-origin",
537
- headers: { "content-type": "application/json" },
538
- body: "{}",
539
- });
540
- }
541
-
542
- async function signOut() {
543
- await clearAuthSession();
544
- location.reload();
545
- }
546
-
547
- function renderSignedOut(message) {
548
- userEl.replaceChildren();
549
- sessionTreeEl.innerHTML = "";
550
- contentEl.innerHTML = '<div class="placeholder">' + escapeText(message || "Sign in to start a Pibo session.") + '</div>';
551
- composer.closest(".composer-wrap").classList.add("hidden");
552
- const button = document.createElement("button");
553
- button.className = "primary";
554
- button.textContent = "Sign in with Google";
555
- button.addEventListener("click", signIn);
556
- userEl.append(button);
557
- sessionTitleEl.textContent = "Signed Out";
558
- sessionMetaEl.textContent = "";
559
- }
560
-
561
- function renderSignedIn(data) {
562
- userEl.replaceChildren();
563
- const label = document.createElement("span");
564
- label.textContent = data.identity.email || data.identity.name || data.identity.userId;
565
- userEl.append(label);
566
- const button = document.createElement("button");
567
- button.textContent = "Sign out";
568
- button.addEventListener("click", signOut);
569
- userEl.append(button);
570
- composer.closest(".composer-wrap").classList.remove("hidden");
571
- }
572
-
573
- function connectEvents() {
574
- if (eventSource) eventSource.close();
575
- if (!selectedPiboSessionId) return;
576
- eventSource = new EventSource("/api/chat/events?piboSessionId=" + encodeURIComponent(selectedPiboSessionId));
577
- eventSource.addEventListener("pibo", (event) => {
578
- const payload = JSON.parse(event.data);
579
- appendRawEvent({ type: payload.type, payload });
580
- if (payload.type !== "TEXT_MESSAGE_CONTENT" && payload.type !== "REASONING_MESSAGE_CONTENT") {
581
- void refreshTrace();
582
- }
583
- });
584
- eventSource.onerror = () => appendRawEvent({ type: "stream_error", payload: "Event stream disconnected." });
585
- }
586
-
587
- async function loadSession() {
588
- const response = await fetch("/api/chat/bootstrap");
589
- if (response.status === 401) {
590
- renderSignedOut();
591
- return;
592
- }
593
- const data = await response.json();
594
- if (response.status === 403) {
595
- await clearAuthSession().catch(() => {});
596
- renderSignedOut("Not authorized. Sign in with an allowed Google account.");
597
- return;
598
- }
599
- if (!response.ok) {
600
- renderSignedOut(data.error || "Could not load session.");
601
- return;
602
- }
603
- bootstrap = data;
604
- selectedPiboSessionId = selectedPiboSessionId || data.selectedPiboSessionId;
605
- ensureSelectedAgentProfile();
606
- renderSignedIn(data);
607
- renderArea();
608
- connectEvents();
609
- }
610
-
611
- function ensureSelectedAgentProfile() {
612
- const agents = (bootstrap && bootstrap.agents) || [];
613
- if (!agents.length) return "";
614
- const exists = agents.some(function(agent) {
615
- return agent.name === selectedAgentProfile || (agent.aliases || []).includes(selectedAgentProfile);
616
- });
617
- if (!selectedAgentProfile || !exists) {
618
- selectedAgentProfile = bootstrap.session && bootstrap.session.profile || agents[0].name;
619
- }
620
- localStorage.setItem("pibo.chat.newSessionProfile", selectedAgentProfile);
621
- return selectedAgentProfile;
622
- }
623
-
624
- function renderArea() {
625
- document.querySelectorAll(".tab").forEach(function(tab) {
626
- tab.classList.toggle("active", tab.dataset.area === area);
627
- });
628
- document.querySelector("#sidebar-title").textContent = area[0].toUpperCase() + area.slice(1);
629
- newSessionButton.classList.toggle("hidden", area !== "sessions");
630
- if (area === "sessions") {
631
- renderSessions();
632
- void refreshTrace();
633
- return;
634
- }
635
- if (area === "agents") {
636
- renderAgents();
637
- return;
638
- }
639
- renderSettings();
640
- }
641
- function renderSessions() {
642
- sessionTreeEl.innerHTML = "";
643
- (bootstrap.sessions || []).forEach(function(node) { sessionTreeEl.append(renderSessionNode(node, 0)); });
644
- }
645
- function renderSessionNode(node, depth) {
646
- const children = node.children || [];
647
- const hasChildren = children.length > 0;
648
- if (sessionTreeHasSession(children, selectedPiboSessionId)) {
649
- openSessionNodes.add(node.piboSessionId);
650
- }
651
- const expanded = openSessionNodes.has(node.piboSessionId);
652
- const wrap = document.createElement("div");
653
- const row = document.createElement("div");
654
- row.className = "session-row" + (node.piboSessionId === selectedPiboSessionId ? " active" : "");
655
- row.style.paddingLeft = 8 + depth * 14 + "px";
656
- row.title = (node.title || "Untitled Session") + "\\n" + node.piboSessionId;
657
- if (hasChildren) {
658
- const toggle = document.createElement("button");
659
- toggle.type = "button";
660
- toggle.className = "session-toggle";
661
- toggle.textContent = expanded ? "v" : ">";
662
- toggle.setAttribute("aria-expanded", String(expanded));
663
- toggle.setAttribute("aria-label", expanded ? "Collapse Subsessions" : "Expand Subsessions");
664
- toggle.title = expanded ? "Collapse Subsessions" : "Expand Subsessions";
665
- toggle.addEventListener("click", function() {
666
- if (openSessionNodes.has(node.piboSessionId)) openSessionNodes.delete(node.piboSessionId);
667
- else openSessionNodes.add(node.piboSessionId);
668
- renderSessions();
669
- });
670
- row.append(toggle);
671
- } else {
672
- row.append(document.createElement("span"));
673
- }
674
- const select = document.createElement("button");
675
- select.type = "button";
676
- select.className = "session-select";
677
- select.innerHTML =
678
- '<span><span class="session-title">' + escapeText(node.title) + '</span><span class="session-id">' + escapeText(node.piboSessionId) + '</span></span>' +
679
- '<span class="status-dot ' + escapeText(node.status) + '"></span>';
680
- select.addEventListener("click", function() { selectSession(node.piboSessionId); });
681
- row.append(select);
682
- wrap.append(row);
683
- if (expanded) children.forEach(function(child) { wrap.append(renderSessionNode(child, depth + 1)); });
684
- return wrap;
685
- }
686
- function sessionTreeHasSession(nodes, piboSessionId) {
687
- return (nodes || []).some(function(node) {
688
- return node.piboSessionId === piboSessionId || sessionTreeHasSession(node.children, piboSessionId);
689
- });
690
- }
691
- async function selectSession(piboSessionId) {
692
- selectedPiboSessionId = piboSessionId;
693
- connectEvents();
694
- await refreshBootstrap(false);
695
- renderArea();
696
- }
697
- async function createSession(profile) {
698
- const selectedProfile = profile || ensureSelectedAgentProfile();
699
- const created = await postJson("/api/chat/sessions", selectedProfile ? { profile: selectedProfile } : {});
700
- selectedPiboSessionId = created.session && created.session.id;
701
- connectEvents();
702
- await refreshBootstrap(false);
703
- renderArea();
704
- }
705
- async function refreshBootstrap(keepTrace) {
706
- const response = await fetch("/api/chat/bootstrap?piboSessionId=" + encodeURIComponent(selectedPiboSessionId || ""));
707
- if (!response.ok) return;
708
- bootstrap = await response.json();
709
- selectedPiboSessionId = bootstrap.selectedPiboSessionId;
710
- if (!keepTrace) renderSessions();
711
- }
712
- async function refreshTrace() {
713
- if (!selectedPiboSessionId) return;
714
- const response = await fetch("/api/chat/trace?piboSessionId=" + encodeURIComponent(selectedPiboSessionId) + "&includeRawEvents=true&rawEventsLimit=80");
715
- if (!response.ok) return;
716
- const trace = await response.json();
717
- sessionTitleEl.textContent = trace.title || selectedPiboSessionId;
718
- sessionMetaEl.textContent = trace.piboSessionId + " · " + trace.piSessionId;
719
- renderTrace(trace.nodes || []);
720
- renderRawEvents(trace.rawEvents || []);
721
- }
722
- function renderTrace(nodes) {
723
- const visible = nodes.filter(function(node) { return showThinking || node.type !== "model.reasoning"; });
724
- if (!visible.length) {
725
- contentEl.innerHTML = '<div class="placeholder">No trace events yet.</div>';
726
- return;
727
- }
728
- const list = document.createElement("div");
729
- list.className = "trace-list";
730
- visible.forEach(function(node) { list.append(renderTraceNode(node, 0)); });
731
- contentEl.replaceChildren(list);
732
- }
733
- function renderTraceNode(node, depth) {
734
- const wrapper = document.createElement("article");
735
- const mappedSpanType = spanType(node.type);
736
- wrapper.className = "span-card " + spanStatusClass(node.status) + (openNodes.has(node.id) ? " open" : "");
737
- wrapper.dataset.spanType = mappedSpanType;
738
- wrapper.dataset.nodeId = node.id;
739
- wrapper.style.marginLeft = Math.min(depth * 12, 96) + "px";
740
- const header = document.createElement("button");
741
- header.className = "span-header";
742
- const statusClass = node.status === "running" ? "running" : node.status === "error" ? "error" : "";
743
- header.innerHTML =
744
- '<span class="span-chevron">' + (openNodes.has(node.id) ? "v" : ">") + '</span>' +
745
- '<span class="span-icon">' + escapeText(nodeIcon(node.type)) + '</span>' +
746
- '<span><span class="span-type-label">' + escapeText(spanLabel(node.type)) + '</span><span class="span-title">' + escapeText(node.title || "") + (node.summary ? " · " + escapeText(short(node.summary, 160)) : "") + '</span></span>' +
747
- '<span class="span-meta"><span class="badge ' + statusClass + '">' + escapeText(node.status) + '</span></span>';
748
- header.addEventListener("click", function() {
749
- if (openNodes.has(node.id)) openNodes.delete(node.id); else openNodes.add(node.id);
750
- saveOpenNodes();
751
- wrapper.classList.toggle("open");
752
- header.querySelector(".span-chevron").textContent = wrapper.classList.contains("open") ? "v" : ">";
753
- });
754
- wrapper.append(header);
755
- const body = document.createElement("div");
756
- body.className = "span-body";
757
- body.innerHTML = renderSpanContent(node);
758
- body.querySelectorAll(".fork-button").forEach(function(button) {
759
- button.addEventListener("click", function(event) {
760
- event.stopPropagation();
761
- void forkFrom(button.dataset.entryId);
762
- });
763
- });
764
- body.querySelectorAll(".session-link").forEach(function(button) {
765
- button.addEventListener("click", function(event) {
766
- event.stopPropagation();
767
- void selectSession(button.dataset.piboSessionId);
768
- });
769
- });
770
- if (node.children && node.children.length) {
771
- const childWrap = document.createElement("div");
772
- childWrap.className = "children span-children";
773
- node.children.filter(function(child) { return showThinking || child.type !== "model.reasoning"; }).forEach(function(child) {
774
- childWrap.append(renderTraceNode(child, depth + 1));
775
- });
776
- body.append(childWrap);
777
- }
778
- wrapper.append(body);
779
- return wrapper;
780
- }
781
- function appendRawEvent(event) {
782
- const item = document.createElement("div");
783
- item.className = "raw-event";
784
- item.innerHTML = '<div class="raw-event-type">' + escapeText(event.type) + '</div><pre class="payload">' + escapeText(pretty(event.payload)) + '</pre>';
785
- rawLogEl.prepend(item);
786
- }
787
- function renderRawEvents(events) {
788
- rawLogEl.innerHTML = "";
789
- events.slice(-80).reverse().forEach(appendRawEvent);
790
- }
791
- function renderAgents() {
792
- sessionTitleEl.textContent = "Agents";
793
- sessionMetaEl.textContent = "Profile selection";
794
- const agents = (bootstrap && bootstrap.agents) || [];
795
- ensureSelectedAgentProfile();
796
- sessionTreeEl.innerHTML = agents.map(function(agent) {
797
- const active = agent.name === selectedAgentProfile ? " active" : "";
798
- return '<button class="session-row' + active + '" data-profile="' + escapeText(agent.name) + '"><span></span><span><span class="session-title">' + escapeText(agent.name) + '</span><span class="session-id">' + escapeText((agent.aliases || []).join(", ") || "profile") + '</span></span><span class="status-dot"></span></button>';
799
- }).join("");
800
- sessionTreeEl.querySelectorAll("[data-profile]").forEach(function(button) {
801
- button.addEventListener("click", function() {
802
- selectedAgentProfile = button.dataset.profile;
803
- localStorage.setItem("pibo.chat.newSessionProfile", selectedAgentProfile);
804
- renderAgents();
805
- });
806
- });
807
- contentEl.innerHTML = '<div class="trace-list">' + agents.map(function(agent) {
808
- const selected = agent.name === selectedAgentProfile ? "selected" : "available";
809
- return '<article class="trace-node open" data-type="agent.turn"><div class="trace-header"><span class="trace-icon">A</span><span><span class="trace-label">' + escapeText(agent.name) + '</span><span class="trace-summary">' + escapeText(agent.description || "No description") + '</span></span><span class="trace-actions"><button class="ghost profile-select" data-profile="' + escapeText(agent.name) + '">' + selected + '</button><button class="ghost profile-create" data-profile="' + escapeText(agent.name) + '">New Session</button></span></div><div class="trace-body"><pre class="payload">' + escapeText(pretty(agent)) + '</pre></div></article>';
810
- }).join("") + '</div>';
811
- contentEl.querySelectorAll(".profile-select").forEach(function(button) {
812
- button.addEventListener("click", function() {
813
- selectedAgentProfile = button.dataset.profile;
814
- localStorage.setItem("pibo.chat.newSessionProfile", selectedAgentProfile);
815
- renderAgents();
816
- });
817
- });
818
- contentEl.querySelectorAll(".profile-create").forEach(function(button) {
819
- button.addEventListener("click", function() {
820
- selectedAgentProfile = button.dataset.profile;
821
- localStorage.setItem("pibo.chat.newSessionProfile", selectedAgentProfile);
822
- void createSession(selectedAgentProfile);
823
- });
824
- });
825
- }
826
- function renderSettings() {
827
- sessionTitleEl.textContent = "Settings";
828
- sessionMetaEl.textContent = "Browser-local V1 settings";
829
- sessionTreeEl.innerHTML = '<div class="placeholder">Settings navigation placeholder.</div>';
830
- contentEl.innerHTML = '<div class="placeholder">Thinking display and trace expansion state are stored in this browser.</div>';
831
- }
832
- function availableCommands() {
833
- const actions = (bootstrap && bootstrap.capabilities && bootstrap.capabilities.actions) || [];
834
- const commands = [];
835
- actions.forEach(function(action) {
836
- (action.slashCommands || []).forEach(function(command) {
837
- if (command !== "tree") commands.push({ slash: "/" + command, action: action.name, description: action.description || action.name });
838
- });
839
- });
840
- commands.push({ slash: "/thinking-show", action: "thinking-show", description: "Toggle historical thinking display in this browser." });
841
- return commands;
842
- }
843
- function renderCommandMenu() {
844
- const query = messageInput.value.trim();
845
- if (!query.startsWith("/")) { commandMenu.classList.add("hidden"); return; }
846
- const commands = availableCommands().filter(function(command) { return command.slash.startsWith(query.split(/\\s+/)[0]); });
847
- if (!commands.length) { commandMenu.classList.add("hidden"); return; }
848
- commandIndex = Math.min(commandIndex, commands.length - 1);
849
- commandMenu.innerHTML = "";
850
- commands.forEach(function(command, index) {
851
- const item = document.createElement("button");
852
- item.type = "button";
853
- item.className = "command-item" + (index === commandIndex ? " active" : "");
854
- item.innerHTML = '<span class="command-name">' + escapeText(command.slash) + '</span><span class="command-desc">' + escapeText(command.description) + '</span>';
855
- item.addEventListener("click", function() { void runCommand(command); });
856
- commandMenu.append(item);
857
- });
858
- commandMenu.classList.remove("hidden");
859
- }
860
- async function runCommand(command) {
861
- commandMenu.classList.add("hidden");
862
- const text = messageInput.value.trim();
863
- messageInput.value = "";
864
- if (command.action === "thinking-show") {
865
- showThinking = !showThinking;
866
- localStorage.setItem("pibo.chat.showThinking", String(showThinking));
867
- updateThinkingButton();
868
- await refreshTrace();
869
- return;
870
- }
871
- const commandArgs = text.slice(command.slash.length).trim();
872
- const params = command.action === "thinking" && commandArgs
873
- ? { level: commandArgs.split(/\\s+/, 1)[0] }
874
- : command.action === "compact" && commandArgs
875
- ? { customInstructions: commandArgs }
876
- : undefined;
877
- const result = await postJson("/api/chat/action", { piboSessionId: selectedPiboSessionId, action: command.action, params: params });
878
- appendRawEvent({ type: "command_result", payload: result });
879
- if (command.action === "session.clone" && result && result.result && result.result.piboSessionId) {
880
- await selectSession(result.result.piboSessionId);
881
- }
882
- }
883
- async function forkFrom(entryId) {
884
- const result = await postJson("/api/chat/action", { piboSessionId: selectedPiboSessionId, action: "session.fork", params: { entryId: entryId } });
885
- appendRawEvent({ type: "fork_result", payload: result });
886
- pendingForkResult = result && result.result ? result.result : undefined;
887
- const selectedText = pendingForkResult && typeof pendingForkResult.selectedText === "string" ? pendingForkResult.selectedText : undefined;
888
- if (pendingForkResult && pendingForkResult.piboSessionId) {
889
- await selectSession(pendingForkResult.piboSessionId);
890
- } else {
891
- await refreshBootstrap(false);
892
- await refreshTrace();
893
- }
894
- if (selectedText !== undefined) {
895
- messageInput.value = selectedText;
896
- messageInput.focus();
897
- }
898
- }
899
- async function postJson(url, payload) {
900
- const response = await fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) });
901
- const data = await response.json().catch(function() { return { error: "Request failed" }; });
902
- if (!response.ok) throw new Error(data.error || "Request failed");
903
- return data;
904
- }
905
-
906
- newSessionButton.addEventListener("click", function() { void createSession(); });
907
- composer.addEventListener("submit", async (event) => {
908
- event.preventDefault();
909
- const text = messageInput.value.trim();
910
- if (!text) return;
911
- const command = availableCommands().find(function(candidate) { return text.split(/\\s+/)[0] === candidate.slash; });
912
- if (text.startsWith("/") && command) {
913
- await runCommand(command);
914
- return;
915
- }
916
- messageInput.value = "";
917
- await postJson("/api/chat/message", { piboSessionId: selectedPiboSessionId, text: text });
918
- await refreshTrace();
919
- });
920
- messageInput.addEventListener("input", renderCommandMenu);
921
- messageInput.addEventListener("keydown", function(event) {
922
- if (commandMenu.classList.contains("hidden")) {
923
- if (event.key === "Enter" && !event.shiftKey) {
924
- event.preventDefault();
925
- composer.requestSubmit();
926
- }
927
- return;
928
- }
929
- const count = commandMenu.querySelectorAll(".command-item").length;
930
- if (event.key === "ArrowDown") { event.preventDefault(); commandIndex = (commandIndex + 1) % count; renderCommandMenu(); }
931
- if (event.key === "ArrowUp") { event.preventDefault(); commandIndex = (commandIndex - 1 + count) % count; renderCommandMenu(); }
932
- if (event.key === "Enter" && !event.shiftKey) {
933
- const item = commandMenu.querySelectorAll(".command-item")[commandIndex];
934
- if (item) { event.preventDefault(); item.click(); }
935
- }
936
- });
937
- document.querySelectorAll(".tab").forEach(function(tab) {
938
- tab.addEventListener("click", function() { area = tab.dataset.area; renderArea(); });
939
- });
940
- document.querySelector("#refresh").addEventListener("click", function() { void refreshBootstrap(false).then(renderArea); });
941
- document.querySelector("#clear-local").addEventListener("click", function() { rawLogEl.innerHTML = ""; });
942
- document.querySelector("#collapse-all").addEventListener("click", function() { openNodes.clear(); saveOpenNodes(); void refreshTrace(); });
943
- document.querySelector("#expand-all").addEventListener("click", function() {
944
- contentEl.querySelectorAll(".span-card").forEach(function(node) {
945
- if (node.dataset.nodeId) openNodes.add(node.dataset.nodeId);
946
- node.classList.add("open");
947
- const chevron = node.querySelector(".span-chevron");
948
- if (chevron) chevron.textContent = "v";
949
- });
950
- saveOpenNodes();
951
- });
952
- document.querySelector("#expand-one").addEventListener("click", function() {
953
- openNodes.clear();
954
- contentEl.querySelectorAll(".trace-list > .span-card").forEach(function(node) {
955
- if (node.dataset.nodeId) openNodes.add(node.dataset.nodeId);
956
- node.classList.add("open");
957
- const chevron = node.querySelector(".span-chevron");
958
- if (chevron) chevron.textContent = "v";
959
- });
960
- saveOpenNodes();
961
- });
962
- function updateThinkingButton() {
963
- document.querySelector("#thinking-toggle").textContent = showThinking ? "Thinking On" : "Thinking Off";
964
- }
965
- document.querySelector("#thinking-toggle").addEventListener("click", async function() {
966
- showThinking = !showThinking;
967
- localStorage.setItem("pibo.chat.showThinking", String(showThinking));
968
- updateThinkingButton();
969
- await refreshTrace();
970
- });
971
- document.querySelector("#fork-stay").addEventListener("click", function() {
972
- forkModal.classList.add("hidden");
973
- const previousFile = pendingForkResult && pendingForkResult.previous && pendingForkResult.previous.sessionFile;
974
- const targetPiboSessionId = pendingForkResult && pendingForkResult.piboSessionId ? pendingForkResult.piboSessionId : selectedPiboSessionId;
975
- pendingForkResult = undefined;
976
- messageInput.value = pendingForkComposerText || "";
977
- pendingForkComposerText = undefined;
978
- if (previousFile) {
979
- void postJson("/api/chat/action", { piboSessionId: targetPiboSessionId, action: "session.switch", params: { sessionFile: previousFile } })
980
- .then(function(result) { appendRawEvent({ type: "fork_restore", payload: result }); return refreshTrace(); })
981
- .catch(function(error) { appendRawEvent({ type: "fork_restore_error", payload: String(error) }); });
982
- }
983
- });
984
- document.querySelector("#fork-switch").addEventListener("click", async function() {
985
- forkModal.classList.add("hidden");
986
- const targetPiboSessionId = pendingForkResult && pendingForkResult.piboSessionId ? pendingForkResult.piboSessionId : selectedPiboSessionId;
987
- pendingForkResult = undefined;
988
- pendingForkComposerText = undefined;
989
- if (targetPiboSessionId) await selectSession(targetPiboSessionId);
990
- });
991
-
992
- updateThinkingButton();
993
- loadSession();
994
- </script>
995
- </body>
142
+ return `<!doctype html>
143
+ <html lang="de">
144
+ <head>
145
+ <meta charset="utf-8">
146
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
147
+ <meta name="theme-color" content="#101d22">
148
+ <meta name="apple-mobile-web-app-capable" content="yes">
149
+ <meta name="apple-mobile-web-app-title" content="Pibo Chat">
150
+ <link rel="manifest" href="/apps/chat/manifest.webmanifest">
151
+ <link rel="apple-touch-icon" href="/apps/chat/assets/pwa-images/ios/180.png">
152
+ <title>Pibo Web Chat</title>
153
+ <style>
154
+ :root { color-scheme: dark; font-family: "Public Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #101d22; color: #d8e3e7; }
155
+ * { box-sizing: border-box; }
156
+ body { margin: 0; height: 100vh; overflow: hidden; background: #101d22; }
157
+ button, textarea { font: inherit; }
158
+ button { border: 1px solid #334155; background: #151f24; color: #d8e3e7; border-radius: 3px; padding: 7px 10px; cursor: pointer; }
159
+ button:hover { border-color: #11a4d4; color: #ffffff; }
160
+ button.primary { background: #11a4d4; border-color: #11a4d4; color: #ffffff; }
161
+ button.ghost { background: transparent; }
162
+ .app { display: grid; grid-template-rows: 56px 1fr; height: 100vh; }
163
+ .topbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 0 16px; background: #1a262b; border-bottom: 1px solid #1e293b; }
164
+ .brand { display: flex; align-items: center; gap: 8px; font-size: 17px; font-weight: 800; letter-spacing: .08em; color: #e7f7fb; text-transform: uppercase; white-space: nowrap; }
165
+ .fallback-badge { border: 1px solid #f59e0b; border-radius: 999px; padding: 2px 6px; color: #fbbf24; font-size: 9px; letter-spacing: .06em; }
166
+ .tabs { display: flex; gap: 6px; }
167
+ .tab { height: 32px; text-transform: uppercase; font-size: 12px; letter-spacing: .06em; }
168
+ .tab.active { border-color: #11a4d4; color: #11a4d4; background: rgba(17,164,212,.08); }
169
+ .userbar { display: flex; align-items: center; gap: 8px; min-width: 0; color: #94a3b8; font-size: 12px; }
170
+ .workspace { display: grid; grid-template-columns: 300px minmax(0,1fr) 320px; min-height: 0; }
171
+ .sidebar, .inspector { background: #1a262b; border-right: 1px solid #1e293b; min-height: 0; overflow: auto; }
172
+ .inspector { border-right: 0; border-left: 1px solid #1e293b; background: #0e1116; }
173
+ .panel-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 42px; padding: 10px 12px; border-bottom: 1px solid #1e293b; color: #cbd5e1; font-size: 12px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
174
+ .panel-actions { display: flex; align-items: center; gap: 6px; }
175
+ .icon-button { width: 28px; height: 28px; padding: 0; display: grid; place-items: center; line-height: 1; }
176
+ .session-tree { padding: 8px; }
177
+ .session-row { width: 100%; display: grid; grid-template-columns: 28px minmax(0,1fr); align-items: center; margin-bottom: 4px; border: 1px solid transparent; border-radius: 3px; background: transparent; padding: 0 8px 0 0; }
178
+ .session-row.active { border-color: #11a4d4; background: rgba(17,164,212,.10); }
179
+ .session-toggle, .session-select { border: 0; background: transparent; border-radius: 0; padding: 0; }
180
+ .session-toggle { width: 28px; height: 32px; display: grid; place-items: center; color: #64748b; }
181
+ .session-toggle:hover { color: #11a4d4; border-color: transparent; }
182
+ .session-select { min-width: 0; display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 7px; align-items: center; text-align: left; padding: 7px 0; }
183
+ .session-select:hover { border-color: transparent; }
184
+ .session-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; color: #e2e8f0; }
185
+ .session-id { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; color: #64748b; }
186
+ .status-dot { width: 7px; height: 7px; border-radius: 999px; background: #64748b; }
187
+ .status-dot.running { background: #0bda57; box-shadow: 0 0 10px rgba(11,218,87,.35); }
188
+ .status-dot.error { background: #ef4444; }
189
+ .main { min-height: 0; display: grid; grid-template-rows: auto 1fr auto; background: #101d22; }
190
+ .session-head { min-height: 58px; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-bottom: 1px solid #1e293b; background: #151f24; }
191
+ .session-name { margin: 0; font-size: 16px; line-height: 1.2; }
192
+ .session-meta { margin-top: 4px; color: #64748b; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; }
193
+ .trace-controls { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end; }
194
+ .content { min-height: 0; overflow: auto; padding: 14px; }
195
+ .trace-list { min-width: 620px; display: flex; flex-direction: column; gap: 9px; }
196
+ /* Ported from pydantic-tracing SpanNode/JsonRenderer visual structure. */
197
+ .span-card { border: 1px solid #334155; border-radius: 3px; background: #1a262b; overflow: hidden; }
198
+ .span-card.ok { border-color: rgba(11,218,87,.30); }
199
+ .span-card.running { border-color: rgba(17,164,212,.50); box-shadow: 0 0 10px rgba(17,164,212,.10); }
200
+ .span-card.error { border-color: rgba(255,107,0,.50); }
201
+ .span-card[data-span-type="tool.call"] { border-color: rgba(168,85,247,.52); }
202
+ .span-card[data-span-type="tool.result"] { border-color: rgba(34,197,94,.42); }
203
+ .span-card[data-span-type="model.reasoning"] { border-color: rgba(245,158,11,.48); }
204
+ .span-card[data-span-type="agent.delegation"] { border-color: rgba(249,115,22,.58); }
205
+ .span-card[data-span-type="user.prompt"] { border-color: rgba(6,182,212,.48); }
206
+ .span-header { width: 100%; display: grid; grid-template-columns: 18px 22px minmax(0,1fr) auto; align-items: center; gap: 8px; padding: 9px 10px; background: #151f24; border: 0; text-align: left; }
207
+ .span-card.ok > .span-header { background: rgba(11,218,87,.05); }
208
+ .span-card.running > .span-header { background: rgba(17,164,212,.05); }
209
+ .span-card.error > .span-header { background: rgba(255,107,0,.05); }
210
+ .span-chevron { color: #64748b; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
211
+ .span-icon { width: 18px; height: 18px; display: grid; place-items: center; border-radius: 999px; background: rgba(17,164,212,.20); color: #11a4d4; font: 11px ui-monospace, SFMono-Regular, Menlo, monospace; }
212
+ .span-card[data-span-type="tool.call"] .span-icon { background: rgba(168,85,247,.20); color: #a855f7; }
213
+ .span-card[data-span-type="tool.result"] .span-icon { background: rgba(34,197,94,.20); color: #22c55e; }
214
+ .span-card[data-span-type="model.reasoning"] .span-icon { background: rgba(245,158,11,.20); color: #f59e0b; }
215
+ .span-card[data-span-type="agent.delegation"] .span-icon { background: rgba(249,115,22,.20); color: #f97316; }
216
+ .span-card[data-span-type="user.prompt"] .span-icon { background: rgba(6,182,212,.20); color: #06b6d4; }
217
+ .span-type-label { color: #cbd5e1; font-size: 11px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase; }
218
+ .span-title { color: #94a3b8; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
219
+ .span-meta { display: flex; align-items: center; gap: 6px; }
220
+ .span-body { display: none; border-top: 1px solid #1e293b; }
221
+ .span-card.open > .span-body { display: block; }
222
+ .span-actions { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px 10px; border-bottom: 1px solid #1e293b; background: rgba(15,23,42,.18); }
223
+ .span-quote { padding: 14px; color: #cbd5e1; font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; }
224
+ .span-code-well { padding: 12px 14px; background: #0e1116; border-bottom: 1px solid #1e293b; overflow: auto; }
225
+ .span-function { font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; color: #22c55e; white-space: pre-wrap; }
226
+ .syntax-keyword { color: #c084fc; }
227
+ .syntax-name { color: #fde047; }
228
+ .syntax-key { color: #60a5fa; }
229
+ .syntax-string { color: #86efac; }
230
+ .span-output-summary { display: flex; align-items: center; gap: 8px; min-width: 0; padding: 8px 14px; background: rgba(15,23,42,.28); border-bottom: 1px solid #1e293b; color: #94a3b8; font-size: 12px; }
231
+ .span-output-summary code { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #cbd5e1; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
232
+ .span-section { padding: 12px 14px; background: #0e1116; border-top: 1px solid rgba(51,65,85,.55); }
233
+ .span-section-title { margin-bottom: 7px; color: #60a5fa; font-size: 10px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
234
+ .span-section-title.output { color: #22c55e; }
235
+ .reasoning-block { padding: 14px; background: rgba(245,158,11,.06); color: #cbd5e1; font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; }
236
+ .reasoning-comment { color: rgba(245,158,11,.75); }
237
+ .delegation-card { padding: 14px; background: rgba(249,115,22,.06); border-bottom: 1px solid rgba(249,115,22,.20); }
238
+ .delegation-title { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; color: #f97316; font-size: 13px; font-weight: 700; }
239
+ .delegation-query { color: #94a3b8; font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; }
240
+ .json-renderer { margin: 0; max-height: 24rem; overflow: auto; white-space: pre-wrap; color: #dbeafe; font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; }
241
+ .trace-node { border: 1px solid #334155; border-radius: 3px; background: #1a262b; overflow: hidden; }
242
+ .trace-node[data-type="user.message"] { border-color: rgba(6,182,212,.42); }
243
+ .trace-node[data-type="assistant.message"] { border-color: rgba(34,197,94,.35); }
244
+ .trace-node[data-type="model.reasoning"] { border-color: rgba(245,158,11,.45); }
245
+ .trace-node[data-type="tool.call"] { border-color: rgba(168,85,247,.45); }
246
+ .trace-node[data-type="agent.delegation"] { border-color: rgba(249,115,22,.55); }
247
+ .trace-node[data-type="agent.async"] { border-color: rgba(249,115,22,.55); }
248
+ .trace-node[data-type="execution.command"] { border-color: rgba(17,164,212,.42); }
249
+ .trace-node[data-type="error"] { border-color: rgba(239,68,68,.65); }
250
+ .trace-header { width: 100%; display: grid; grid-template-columns: 22px 1fr auto; align-items: center; gap: 8px; padding: 9px 10px; background: #151f24; border: 0; text-align: left; }
251
+ .trace-icon { width: 18px; height: 18px; display: grid; place-items: center; border-radius: 999px; background: rgba(17,164,212,.14); color: #11a4d4; font-size: 11px; }
252
+ .trace-label { color: #cbd5e1; font-size: 11px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase; }
253
+ .trace-summary { margin-top: 3px; color: #94a3b8; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
254
+ .trace-actions { display: flex; align-items: center; gap: 6px; }
255
+ .badge { border: 1px solid #334155; border-radius: 999px; padding: 2px 6px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; color: #94a3b8; }
256
+ .badge.running { color: #0bda57; border-color: rgba(11,218,87,.35); }
257
+ .badge.error { color: #ef4444; border-color: rgba(239,68,68,.45); }
258
+ .trace-body { display: none; padding: 10px; border-top: 1px solid #1e293b; }
259
+ .trace-node.open > .trace-body { display: block; }
260
+ .payload { margin: 0; white-space: pre-wrap; overflow: auto; max-height: 360px; padding: 10px; border: 1px solid #1e293b; border-radius: 3px; background: #0e1116; color: #dbeafe; font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; }
261
+ .children { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; padding-left: 12px; border-left: 1px solid #334155; }
262
+ .composer-wrap { position: relative; padding: 10px 12px; border-top: 1px solid #1e293b; background: #151f24; }
263
+ .composer { display: grid; grid-template-columns: 1fr auto; gap: 8px; }
264
+ textarea { width: 100%; min-height: 48px; max-height: 150px; resize: vertical; border: 1px solid #334155; border-radius: 3px; padding: 10px; background: #0e1116; color: #e2e8f0; }
265
+ .command-menu { position: absolute; left: 12px; bottom: 74px; width: min(520px, calc(100% - 24px)); max-height: 280px; overflow: auto; background: #0e1116; border: 1px solid #11a4d4; border-radius: 3px; box-shadow: 0 12px 30px rgba(0,0,0,.35); z-index: 20; }
266
+ .command-item { display: grid; grid-template-columns: 120px 1fr; gap: 10px; width: 100%; padding: 9px 10px; border: 0; border-bottom: 1px solid #1e293b; text-align: left; background: transparent; }
267
+ .command-item.active { background: rgba(17,164,212,.13); }
268
+ .command-name { color: #11a4d4; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
269
+ .command-desc { color: #94a3b8; font-size: 12px; }
270
+ .inspector-log { padding: 10px; display: flex; flex-direction: column; gap: 8px; }
271
+ .raw-event { border-left: 2px solid #11a4d4; padding: 8px; background: #151f24; }
272
+ .raw-event-type { color: #11a4d4; font: 11px ui-monospace, SFMono-Regular, Menlo, monospace; margin-bottom: 5px; }
273
+ .placeholder { padding: 18px; color: #94a3b8; }
274
+ .modal-backdrop { position: fixed; inset: 0; display: grid; place-items: center; background: rgba(0,0,0,.62); z-index: 50; }
275
+ .modal { width: min(420px, calc(100vw - 32px)); border: 1px solid #334155; border-radius: 4px; background: #1a262b; padding: 16px; }
276
+ .modal h2 { margin: 0 0 8px; font-size: 15px; }
277
+ .modal p { margin: 0 0 14px; color: #94a3b8; font-size: 13px; }
278
+ .modal-actions { display: flex; justify-content: flex-end; gap: 8px; }
279
+ .hidden { display: none !important; }
280
+ @media (max-width: 980px) {
281
+ .workspace { grid-template-columns: 240px minmax(0,1fr); }
282
+ .inspector { display: none; }
283
+ }
284
+ @media (max-width: 720px) {
285
+ body { overflow: auto; }
286
+ .app { height: auto; min-height: 100vh; }
287
+ .topbar { flex-wrap: wrap; height: auto; min-height: 56px; padding: 10px; }
288
+ .workspace { grid-template-columns: 1fr; }
289
+ .sidebar { max-height: 260px; }
290
+ .main { min-height: 70vh; }
291
+ .trace-list { min-width: 0; }
292
+ .composer { grid-template-columns: 1fr; }
293
+ }
294
+ </style>
295
+ </head>
296
+ <body>
297
+ <div class="app">
298
+ <header class="topbar">
299
+ <div class="brand">Pibo Chat <span class="fallback-badge" title="Built React Chat assets are unavailable">Fallback UI</span></div>
300
+ <nav class="tabs">
301
+ <button class="tab active" data-area="sessions">Sessions</button>
302
+ <button class="tab" data-area="agents">Agents</button>
303
+ <button class="tab" data-area="settings">Settings</button>
304
+ </nav>
305
+ <div class="userbar" id="user"></div>
306
+ </header>
307
+ <div class="workspace">
308
+ <aside class="sidebar">
309
+ <div class="panel-head">
310
+ <span id="sidebar-title">Sessions</span>
311
+ <div class="panel-actions">
312
+ <button class="ghost icon-button" id="new-session" title="New Session" aria-label="New Session">+</button>
313
+ <button class="ghost" id="refresh">Refresh</button>
314
+ </div>
315
+ </div>
316
+ <div class="session-tree" id="session-tree"></div>
317
+ </aside>
318
+ <main class="main">
319
+ <div class="session-head">
320
+ <div>
321
+ <h1 class="session-name" id="session-title">Loading</h1>
322
+ <div class="session-meta" id="session-meta"></div>
323
+ </div>
324
+ <div class="trace-controls">
325
+ <button class="ghost" id="thinking-toggle">Thinking Off</button>
326
+ <button class="ghost" id="collapse-all">Collapse All</button>
327
+ <button class="ghost" id="expand-one">Depth 1</button>
328
+ <button class="ghost" id="expand-all">Expand All</button>
329
+ </div>
330
+ </div>
331
+ <section class="content" id="content"></section>
332
+ <div class="composer-wrap" id="composer-wrap">
333
+ <div class="command-menu hidden" id="command-menu"></div>
334
+ <form class="composer" id="composer">
335
+ <textarea id="message" name="message" placeholder="Send Message (/ for commands or $ for skills)"></textarea>
336
+ <button class="primary" type="submit">Send</button>
337
+ </form>
338
+ </div>
339
+ </main>
340
+ <aside class="inspector">
341
+ <div class="panel-head"><span>Raw Events</span><button class="ghost" id="clear-local">Clear UI</button></div>
342
+ <div class="inspector-log" id="raw-log"></div>
343
+ </aside>
344
+ </div>
345
+ </div>
346
+ <div class="modal-backdrop hidden" id="fork-modal">
347
+ <div class="modal">
348
+ <h2>Zur geforkten Session wechseln?</h2>
349
+ <p>Der Fork wurde erstellt. Wenn du wechselst, wird die neue Session geladen.</p>
350
+ <div class="modal-actions">
351
+ <button id="fork-stay">Nein</button>
352
+ <button class="primary" id="fork-switch">Ja</button>
353
+ </div>
354
+ </div>
355
+ </div>
356
+ <script>
357
+ const userEl = document.querySelector("#user");
358
+ const sessionTreeEl = document.querySelector("#session-tree");
359
+ const sessionTitleEl = document.querySelector("#session-title");
360
+ const sessionMetaEl = document.querySelector("#session-meta");
361
+ const contentEl = document.querySelector("#content");
362
+ const rawLogEl = document.querySelector("#raw-log");
363
+ const composer = document.querySelector("#composer");
364
+ const messageInput = document.querySelector("#message");
365
+ const commandMenu = document.querySelector("#command-menu");
366
+ const forkModal = document.querySelector("#fork-modal");
367
+ const newSessionButton = document.querySelector("#new-session");
368
+ let eventSource;
369
+ let bootstrap;
370
+ let selectedPiboSessionId;
371
+ let area = "sessions";
372
+ let pendingForkResult;
373
+ let pendingForkComposerText;
374
+ let showThinking = localStorage.getItem("pibo.chat.showThinking") === "true";
375
+ let selectedAgentProfile = localStorage.getItem("pibo.chat.newSessionProfile") || "";
376
+ let openNodes = new Set(JSON.parse(localStorage.getItem("pibo.chat.openNodes") || "[]"));
377
+ let openSessionNodes = new Set();
378
+ let commandIndex = 0;
379
+
380
+ function saveOpenNodes() {
381
+ localStorage.setItem("pibo.chat.openNodes", JSON.stringify(Array.from(openNodes)));
382
+ }
383
+ function escapeText(value) {
384
+ return String(value == null ? "" : value).replace(/[&<>"']/g, function(ch) {
385
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[ch];
386
+ });
387
+ }
388
+ function pretty(value) {
389
+ if (value == null || value === "") return "";
390
+ if (typeof value === "string") return value;
391
+ try { return JSON.stringify(value, null, 2); } catch { return String(value); }
392
+ }
393
+ function short(value, n) {
394
+ const text = String(value || "").replace(/\\s+/g, " ").trim();
395
+ return text.length > n ? text.slice(0, n - 1) + "…" : text;
396
+ }
397
+ function nodeLabel(type) {
398
+ return {
399
+ "user.message": "User Message",
400
+ "assistant.message": "Agent Message",
401
+ "agent.turn": "Agent Turn",
402
+ "model.reasoning": "Reasoning",
403
+ "tool.call": "Tool Call",
404
+ "tool.result": "Tool Result",
405
+ "agent.delegation": "Agent Delegation",
406
+ "agent.async": "Async Agent",
407
+ "execution.command": "Execution Command",
408
+ "yielded.run": "Yielded Run",
409
+ "error": "Error"
410
+ }[type] || type;
411
+ }
412
+ function nodeIcon(type) {
413
+ return {
414
+ "user.message": "U",
415
+ "assistant.message": "A",
416
+ "agent.turn": "R",
417
+ "model.reasoning": "?",
418
+ "tool.call": "T",
419
+ "tool.result": "O",
420
+ "agent.delegation": "D",
421
+ "agent.async": "A",
422
+ "execution.command": "$",
423
+ "yielded.run": "Y",
424
+ "error": "!"
425
+ }[type] || "*";
426
+ }
427
+ function spanType(type) {
428
+ return {
429
+ "user.message": "user.prompt",
430
+ "assistant.message": "model.response",
431
+ "agent.turn": "agent.run",
432
+ "model.reasoning": "model.reasoning",
433
+ "tool.call": "tool.call",
434
+ "tool.result": "tool.result",
435
+ "agent.delegation": "agent.delegation",
436
+ "agent.async": "agent.async",
437
+ "execution.command": "tool.result",
438
+ "yielded.run": "tool.call",
439
+ "error": "tool.result"
440
+ }[type] || "agent.run";
441
+ }
442
+ function spanLabel(type) {
443
+ const mapped = spanType(type);
444
+ return {
445
+ "agent.run": "Agent Run",
446
+ "tool.call": "Tool Call",
447
+ "tool.result": "Tool Result",
448
+ "model.response": "Model Response",
449
+ "model.reasoning": "Reasoning",
450
+ "agent.delegation": "Agent Delegation",
451
+ "agent.async": "Async Agent",
452
+ "user.prompt": "User Prompt"
453
+ }[mapped] || mapped;
454
+ }
455
+ function spanStatusClass(status) {
456
+ if (status === "error") return "error";
457
+ if (status === "running") return "running";
458
+ return "ok";
459
+ }
460
+ function renderJson(value) {
461
+ return '<pre class="json-renderer">' + escapeText(pretty(value)) + '</pre>';
462
+ }
463
+ function functionSignature(name, args) {
464
+ const input = args && typeof args === "object" && !Array.isArray(args) ? args : {};
465
+ const params = Object.entries(input).map(function(entry) {
466
+ const key = entry[0];
467
+ const value = entry[1];
468
+ const raw = typeof value === "string" ? "'" + short(value, 50) + "'" : short(pretty(value), 50);
469
+ return '<span class="syntax-key">' + escapeText(key) + '</span>=<span class="syntax-string">' + escapeText(raw) + '</span>';
470
+ }).join(", ");
471
+ return '<code class="span-function"><span class="syntax-keyword">def</span> <span class="syntax-name">' + escapeText(name || "tool") + '</span>(' + params + ')</code>';
472
+ }
473
+ function renderSpanContent(node) {
474
+ const actionHtml = [];
475
+ if (node.type === "user.message" && node.entryId) actionHtml.push('<button class="ghost fork-button" data-entry-id="' + escapeText(node.entryId) + '">Fork From Here</button>');
476
+ if (node.linkedPiboSessionId) actionHtml.push('<button class="ghost session-link" data-pibo-session-id="' + escapeText(node.linkedPiboSessionId) + '">Open Child Session</button>');
477
+ const actions = actionHtml.length ? '<div class="span-actions">' + actionHtml.join("") + '</div>' : "";
478
+
479
+ if (node.type === "user.message") {
480
+ return actions + '<div class="span-quote">' + escapeText(node.output || node.summary || "") + '</div>';
481
+ }
482
+ if (node.type === "assistant.message") {
483
+ return actions + '<div class="span-section"><div class="span-section-title output">Structured Output</div>' + renderJson(node.output || node.summary || "") + '</div>';
484
+ }
485
+ if (node.type === "model.reasoning") {
486
+ return actions + '<div class="reasoning-block"><span class="reasoning-comment">// Model reasoning</span>\\n' + escapeText(node.output || node.summary || "") + '</div>';
487
+ }
488
+ if (node.type === "tool.call" || node.type === "yielded.run") {
489
+ return actions +
490
+ '<div class="span-code-well">' + functionSignature(node.title, node.input || {}) + '</div>' +
491
+ (node.output !== undefined ? '<div class="span-output-summary"><span>Output:</span><code>' + escapeText(short(pretty(node.output), 120)) + '</code></div>' : "") +
492
+ '<div class="span-section"><div class="span-section-title">Input</div>' + renderJson(node.input || {}) + '</div>' +
493
+ (node.output !== undefined ? '<div class="span-section"><div class="span-section-title output">Output</div>' + renderJson(node.output) + '</div>' : "");
494
+ }
495
+ if (node.type === "tool.result" || node.type === "execution.command") {
496
+ return actions + '<div class="span-section"><div class="span-section-title output">Output</div>' + renderJson(node.output || node.input || "") + '</div>';
497
+ }
498
+ if (node.type === "agent.delegation") {
499
+ return actions +
500
+ '<div class="delegation-card"><div class="delegation-title">Agent Delegation · ' + escapeText(node.title || "subagent") + '</div><div class="delegation-query">' + escapeText(short(node.summary || pretty(node.input), 220)) + '</div></div>' +
501
+ '<div class="span-section"><div class="span-section-title">Input</div>' + renderJson(node.input || {}) + '</div>' +
502
+ (node.output !== undefined ? '<div class="span-section"><div class="span-section-title output">Output</div>' + renderJson(node.output) + '</div>' : "");
503
+ }
504
+ if (node.type === "agent.async") {
505
+ const input = node.input && typeof node.input === "object" ? node.input : {};
506
+ const startedBy = input.startedBy || "pibo_run_start";
507
+ const runId = input.runId || node.runId || "";
508
+ return actions +
509
+ '<div class="delegation-card"><div class="delegation-title">Async Agent · ' + escapeText(node.title || "subagent") + '</div><div class="delegation-query">Started by ' + escapeText(startedBy) + (runId ? ' · ' + escapeText(runId) : "") + '</div></div>' +
510
+ '<div class="span-section"><div class="span-section-title">Input</div>' + renderJson(node.input || {}) + '</div>' +
511
+ (node.output !== undefined ? '<div class="span-section"><div class="span-section-title output">Run</div>' + renderJson(node.output) + '</div>' : "");
512
+ }
513
+ if (node.type === "error") {
514
+ return actions + '<div class="span-section"><div class="span-section-title">Error</div>' + renderJson(node.error || node.output || "") + '</div>';
515
+ }
516
+ return actions + '<div class="span-section">' + renderJson(node.output || node.input || node.summary || "") + '</div>';
517
+ }
518
+
519
+ async function signIn() {
520
+ const response = await fetch("/api/auth/sign-in/social", {
521
+ method: "POST",
522
+ headers: { "content-type": "application/json" },
523
+ body: JSON.stringify({ provider: "google", callbackURL: "/apps/chat", disableRedirect: true }),
524
+ });
525
+ const data = await response.json();
526
+ if (!response.ok || !data.url) {
527
+ renderSignedOut(data.message || data.error || "Could not start Google sign in.");
528
+ return;
529
+ }
530
+ location.href = data.url;
531
+ }
532
+
533
+ async function clearAuthSession() {
534
+ await fetch("/api/auth/sign-out", {
535
+ method: "POST",
536
+ credentials: "same-origin",
537
+ headers: { "content-type": "application/json" },
538
+ body: "{}",
539
+ });
540
+ }
541
+
542
+ async function signOut() {
543
+ await clearAuthSession();
544
+ location.reload();
545
+ }
546
+
547
+ function renderSignedOut(message) {
548
+ userEl.replaceChildren();
549
+ sessionTreeEl.innerHTML = "";
550
+ contentEl.innerHTML = '<div class="placeholder">' + escapeText(message || "Sign in to start a Pibo session.") + '</div>';
551
+ composer.closest(".composer-wrap").classList.add("hidden");
552
+ const button = document.createElement("button");
553
+ button.className = "primary";
554
+ button.textContent = "Sign in with Google";
555
+ button.addEventListener("click", signIn);
556
+ userEl.append(button);
557
+ sessionTitleEl.textContent = "Signed Out";
558
+ sessionMetaEl.textContent = "";
559
+ }
560
+
561
+ function renderSignedIn(data) {
562
+ userEl.replaceChildren();
563
+ const label = document.createElement("span");
564
+ label.textContent = data.identity.email || data.identity.name || data.identity.userId;
565
+ userEl.append(label);
566
+ const button = document.createElement("button");
567
+ button.textContent = "Sign out";
568
+ button.addEventListener("click", signOut);
569
+ userEl.append(button);
570
+ composer.closest(".composer-wrap").classList.remove("hidden");
571
+ }
572
+
573
+ function connectEvents() {
574
+ if (eventSource) eventSource.close();
575
+ if (!selectedPiboSessionId) return;
576
+ eventSource = new EventSource("/api/chat/events?piboSessionId=" + encodeURIComponent(selectedPiboSessionId));
577
+ eventSource.addEventListener("pibo", (event) => {
578
+ const payload = JSON.parse(event.data);
579
+ appendRawEvent({ type: payload.type, payload });
580
+ if (payload.type !== "TEXT_MESSAGE_CONTENT" && payload.type !== "REASONING_MESSAGE_CONTENT") {
581
+ void refreshTrace();
582
+ }
583
+ });
584
+ eventSource.onerror = () => appendRawEvent({ type: "stream_error", payload: "Event stream disconnected." });
585
+ }
586
+
587
+ async function loadSession() {
588
+ const response = await fetch("/api/chat/bootstrap");
589
+ if (response.status === 401) {
590
+ renderSignedOut();
591
+ return;
592
+ }
593
+ const data = await response.json();
594
+ if (response.status === 403) {
595
+ await clearAuthSession().catch(() => {});
596
+ renderSignedOut("Not authorized. Sign in with an allowed Google account.");
597
+ return;
598
+ }
599
+ if (!response.ok) {
600
+ renderSignedOut(data.error || "Could not load session.");
601
+ return;
602
+ }
603
+ bootstrap = data;
604
+ selectedPiboSessionId = selectedPiboSessionId || data.selectedPiboSessionId;
605
+ ensureSelectedAgentProfile();
606
+ renderSignedIn(data);
607
+ renderArea();
608
+ connectEvents();
609
+ }
610
+
611
+ function ensureSelectedAgentProfile() {
612
+ const agents = (bootstrap && bootstrap.agents) || [];
613
+ if (!agents.length) return "";
614
+ const exists = agents.some(function(agent) {
615
+ return agent.name === selectedAgentProfile || (agent.aliases || []).includes(selectedAgentProfile);
616
+ });
617
+ if (!selectedAgentProfile || !exists) {
618
+ selectedAgentProfile = bootstrap.session && bootstrap.session.profile || agents[0].name;
619
+ }
620
+ localStorage.setItem("pibo.chat.newSessionProfile", selectedAgentProfile);
621
+ return selectedAgentProfile;
622
+ }
623
+
624
+ function renderArea() {
625
+ document.querySelectorAll(".tab").forEach(function(tab) {
626
+ tab.classList.toggle("active", tab.dataset.area === area);
627
+ });
628
+ document.querySelector("#sidebar-title").textContent = area[0].toUpperCase() + area.slice(1);
629
+ newSessionButton.classList.toggle("hidden", area !== "sessions");
630
+ if (area === "sessions") {
631
+ renderSessions();
632
+ void refreshTrace();
633
+ return;
634
+ }
635
+ if (area === "agents") {
636
+ renderAgents();
637
+ return;
638
+ }
639
+ renderSettings();
640
+ }
641
+ function renderSessions() {
642
+ sessionTreeEl.innerHTML = "";
643
+ (bootstrap.sessions || []).forEach(function(node) { sessionTreeEl.append(renderSessionNode(node, 0)); });
644
+ }
645
+ function renderSessionNode(node, depth) {
646
+ const children = node.children || [];
647
+ const hasChildren = children.length > 0;
648
+ if (sessionTreeHasSession(children, selectedPiboSessionId)) {
649
+ openSessionNodes.add(node.piboSessionId);
650
+ }
651
+ const expanded = openSessionNodes.has(node.piboSessionId);
652
+ const wrap = document.createElement("div");
653
+ const row = document.createElement("div");
654
+ row.className = "session-row" + (node.piboSessionId === selectedPiboSessionId ? " active" : "");
655
+ row.style.paddingLeft = 8 + depth * 14 + "px";
656
+ row.title = (node.title || "Untitled Session") + "\\n" + node.piboSessionId;
657
+ if (hasChildren) {
658
+ const toggle = document.createElement("button");
659
+ toggle.type = "button";
660
+ toggle.className = "session-toggle";
661
+ toggle.textContent = expanded ? "v" : ">";
662
+ toggle.setAttribute("aria-expanded", String(expanded));
663
+ toggle.setAttribute("aria-label", expanded ? "Collapse Subsessions" : "Expand Subsessions");
664
+ toggle.title = expanded ? "Collapse Subsessions" : "Expand Subsessions";
665
+ toggle.addEventListener("click", function() {
666
+ if (openSessionNodes.has(node.piboSessionId)) openSessionNodes.delete(node.piboSessionId);
667
+ else openSessionNodes.add(node.piboSessionId);
668
+ renderSessions();
669
+ });
670
+ row.append(toggle);
671
+ } else {
672
+ row.append(document.createElement("span"));
673
+ }
674
+ const select = document.createElement("button");
675
+ select.type = "button";
676
+ select.className = "session-select";
677
+ select.innerHTML =
678
+ '<span><span class="session-title">' + escapeText(node.title) + '</span><span class="session-id">' + escapeText(node.piboSessionId) + '</span></span>' +
679
+ '<span class="status-dot ' + escapeText(node.status) + '"></span>';
680
+ select.addEventListener("click", function() { selectSession(node.piboSessionId); });
681
+ row.append(select);
682
+ wrap.append(row);
683
+ if (expanded) children.forEach(function(child) { wrap.append(renderSessionNode(child, depth + 1)); });
684
+ return wrap;
685
+ }
686
+ function sessionTreeHasSession(nodes, piboSessionId) {
687
+ return (nodes || []).some(function(node) {
688
+ return node.piboSessionId === piboSessionId || sessionTreeHasSession(node.children, piboSessionId);
689
+ });
690
+ }
691
+ async function selectSession(piboSessionId) {
692
+ selectedPiboSessionId = piboSessionId;
693
+ connectEvents();
694
+ await refreshBootstrap(false);
695
+ renderArea();
696
+ }
697
+ async function createSession(profile) {
698
+ const selectedProfile = profile || ensureSelectedAgentProfile();
699
+ const created = await postJson("/api/chat/sessions", selectedProfile ? { profile: selectedProfile } : {});
700
+ selectedPiboSessionId = created.session && created.session.id;
701
+ connectEvents();
702
+ await refreshBootstrap(false);
703
+ renderArea();
704
+ }
705
+ async function refreshBootstrap(keepTrace) {
706
+ const response = await fetch("/api/chat/bootstrap?piboSessionId=" + encodeURIComponent(selectedPiboSessionId || ""));
707
+ if (!response.ok) return;
708
+ bootstrap = await response.json();
709
+ selectedPiboSessionId = bootstrap.selectedPiboSessionId;
710
+ if (!keepTrace) renderSessions();
711
+ }
712
+ async function refreshTrace() {
713
+ if (!selectedPiboSessionId) return;
714
+ const response = await fetch("/api/chat/trace?piboSessionId=" + encodeURIComponent(selectedPiboSessionId) + "&includeRawEvents=true&rawEventsLimit=80");
715
+ if (!response.ok) return;
716
+ const trace = await response.json();
717
+ sessionTitleEl.textContent = trace.title || selectedPiboSessionId;
718
+ sessionMetaEl.textContent = trace.piboSessionId + " · " + trace.piSessionId;
719
+ renderTrace(trace.nodes || []);
720
+ renderRawEvents(trace.rawEvents || []);
721
+ }
722
+ function renderTrace(nodes) {
723
+ const visible = nodes.filter(function(node) { return showThinking || node.type !== "model.reasoning"; });
724
+ if (!visible.length) {
725
+ contentEl.innerHTML = '<div class="placeholder">No trace events yet.</div>';
726
+ return;
727
+ }
728
+ const list = document.createElement("div");
729
+ list.className = "trace-list";
730
+ visible.forEach(function(node) { list.append(renderTraceNode(node, 0)); });
731
+ contentEl.replaceChildren(list);
732
+ }
733
+ function renderTraceNode(node, depth) {
734
+ const wrapper = document.createElement("article");
735
+ const mappedSpanType = spanType(node.type);
736
+ wrapper.className = "span-card " + spanStatusClass(node.status) + (openNodes.has(node.id) ? " open" : "");
737
+ wrapper.dataset.spanType = mappedSpanType;
738
+ wrapper.dataset.nodeId = node.id;
739
+ wrapper.style.marginLeft = Math.min(depth * 12, 96) + "px";
740
+ const header = document.createElement("button");
741
+ header.className = "span-header";
742
+ const statusClass = node.status === "running" ? "running" : node.status === "error" ? "error" : "";
743
+ header.innerHTML =
744
+ '<span class="span-chevron">' + (openNodes.has(node.id) ? "v" : ">") + '</span>' +
745
+ '<span class="span-icon">' + escapeText(nodeIcon(node.type)) + '</span>' +
746
+ '<span><span class="span-type-label">' + escapeText(spanLabel(node.type)) + '</span><span class="span-title">' + escapeText(node.title || "") + (node.summary ? " · " + escapeText(short(node.summary, 160)) : "") + '</span></span>' +
747
+ '<span class="span-meta"><span class="badge ' + statusClass + '">' + escapeText(node.status) + '</span></span>';
748
+ header.addEventListener("click", function() {
749
+ if (openNodes.has(node.id)) openNodes.delete(node.id); else openNodes.add(node.id);
750
+ saveOpenNodes();
751
+ wrapper.classList.toggle("open");
752
+ header.querySelector(".span-chevron").textContent = wrapper.classList.contains("open") ? "v" : ">";
753
+ });
754
+ wrapper.append(header);
755
+ const body = document.createElement("div");
756
+ body.className = "span-body";
757
+ body.innerHTML = renderSpanContent(node);
758
+ body.querySelectorAll(".fork-button").forEach(function(button) {
759
+ button.addEventListener("click", function(event) {
760
+ event.stopPropagation();
761
+ void forkFrom(button.dataset.entryId);
762
+ });
763
+ });
764
+ body.querySelectorAll(".session-link").forEach(function(button) {
765
+ button.addEventListener("click", function(event) {
766
+ event.stopPropagation();
767
+ void selectSession(button.dataset.piboSessionId);
768
+ });
769
+ });
770
+ if (node.children && node.children.length) {
771
+ const childWrap = document.createElement("div");
772
+ childWrap.className = "children span-children";
773
+ node.children.filter(function(child) { return showThinking || child.type !== "model.reasoning"; }).forEach(function(child) {
774
+ childWrap.append(renderTraceNode(child, depth + 1));
775
+ });
776
+ body.append(childWrap);
777
+ }
778
+ wrapper.append(body);
779
+ return wrapper;
780
+ }
781
+ function appendRawEvent(event) {
782
+ const item = document.createElement("div");
783
+ item.className = "raw-event";
784
+ item.innerHTML = '<div class="raw-event-type">' + escapeText(event.type) + '</div><pre class="payload">' + escapeText(pretty(event.payload)) + '</pre>';
785
+ rawLogEl.prepend(item);
786
+ }
787
+ function renderRawEvents(events) {
788
+ rawLogEl.innerHTML = "";
789
+ events.slice(-80).reverse().forEach(appendRawEvent);
790
+ }
791
+ function renderAgents() {
792
+ sessionTitleEl.textContent = "Agents";
793
+ sessionMetaEl.textContent = "Profile selection";
794
+ const agents = (bootstrap && bootstrap.agents) || [];
795
+ ensureSelectedAgentProfile();
796
+ sessionTreeEl.innerHTML = agents.map(function(agent) {
797
+ const active = agent.name === selectedAgentProfile ? " active" : "";
798
+ return '<button class="session-row' + active + '" data-profile="' + escapeText(agent.name) + '"><span></span><span><span class="session-title">' + escapeText(agent.name) + '</span><span class="session-id">' + escapeText((agent.aliases || []).join(", ") || "profile") + '</span></span><span class="status-dot"></span></button>';
799
+ }).join("");
800
+ sessionTreeEl.querySelectorAll("[data-profile]").forEach(function(button) {
801
+ button.addEventListener("click", function() {
802
+ selectedAgentProfile = button.dataset.profile;
803
+ localStorage.setItem("pibo.chat.newSessionProfile", selectedAgentProfile);
804
+ renderAgents();
805
+ });
806
+ });
807
+ contentEl.innerHTML = '<div class="trace-list">' + agents.map(function(agent) {
808
+ const selected = agent.name === selectedAgentProfile ? "selected" : "available";
809
+ return '<article class="trace-node open" data-type="agent.turn"><div class="trace-header"><span class="trace-icon">A</span><span><span class="trace-label">' + escapeText(agent.name) + '</span><span class="trace-summary">' + escapeText(agent.description || "No description") + '</span></span><span class="trace-actions"><button class="ghost profile-select" data-profile="' + escapeText(agent.name) + '">' + selected + '</button><button class="ghost profile-create" data-profile="' + escapeText(agent.name) + '">New Session</button></span></div><div class="trace-body"><pre class="payload">' + escapeText(pretty(agent)) + '</pre></div></article>';
810
+ }).join("") + '</div>';
811
+ contentEl.querySelectorAll(".profile-select").forEach(function(button) {
812
+ button.addEventListener("click", function() {
813
+ selectedAgentProfile = button.dataset.profile;
814
+ localStorage.setItem("pibo.chat.newSessionProfile", selectedAgentProfile);
815
+ renderAgents();
816
+ });
817
+ });
818
+ contentEl.querySelectorAll(".profile-create").forEach(function(button) {
819
+ button.addEventListener("click", function() {
820
+ selectedAgentProfile = button.dataset.profile;
821
+ localStorage.setItem("pibo.chat.newSessionProfile", selectedAgentProfile);
822
+ void createSession(selectedAgentProfile);
823
+ });
824
+ });
825
+ }
826
+ function renderSettings() {
827
+ sessionTitleEl.textContent = "Settings";
828
+ sessionMetaEl.textContent = "Browser-local V1 settings";
829
+ sessionTreeEl.innerHTML = '<div class="placeholder">Settings navigation placeholder.</div>';
830
+ contentEl.innerHTML = '<div class="placeholder">Thinking display and trace expansion state are stored in this browser.</div>';
831
+ }
832
+ function availableCommands() {
833
+ const actions = (bootstrap && bootstrap.capabilities && bootstrap.capabilities.actions) || [];
834
+ const commands = [];
835
+ actions.forEach(function(action) {
836
+ (action.slashCommands || []).forEach(function(command) {
837
+ if (command !== "tree") commands.push({ slash: "/" + command, action: action.name, description: action.description || action.name });
838
+ });
839
+ });
840
+ commands.push({ slash: "/thinking-show", action: "thinking-show", description: "Toggle historical thinking display in this browser." });
841
+ return commands;
842
+ }
843
+ function renderCommandMenu() {
844
+ const query = messageInput.value.trim();
845
+ if (!query.startsWith("/")) { commandMenu.classList.add("hidden"); return; }
846
+ const commands = availableCommands().filter(function(command) { return command.slash.startsWith(query.split(/\\s+/)[0]); });
847
+ if (!commands.length) { commandMenu.classList.add("hidden"); return; }
848
+ commandIndex = Math.min(commandIndex, commands.length - 1);
849
+ commandMenu.innerHTML = "";
850
+ commands.forEach(function(command, index) {
851
+ const item = document.createElement("button");
852
+ item.type = "button";
853
+ item.className = "command-item" + (index === commandIndex ? " active" : "");
854
+ item.innerHTML = '<span class="command-name">' + escapeText(command.slash) + '</span><span class="command-desc">' + escapeText(command.description) + '</span>';
855
+ item.addEventListener("click", function() { void runCommand(command); });
856
+ commandMenu.append(item);
857
+ });
858
+ commandMenu.classList.remove("hidden");
859
+ }
860
+ async function runCommand(command) {
861
+ commandMenu.classList.add("hidden");
862
+ const text = messageInput.value.trim();
863
+ messageInput.value = "";
864
+ if (command.action === "thinking-show") {
865
+ showThinking = !showThinking;
866
+ localStorage.setItem("pibo.chat.showThinking", String(showThinking));
867
+ updateThinkingButton();
868
+ await refreshTrace();
869
+ return;
870
+ }
871
+ const commandArgs = text.slice(command.slash.length).trim();
872
+ const params = command.action === "thinking" && commandArgs
873
+ ? { level: commandArgs.split(/\\s+/, 1)[0] }
874
+ : command.action === "compact" && commandArgs
875
+ ? { customInstructions: commandArgs }
876
+ : undefined;
877
+ const result = await postJson("/api/chat/action", { piboSessionId: selectedPiboSessionId, action: command.action, params: params });
878
+ appendRawEvent({ type: "command_result", payload: result });
879
+ if (command.action === "session.clone" && result && result.result && result.result.piboSessionId) {
880
+ await selectSession(result.result.piboSessionId);
881
+ }
882
+ }
883
+ async function forkFrom(entryId) {
884
+ const result = await postJson("/api/chat/action", { piboSessionId: selectedPiboSessionId, action: "session.fork", params: { entryId: entryId } });
885
+ appendRawEvent({ type: "fork_result", payload: result });
886
+ pendingForkResult = result && result.result ? result.result : undefined;
887
+ const selectedText = pendingForkResult && typeof pendingForkResult.selectedText === "string" ? pendingForkResult.selectedText : undefined;
888
+ if (pendingForkResult && pendingForkResult.piboSessionId) {
889
+ await selectSession(pendingForkResult.piboSessionId);
890
+ } else {
891
+ await refreshBootstrap(false);
892
+ await refreshTrace();
893
+ }
894
+ if (selectedText !== undefined) {
895
+ messageInput.value = selectedText;
896
+ messageInput.focus();
897
+ }
898
+ }
899
+ async function postJson(url, payload) {
900
+ const response = await fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) });
901
+ const data = await response.json().catch(function() { return { error: "Request failed" }; });
902
+ if (!response.ok) throw new Error(data.error || "Request failed");
903
+ return data;
904
+ }
905
+
906
+ newSessionButton.addEventListener("click", function() { void createSession(); });
907
+ composer.addEventListener("submit", async (event) => {
908
+ event.preventDefault();
909
+ const text = messageInput.value.trim();
910
+ if (!text) return;
911
+ const command = availableCommands().find(function(candidate) { return text.split(/\\s+/)[0] === candidate.slash; });
912
+ if (text.startsWith("/") && command) {
913
+ await runCommand(command);
914
+ return;
915
+ }
916
+ messageInput.value = "";
917
+ await postJson("/api/chat/message", { piboSessionId: selectedPiboSessionId, text: text });
918
+ await refreshTrace();
919
+ });
920
+ messageInput.addEventListener("input", renderCommandMenu);
921
+ messageInput.addEventListener("keydown", function(event) {
922
+ if (commandMenu.classList.contains("hidden")) {
923
+ if (event.key === "Enter" && !event.shiftKey) {
924
+ event.preventDefault();
925
+ composer.requestSubmit();
926
+ }
927
+ return;
928
+ }
929
+ const count = commandMenu.querySelectorAll(".command-item").length;
930
+ if (event.key === "ArrowDown") { event.preventDefault(); commandIndex = (commandIndex + 1) % count; renderCommandMenu(); }
931
+ if (event.key === "ArrowUp") { event.preventDefault(); commandIndex = (commandIndex - 1 + count) % count; renderCommandMenu(); }
932
+ if (event.key === "Enter" && !event.shiftKey) {
933
+ const item = commandMenu.querySelectorAll(".command-item")[commandIndex];
934
+ if (item) { event.preventDefault(); item.click(); }
935
+ }
936
+ });
937
+ document.querySelectorAll(".tab").forEach(function(tab) {
938
+ tab.addEventListener("click", function() { area = tab.dataset.area; renderArea(); });
939
+ });
940
+ document.querySelector("#refresh").addEventListener("click", function() { void refreshBootstrap(false).then(renderArea); });
941
+ document.querySelector("#clear-local").addEventListener("click", function() { rawLogEl.innerHTML = ""; });
942
+ document.querySelector("#collapse-all").addEventListener("click", function() { openNodes.clear(); saveOpenNodes(); void refreshTrace(); });
943
+ document.querySelector("#expand-all").addEventListener("click", function() {
944
+ contentEl.querySelectorAll(".span-card").forEach(function(node) {
945
+ if (node.dataset.nodeId) openNodes.add(node.dataset.nodeId);
946
+ node.classList.add("open");
947
+ const chevron = node.querySelector(".span-chevron");
948
+ if (chevron) chevron.textContent = "v";
949
+ });
950
+ saveOpenNodes();
951
+ });
952
+ document.querySelector("#expand-one").addEventListener("click", function() {
953
+ openNodes.clear();
954
+ contentEl.querySelectorAll(".trace-list > .span-card").forEach(function(node) {
955
+ if (node.dataset.nodeId) openNodes.add(node.dataset.nodeId);
956
+ node.classList.add("open");
957
+ const chevron = node.querySelector(".span-chevron");
958
+ if (chevron) chevron.textContent = "v";
959
+ });
960
+ saveOpenNodes();
961
+ });
962
+ function updateThinkingButton() {
963
+ document.querySelector("#thinking-toggle").textContent = showThinking ? "Thinking On" : "Thinking Off";
964
+ }
965
+ document.querySelector("#thinking-toggle").addEventListener("click", async function() {
966
+ showThinking = !showThinking;
967
+ localStorage.setItem("pibo.chat.showThinking", String(showThinking));
968
+ updateThinkingButton();
969
+ await refreshTrace();
970
+ });
971
+ document.querySelector("#fork-stay").addEventListener("click", function() {
972
+ forkModal.classList.add("hidden");
973
+ const previousFile = pendingForkResult && pendingForkResult.previous && pendingForkResult.previous.sessionFile;
974
+ const targetPiboSessionId = pendingForkResult && pendingForkResult.piboSessionId ? pendingForkResult.piboSessionId : selectedPiboSessionId;
975
+ pendingForkResult = undefined;
976
+ messageInput.value = pendingForkComposerText || "";
977
+ pendingForkComposerText = undefined;
978
+ if (previousFile) {
979
+ void postJson("/api/chat/action", { piboSessionId: targetPiboSessionId, action: "session.switch", params: { sessionFile: previousFile } })
980
+ .then(function(result) { appendRawEvent({ type: "fork_restore", payload: result }); return refreshTrace(); })
981
+ .catch(function(error) { appendRawEvent({ type: "fork_restore_error", payload: String(error) }); });
982
+ }
983
+ });
984
+ document.querySelector("#fork-switch").addEventListener("click", async function() {
985
+ forkModal.classList.add("hidden");
986
+ const targetPiboSessionId = pendingForkResult && pendingForkResult.piboSessionId ? pendingForkResult.piboSessionId : selectedPiboSessionId;
987
+ pendingForkResult = undefined;
988
+ pendingForkComposerText = undefined;
989
+ if (targetPiboSessionId) await selectSession(targetPiboSessionId);
990
+ });
991
+
992
+ updateThinkingButton();
993
+ loadSession();
994
+ </script>
995
+ </body>
996
996
  </html>`;
997
997
  }