@pasko70/pibo 1.4.1 → 1.4.2

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