insika 0.2.0 → 0.7.0

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 (300) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +325 -17
  3. data/README.md +52 -11
  4. data/bin/insika +895 -8
  5. data/bin/insika-router +87 -0
  6. data/docs/AGENTS.md +229 -163
  7. data/docs/API.md +56 -0
  8. data/docs/ARCHITECTURE.md +3 -2
  9. data/docs/ARTIFACTS.md +95 -0
  10. data/docs/BENCHMARK.md +2 -2
  11. data/docs/CHANNELS.md +111 -21
  12. data/docs/CONTEXT.md +132 -19
  13. data/docs/DEMO.md +80 -0
  14. data/docs/DEPLOY.md +75 -4
  15. data/docs/EMBEDDING.md +1 -1
  16. data/docs/EVALS.md +146 -2
  17. data/docs/FACTS.md +135 -0
  18. data/docs/HARVEST.md +116 -0
  19. data/docs/KNOWLEDGE.md +290 -0
  20. data/docs/LOADTEST.md +16 -10
  21. data/docs/MEDIA.md +128 -0
  22. data/docs/OBSERVABILITY.md +65 -6
  23. data/docs/OUTCOMES.md +137 -0
  24. data/docs/PLUGINS.md +51 -6
  25. data/docs/POLICY.md +216 -0
  26. data/docs/REFINEMENT.md +17 -12
  27. data/docs/RELEASING.md +27 -0
  28. data/docs/ROUTER.md +213 -0
  29. data/docs/RUNNING-LOCAL.md +6 -6
  30. data/docs/SCHEDULING.md +121 -0
  31. data/docs/SECURITY.md +79 -2
  32. data/docs/SKILLS.md +13 -4
  33. data/docs/SOAK.md +127 -0
  34. data/docs/TEMPLATES.md +134 -0
  35. data/docs/TOOLS.md +220 -27
  36. data/docs/WHY.md +1 -1
  37. data/docs/WORKFLOWS.md +2 -2
  38. data/docs/_includes/head_custom.html +5 -0
  39. data/docs/_includes/title.html +13 -0
  40. data/docs/_sass/color_schemes/insika.scss +32 -0
  41. data/docs/_sass/custom/custom.scss +199 -0
  42. data/docs/_sass/custom/setup.scss +26 -0
  43. data/docs/assets/img/favicon.svg +7 -0
  44. data/docs/assets/img/insika-mark.svg +7 -0
  45. data/docs/core-concepts.md +21 -0
  46. data/docs/domain.md +115 -0
  47. data/docs/improve.md +20 -0
  48. data/docs/index.md +9 -6
  49. data/docs/integrate.md +20 -0
  50. data/docs/operate.md +13 -6
  51. data/docs/prompts/ADD-TOOL.md +118 -0
  52. data/docs/prompts/DIAGNOSE-TURN.md +65 -0
  53. data/docs/prompts/GO-LIVE.md +138 -0
  54. data/docs/prompts/RUN-EXAMPLES.md +70 -0
  55. data/docs/reference.md +19 -0
  56. data/docs/ship.md +10 -2
  57. data/docs/start-here.md +18 -0
  58. data/lib/insika/agent_profile.rb +202 -10
  59. data/lib/insika/artifact_signing.rb +82 -0
  60. data/lib/insika/artifact_store.rb +160 -0
  61. data/lib/insika/balloon_splitter.rb +102 -0
  62. data/lib/insika/budget_ledger.rb +34 -3
  63. data/lib/insika/cache_series_store.rb +49 -0
  64. data/lib/insika/channel_delivery.rb +119 -14
  65. data/lib/insika/channels/relay.rb +77 -3
  66. data/lib/insika/channels/web.rb +2 -2
  67. data/lib/insika/chat_builder.rb +105 -2
  68. data/lib/insika/checkpoint_store.rb +16 -0
  69. data/lib/insika/commands/agent_payload.rb +3 -3
  70. data/lib/insika/commands/backfill_knowledge.rb +145 -0
  71. data/lib/insika/commands/cancel_followup.rb +49 -0
  72. data/lib/insika/commands/delete_artifact.rb +35 -0
  73. data/lib/insika/commands/delete_concept.rb +34 -0
  74. data/lib/insika/commands/delete_mcp.rb +6 -2
  75. data/lib/insika/commands/delete_tenant_data.rb +107 -0
  76. data/lib/insika/commands/export_customer_memory.rb +48 -0
  77. data/lib/insika/commands/forget_customer.rb +117 -0
  78. data/lib/insika/commands/freeze_funnel_baseline.rb +113 -0
  79. data/lib/insika/commands/gate_harvest.rb +138 -0
  80. data/lib/insika/commands/gate_refinement.rb +1 -1
  81. data/lib/insika/commands/judge_shadow_pairs.rb +124 -0
  82. data/lib/insika/commands/memory_forget_fact.rb +20 -4
  83. data/lib/insika/commands/memory_put_fact.rb +23 -4
  84. data/lib/insika/commands/promote_harvest.rb +130 -0
  85. data/lib/insika/commands/record_outcome.rb +46 -0
  86. data/lib/insika/commands/record_shadow_reply.rb +68 -0
  87. data/lib/insika/commands/refresh_mcp_tools.rb +47 -0
  88. data/lib/insika/commands/reject_harvest.rb +38 -0
  89. data/lib/insika/commands/resolve_proposal.rb +108 -0
  90. data/lib/insika/commands/restore_concept.rb +34 -0
  91. data/lib/insika/commands/revoke_contact.rb +49 -0
  92. data/lib/insika/commands/rollback_harvest.rb +86 -0
  93. data/lib/insika/commands/run_distillation.rb +186 -0
  94. data/lib/insika/commands/run_harvest.rb +393 -0
  95. data/lib/insika/commands/seed_demo_data.rb +31 -0
  96. data/lib/insika/commands/send_message.rb +103 -6
  97. data/lib/insika/commands/session_purge.rb +67 -0
  98. data/lib/insika/commands/upsert_mcp.rb +6 -3
  99. data/lib/insika/commands/write_concept.rb +57 -0
  100. data/lib/insika/contact_store.rb +183 -0
  101. data/lib/insika/context/builder.rb +21 -3
  102. data/lib/insika/context/fragment.rb +7 -3
  103. data/lib/insika/context/priority.rb +5 -0
  104. data/lib/insika/context/provider.rb +17 -3
  105. data/lib/insika/context/providers/briefing.rb +96 -0
  106. data/lib/insika/context/providers/knowledge.rb +108 -0
  107. data/lib/insika/context/providers/memory.rb +16 -7
  108. data/lib/insika/context/providers/prompt.rb +57 -23
  109. data/lib/insika/context/providers/skill.rb +2 -0
  110. data/lib/insika/context/providers/tool_search.rb +2 -0
  111. data/lib/insika/context_trace_store.rb +38 -2
  112. data/lib/insika/cron.rb +189 -0
  113. data/lib/insika/demo/agent_attrs.rb +43 -0
  114. data/lib/insika/demo/golden_cases.rb +81 -0
  115. data/lib/insika/demo/seeder.rb +336 -0
  116. data/lib/insika/distill.rb +224 -0
  117. data/lib/insika/distill_engine.rb +169 -0
  118. data/lib/insika/doctor.rb +889 -11
  119. data/lib/insika/dsl/definition.rb +3 -2
  120. data/lib/insika/dsl/runtime.rb +69 -82
  121. data/lib/insika/dsl/server_boot.rb +92 -1
  122. data/lib/insika/dsl/system.rb +10 -2
  123. data/lib/insika/dsl.rb +186 -5
  124. data/lib/insika/edge_limiter.rb +28 -7
  125. data/lib/insika/env_schema.rb +25 -3
  126. data/lib/insika/errors.rb +11 -0
  127. data/lib/insika/evals/assertions.rb +3 -2
  128. data/lib/insika/evals/golden.rb +41 -4
  129. data/lib/insika/evals/judge.rb +47 -2
  130. data/lib/insika/evals/pairwise.rb +29 -6
  131. data/lib/insika/evals/persona.rb +98 -0
  132. data/lib/insika/evals/runner.rb +9 -0
  133. data/lib/insika/evals/simulator.rb +225 -0
  134. data/lib/insika/evals/transport.rb +83 -1
  135. data/lib/insika/event_stream.rb +10 -0
  136. data/lib/insika/evidence.rb +183 -0
  137. data/lib/insika/executor.rb +869 -76
  138. data/lib/insika/followup_engine.rb +207 -0
  139. data/lib/insika/followup_policy.rb +198 -0
  140. data/lib/insika/followup_store.rb +306 -0
  141. data/lib/insika/funnel_declaration.rb +106 -0
  142. data/lib/insika/funnel_fold.rb +179 -0
  143. data/lib/insika/funnel_store.rb +163 -0
  144. data/lib/insika/golden_store.rb +17 -2
  145. data/lib/insika/grounding/matcher.rb +69 -0
  146. data/lib/insika/grounding.rb +44 -0
  147. data/lib/insika/harvest/conversion_gate.rb +159 -0
  148. data/lib/insika/harvest/criterion.rb +98 -0
  149. data/lib/insika/harvest/gate.rb +194 -0
  150. data/lib/insika/harvest/negative_list.rb +199 -0
  151. data/lib/insika/harvest.rb +241 -0
  152. data/lib/insika/harvest_engine.rb +193 -0
  153. data/lib/insika/harvest_store.rb +548 -0
  154. data/lib/insika/knowledge.rb +680 -0
  155. data/lib/insika/knowledge_store.rb +140 -0
  156. data/lib/insika/mcp_client.rb +94 -0
  157. data/lib/insika/mcp_json.rb +74 -0
  158. data/lib/insika/mcp_live_tool.rb +43 -0
  159. data/lib/insika/mcp_store.rb +98 -26
  160. data/lib/insika/mcp_tool_ingestor.rb +30 -8
  161. data/lib/insika/mcp_tool_registry.rb +100 -0
  162. data/lib/insika/media.rb +382 -0
  163. data/lib/insika/memory_audit_store.rb +85 -0
  164. data/lib/insika/memory_store.rb +264 -23
  165. data/lib/insika/message_origin.rb +7 -2
  166. data/lib/insika/middleware.rb +9 -0
  167. data/lib/insika/model_visible.rb +87 -0
  168. data/lib/insika/model_visible_trace_store.rb +66 -0
  169. data/lib/insika/onboarding.rb +22 -1
  170. data/lib/insika/outbox_store.rb +42 -4
  171. data/lib/insika/outcome_store.rb +147 -0
  172. data/lib/insika/overlay_tool_registry.rb +37 -17
  173. data/lib/insika/packaging.rb +163 -0
  174. data/lib/insika/parity/criterion.rb +79 -0
  175. data/lib/insika/parity/verdict.rb +318 -0
  176. data/lib/insika/prefix_fingerprint.rb +58 -0
  177. data/lib/insika/profile_source.rb +29 -1
  178. data/lib/insika/prompt_catalog.rb +10 -0
  179. data/lib/insika/proposal_store.rb +271 -0
  180. data/lib/insika/queue_policy.rb +4 -1
  181. data/lib/insika/refinement/proposer.rb +1 -1
  182. data/lib/insika/reliability.rb +32 -6
  183. data/lib/insika/retention.rb +316 -0
  184. data/lib/insika/router/app.rb +157 -0
  185. data/lib/insika/router/backend_pool.rb +98 -0
  186. data/lib/insika/router/hash_ring.rb +55 -0
  187. data/lib/insika/router/proxy_body.rb +34 -0
  188. data/lib/insika/router/session_key.rb +54 -0
  189. data/lib/insika/router.rb +18 -0
  190. data/lib/insika/routing.rb +101 -0
  191. data/lib/insika/safety/config.rb +44 -4
  192. data/lib/insika/safety/corpus.rb +255 -0
  193. data/lib/insika/safety/detectors.rb +32 -113
  194. data/lib/insika/safety/factory.rb +15 -2
  195. data/lib/insika/safety/grounding_enforcer.rb +59 -0
  196. data/lib/insika/safety/grounding_validator.rb +49 -0
  197. data/lib/insika/safety/input_guardrail.rb +1 -1
  198. data/lib/insika/safety/moderator.rb +1 -1
  199. data/lib/insika/safety/output_filter.rb +9 -5
  200. data/lib/insika/safety/output_validator.rb +11 -5
  201. data/lib/insika/schedule.rb +177 -0
  202. data/lib/insika/schedule_engine.rb +314 -0
  203. data/lib/insika/schedule_store.rb +208 -0
  204. data/lib/insika/schema_guard.rb +35 -0
  205. data/lib/insika/server/app.rb +247 -17
  206. data/lib/insika/server/rack_app.rb +21 -1
  207. data/lib/insika/server/responses.rb +40 -1
  208. data/lib/insika/session_actor.rb +8 -4
  209. data/lib/insika/session_store.rb +65 -2
  210. data/lib/insika/settings_store.rb +10 -0
  211. data/lib/insika/shadow_pair_store.rb +258 -0
  212. data/lib/insika/skill_catalog.rb +16 -0
  213. data/lib/insika/soak/envelope.rb +140 -0
  214. data/lib/insika/soak/report.rb +392 -0
  215. data/lib/insika/soak/runner.rb +554 -0
  216. data/lib/insika/steer_injector.rb +21 -10
  217. data/lib/insika/store.rb +10 -1
  218. data/lib/insika/stores/memory.rb +6 -0
  219. data/lib/insika/stores/sqlite.rb +8 -0
  220. data/lib/insika/studio/app.rb +1437 -54
  221. data/lib/insika/studio/assets/dist/application.css +1 -1
  222. data/lib/insika/studio/assets/dist/application.js +23 -23
  223. data/lib/insika/studio/assets/dist/favicon.svg +6 -0
  224. data/lib/insika/studio/forms.rb +308 -10
  225. data/lib/insika/studio/nav_icons.rb +19 -1
  226. data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
  227. data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
  228. data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
  229. data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
  230. data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
  231. data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
  232. data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
  233. data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
  234. data/lib/insika/studio/views/_agents_master.erb +44 -0
  235. data/lib/insika/studio/views/_message.erb +49 -32
  236. data/lib/insika/studio/views/agent_detail.erb +62 -278
  237. data/lib/insika/studio/views/agents.erb +71 -54
  238. data/lib/insika/studio/views/approvals.erb +4 -1
  239. data/lib/insika/studio/views/artifact.erb +23 -0
  240. data/lib/insika/studio/views/artifacts.erb +59 -0
  241. data/lib/insika/studio/views/chats.erb +4 -1
  242. data/lib/insika/studio/views/customer.erb +94 -0
  243. data/lib/insika/studio/views/customers.erb +32 -0
  244. data/lib/insika/studio/views/evals.erb +6 -3
  245. data/lib/insika/studio/views/facts.erb +133 -0
  246. data/lib/insika/studio/views/followups.erb +125 -0
  247. data/lib/insika/studio/views/funnel.erb +106 -0
  248. data/lib/insika/studio/views/harvest.erb +234 -0
  249. data/lib/insika/studio/views/home.erb +106 -66
  250. data/lib/insika/studio/views/knowledge.erb +123 -0
  251. data/lib/insika/studio/views/layout.erb +15 -11
  252. data/lib/insika/studio/views/mcp.erb +174 -80
  253. data/lib/insika/studio/views/parity.erb +147 -0
  254. data/lib/insika/studio/views/playground.erb +7 -1
  255. data/lib/insika/studio/views/session.erb +233 -124
  256. data/lib/insika/studio/views/settings.erb +41 -1
  257. data/lib/insika/studio/views/skills.erb +1 -2
  258. data/lib/insika/studio/views/system_files.erb +1 -1
  259. data/lib/insika/studio/views/task.erb +13 -0
  260. data/lib/insika/studio/views/tasks.erb +4 -1
  261. data/lib/insika/studio/views/tools.erb +24 -10
  262. data/lib/insika/task_store.rb +21 -1
  263. data/lib/insika/templates/browser-agent/README.md +36 -0
  264. data/lib/insika/templates/browser-agent/agent.rb +49 -0
  265. data/lib/insika/templates/daily-digest/README.md +38 -0
  266. data/lib/insika/templates/daily-digest/agent.rb +77 -0
  267. data/lib/insika/templates/repo-explorer/README.md +36 -0
  268. data/lib/insika/templates/repo-explorer/agent.rb +45 -0
  269. data/lib/insika/templates/research-analyst/README.md +26 -0
  270. data/lib/insika/templates/research-analyst/agent.rb +58 -0
  271. data/lib/insika/templates/review-panel/README.md +20 -0
  272. data/lib/insika/templates/review-panel/agent.rb +50 -0
  273. data/lib/insika/templates/travel-planner/README.md +35 -0
  274. data/lib/insika/templates/travel-planner/agent.rb +87 -0
  275. data/lib/insika/templates.rb +112 -0
  276. data/lib/insika/testing/store_contract.rb +27 -6
  277. data/lib/insika/tick.rb +47 -11
  278. data/lib/insika/timezone.rb +45 -0
  279. data/lib/insika/tool_definition.rb +17 -7
  280. data/lib/insika/tool_envelope.rb +69 -0
  281. data/lib/insika/tool_manifest.rb +5 -1
  282. data/lib/insika/tools/data_defined_tool.rb +10 -0
  283. data/lib/insika/tools/generate_image.rb +89 -0
  284. data/lib/insika/tools/load_knowledge.rb +74 -0
  285. data/lib/insika/tools/run_persona_eval.rb +328 -0
  286. data/lib/insika/tools/save_artifact.rb +95 -0
  287. data/lib/insika/tools/schedule_followup.rb +164 -0
  288. data/lib/insika/tools/tts.rb +47 -0
  289. data/lib/insika/tools/update_briefing.rb +126 -0
  290. data/lib/insika/turn_output.rb +1 -1
  291. data/lib/insika/turn_state.rb +49 -1
  292. data/lib/insika/turn_timing.rb +22 -2
  293. data/lib/insika/version.rb +1 -1
  294. data/lib/insika/vitals.rb +84 -0
  295. data/lib/insika/wiring/graph.rb +472 -13
  296. data/lib/insika/wiring/graph_chat.rb +102 -0
  297. data/lib/insika.rb +149 -3
  298. metadata +181 -5
  299. data/docs/build.md +0 -14
  300. data/docs/understand.md +0 -10
@@ -1,285 +1,69 @@
1
- <header class="page-head">
2
- <div>
3
- <nav class="crumbs"><a href="/studio/agents">agents</a> / <span><%= @agent.id %></span></nav>
4
- <h1><span class="avatar sm"><%= @agent.id[0, 2].upcase %></span><%= @agent.id %>
5
- <span class="sub"><%= @agent.model %><%== " · #{h(@agent.provider)}" if @agent.provider %></span>
6
- </h1>
7
- </div>
8
- <a class="btn primary" href="/studio/playground?agent=<%= Rack::Utils.escape(@agent.id) %>">Open playground →</a>
9
- </header>
10
-
11
- <nav class="subnav">
12
- <a href="#config">config</a>
13
- <a href="#prompts">prompts</a>
14
- <a href="#skills">skills</a>
15
- <a href="#memory">memory</a>
16
- <a href="#history">history</a>
17
- </nav>
18
-
19
- <!-- ── Config / model ────────────────────────────────────────────────────── -->
20
- <section class="section" id="config">
21
- <div class="section-head"><h2>Configuration</h2><p class="muted">model, provider, memory, limits, generation params and the model fence — saved via <code>update_agent</code>.</p></div>
22
- <form class="card" method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/config">
23
- <%== csrf_tag %>
24
- <div class="grid-2">
25
- <label>model <span class="muted">(blank = platform <code>default_model</code>)</span>
26
- <input type="text" name="model" value="<%= @agent.model %>" placeholder="inherit platform default">
27
- </label>
28
- <label>provider
29
- <input type="text" name="provider" value="<%= @agent.provider %>" placeholder="deepseek">
30
- </label>
31
- <label>turn_timeout (s)
32
- <input type="text" name="turn_timeout" value="<%= @agent.limits[:turn_timeout] %>" inputmode="numeric">
33
- </label>
34
- <label>tool_timeout (s)
35
- <input type="text" name="tool_timeout" value="<%= @agent.limits[:tool_timeout] %>" inputmode="numeric">
36
- </label>
37
- <label>tool_concurrency <span class="muted">(parallel tool calls; 1 = serial, off for turns with approvals)</span>
38
- <input type="text" name="tool_concurrency" value="<%= @agent.limits[:tool_concurrency] %>" inputmode="numeric">
39
- </label>
40
- <label>chat_rate_limit <span class="muted">(turns/chat/window; blank = platform, 0 = off)</span>
41
- <input type="text" name="chat_rate_limit" value="<%= @agent.limits[:chat_rate_limit] %>" inputmode="numeric" placeholder="inherit platform edge">
42
- </label>
43
- <label>agent_token_ceiling <span class="muted">(tokens/window; blank = platform, 0 = off)</span>
44
- <input type="text" name="agent_token_ceiling" value="<%= @agent.limits[:agent_token_ceiling] %>" inputmode="numeric" placeholder="inherit platform edge">
45
- </label>
1
+ <%# Agent detail — the DETAIL pane of the Agents miller shell.
2
+ Full page (direct visit/refresh): the two-column shell — master list plus
3
+ this pane. Turbo-Frame request (a row click, a config save, a prompt-file
4
+ switch): the pane alone, so the master's scroll state and selection
5
+ survive every interaction. The tab sections live in _agent_tab_* partials
6
+ (one per section, named after it — no abstraction, just containment). %>
7
+ <%# Roda renders with plain locals — no local_assigns hash (Rails-ism). %>
8
+ <% frame_only = defined?(frame_only) && frame_only %>
9
+ <% unless frame_only %>
10
+ <header class="page-head">
11
+ <div>
12
+ <nav class="crumbs"><a href="/studio/agents">agents</a> / <span><%= @agent.id %></span></nav>
13
+ <h1><span class="avatar sm <%= avatar_class(@agent.id) %>"><%= @agent.id[0, 2].upcase %></span><%= @agent.id %>
14
+ <span class="sub"><%= @agent.model %><%== " · #{h(@agent.provider)}" if @agent.provider %></span>
15
+ </h1>
46
16
  </div>
47
- <label class="check">
48
- <input type="checkbox" name="memory" value="1"<%== " checked" if @agent.memory %>>
49
- cross-session memory (the BIA remembers across conversations)
50
- </label>
51
-
52
- <fieldset class="subgroup">
53
- <legend>generation params <span class="muted">— blank = provider default</span></legend>
54
- <div class="grid-3">
55
- <label>temperature
56
- <input type="text" name="temperature" value="<%= agent_param(@params, :temperature) %>" inputmode="decimal" placeholder="0.7">
57
- </label>
58
- <label>max_tokens
59
- <input type="text" name="max_tokens" value="<%= agent_param(@params, :max_tokens) %>" inputmode="numeric" placeholder="4096">
60
- </label>
61
- <label>reasoning <span class="muted">(off/on/effort — blank = inherit model/platform)</span>
62
- <%== thinking_select("thinking", agent_param(@params, :thinking), "inherit") %>
63
- </label>
64
- </div>
65
- </fieldset>
66
-
67
- <label>model fence <span class="muted">(<code>model_policy.allow</code> — one ref per line: <code>provider/model</code>, <code>provider/*</code> or <code>model</code>; blank = no fence)</span>
68
- <textarea name="model_policy_allow" rows="3" placeholder="deepseek/*&#10;openai/gpt-4o-mini"><%= @model_policy_allow %></textarea>
69
- </label>
70
-
71
- <fieldset class="subgroup">
72
- <legend>guardrails <span class="muted">— content safety</span></legend>
73
- <label class="check">
74
- <input type="checkbox" name="guardrail_input" value="1"<%== " checked" if guardrail_field(@guardrails, :input, true) %>>
75
- input guardrail <span class="muted">— block injection / abuse / inappropriate before the LLM</span>
76
- </label>
77
- <label class="check">
78
- <input type="checkbox" name="guardrail_output" value="1"<%== " checked" if guardrail_field(@guardrails, :output, true) %>>
79
- output guardrail <span class="muted">— redact PII/secret in the stream + flag policy slips</span>
80
- </label>
81
- <div class="grid-2">
82
- <label>strictness
83
- <% strictness = guardrail_field(@guardrails, :strictness, "medium").to_s %>
84
- <select name="guardrail_strictness">
85
- <% %w[low medium high].each do |lvl| %>
86
- <option value="<%= lvl %>"<%== " selected" if lvl == strictness %>><%= lvl %></option>
87
- <% end %>
88
- </select>
89
- </label>
90
- <label>LLM moderator <span class="muted">(model ref, or <code>on</code> for the platform utility_model; blank = deterministic only)</span>
91
- <input type="text" name="guardrail_moderator" value="<%= guardrail_field(@guardrails, :moderator, "") %>" placeholder="deepseek/deepseek-v4-flash">
92
- </label>
93
- </div>
94
- <details class="newfile">
95
- <summary>safe replies — override per category <span class="muted">(blank = engine default; this agent's voice/language)</span></summary>
96
- <% gr_resp = (@guardrails["responses"] || @guardrails[:responses] || {}) %>
97
- <label>default <span class="muted">(catch-all for any blocked category)</span>
98
- <textarea name="guardrail_response_default" rows="2" placeholder="I can't help with that particular request…"><%= guardrail_field(gr_resp, :default, "") %></textarea>
99
- </label>
100
- <div class="grid-2">
101
- <% %w[injection sexual abuse escalate].each do |cat| %>
102
- <label><%= cat %>
103
- <textarea name="guardrail_response_<%= cat %>" rows="2"><%= guardrail_field(gr_resp, cat, "") %></textarea>
104
- </label>
105
- <% end %>
106
- </div>
107
- </details>
108
- </fieldset>
17
+ <a class="btn primary" href="/studio/playground?agent=<%= Rack::Utils.escape(@agent.id) %>">Open playground →</a>
18
+ </header>
109
19
 
110
- <div class="actions"><button type="submit" class="primary" data-turbo-submits-with="Saving…">Save config</button></div>
111
- </form>
112
- </section>
113
-
114
- <!-- ── Prompts / identity (island code-editor) ───────────────────────────── -->
115
- <section class="section" id="prompts">
116
- <div class="section-head">
117
- <h2>Prompts <span class="pill info plain"><%= @prompt_files.size %></span></h2>
118
- <p class="muted">the BIA's own identity (store-backed, versioned). Empty = uses the deployment default.</p>
119
- </div>
120
-
121
- <% if @prompt_files.empty? %>
122
- <div class="empty">No authored prompts. Create a file below (e.g. <code>IDENTITY.md</code>) to give this agent its own identity.</div>
123
- <% end %>
124
-
125
- <% @prompt_files.each do |pf| %>
126
- <div class="card promptfile">
127
- <div class="promptfile-head">
128
- <strong class="mono"><%= pf[:name] %></strong>
129
- <form method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/prompts/delete" data-turbo-confirm="Remove prompt file &quot;<%= pf[:name] %>&quot;? This can't be undone.">
130
- <%== csrf_tag %>
131
- <input type="hidden" name="file" value="<%= pf[:name] %>">
132
- <button type="submit" class="ghost danger" data-turbo-submits-with="Removing…">Remove</button>
133
- </form>
134
- </div>
135
- <form method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/prompts" data-controller="dirty-guard">
136
- <%== csrf_tag %>
137
- <input type="hidden" name="file" value="<%= pf[:name] %>">
138
- <div data-controller="code-editor" data-code-editor-language-value="markdown" data-code-editor-preview-value="true">
139
- <textarea name="content" data-code-editor-target="source" rows="10"><%= pf[:content] %></textarea>
140
- </div>
141
- <div class="actions"><button type="submit" class="primary" data-turbo-submits-with="Saving…">Save prompt</button></div>
142
- </form>
143
- <% unless pf[:versions].empty? %>
144
- <details class="versions">
145
- <summary><%= pf[:versions].size %> previous version(s)</summary>
146
- <ul>
147
- <% pf[:versions].each_with_index do |v, i| %>
148
- <li>
149
- <span class="muted mono"><%= v["at"] %></span>
150
- <form method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/prompts/restore">
151
- <%== csrf_tag %>
152
- <input type="hidden" name="file" value="<%= pf[:name] %>">
153
- <input type="hidden" name="version" value="<%= i %>">
154
- <button type="submit" class="ghost">Restore</button>
155
- </form>
156
- </li>
157
- <% end %>
158
- </ul>
159
- </details>
20
+ <div class="drill" data-controller="list-filter">
21
+ <%== render("_agents_master", locals: { active_id: @agent.id }) %>
22
+ <% end %>
23
+ <turbo-frame id="agent-detail" class="drill-pane drill-detail">
24
+ <%# A frame response never sees the layout, so the flash strip lives here
25
+ too without it, save confirmations would vanish on frame submits
26
+ (the layout's copy only renders on full pages). %>
27
+ <% if frame_only %>
28
+ <% if flash["error"] %><div class="flash err" role="alert"><%= flash["error"] %></div><% end %>
29
+ <% if flash["notice"] %><div class="flash ok" role="status"><%= flash["notice"] %></div><% end %>
160
30
  <% end %>
161
- </div>
162
- <% end %>
163
-
164
- <details class="card newfile">
165
- <summary>+ new prompt file</summary>
166
- <form method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/prompts" data-controller="dirty-guard">
167
- <%== csrf_tag %>
168
- <label>file name
169
- <input type="text" name="file" placeholder="IDENTITY.md" required>
170
- </label>
171
- <div data-controller="code-editor" data-code-editor-language-value="markdown" data-code-editor-preview-value="true">
172
- <textarea name="content" data-code-editor-target="source" rows="8" placeholder="# Identity&#10;You are..."></textarea>
31
+ <div class="drill-pane-head">
32
+ <div class="title">
33
+ <strong class="mono"><%= @agent.id %></strong>
34
+ <span class="count"><%= @agent.model %><%== " · #{h(@agent.provider)}" if @agent.provider %></span>
35
+ </div>
36
+ <a class="btn btn-sm" href="/studio/playground?agent=<%= Rack::Utils.escape(@agent.id) %>" data-turbo-frame="_top">Test in playground</a>
173
37
  </div>
174
- <div class="actions"><button type="submit" class="primary" data-turbo-submits-with="Creating…">Create prompt</button></div>
175
- </form>
176
- </details>
177
- </section>
38
+ <div class="drill-pane-body">
39
+ <div class="tabs-page" data-controller="tabs" data-tabs-default-value="<%= @active_tab %>">
40
+ <nav class="subnav" aria-label="agent sections">
41
+ <%# data-turbo="false": these are plain in-page hash links. Without
42
+ the opt-out, Turbo intercepts them as frame navigations (they sit
43
+ inside turbo-frame#agent-detail), dropping the #fragment and
44
+ re-fetching the frame instead of letting the browser set
45
+ location.hash — which is the only thing tabs_controller reads. %>
46
+ <a href="#config" data-tabs-target="tab" data-turbo="false">config</a>
47
+ <a href="#prompts" data-tabs-target="tab" data-turbo="false">prompts</a>
48
+ <a href="#skills" data-tabs-target="tab" data-turbo="false">skills</a>
49
+ <a href="#memory" data-tabs-target="tab" data-turbo="false">memory</a>
50
+ <a href="#outcomes" data-tabs-target="tab" data-turbo="false">outcomes</a>
51
+ <a href="#loops" data-tabs-target="tab" data-turbo="false">loops</a>
52
+ <a href="#cache" data-tabs-target="tab" data-turbo="false">cache</a>
53
+ <a href="#history" data-tabs-target="tab" data-turbo="false">history</a>
54
+ </nav>
178
55
 
179
- <!-- ── Agent skills ──────────────────────────────────────────────────────── -->
180
- <section class="section" id="skills">
181
- <div class="section-head">
182
- <h2>Skills</h2>
183
- <p class="muted">which catalog skills this agent may load (<code>load_skill</code>).</p>
184
- </div>
185
- <form class="card" method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/skills">
186
- <%== csrf_tag %>
187
- <label class="check">
188
- <input type="checkbox" name="all_skills" value="1"<%== " checked" if @agent_skills.nil? %>>
189
- all skills (open allowlist)
190
- </label>
191
- <% if @all_skills.empty? %>
192
- <p class="muted">No skills in the catalog. Create one under <a href="/studio/skills/new">skills → new</a>.</p>
193
- <% else %>
194
- <div class="check-grid">
195
- <% @all_skills.sort_by(&:name).each do |s| %>
196
- <label class="check">
197
- <input type="checkbox" name="skills[]" value="<%= s.name %>"<%== " checked" if @agent_skills&.include?(s.name) %>>
198
- <span class="mono"><%= s.name %></span>
199
- </label>
200
- <% end %>
56
+ <%== render("_agent_tab_config") %>
57
+ <%== render("_agent_tab_prompts") %>
58
+ <%== render("_agent_tab_skills") %>
59
+ <%== render("_agent_tab_memory") %>
60
+ <%== render("_agent_tab_loops") %>
61
+ <%== render("_agent_tab_outcomes") %>
62
+ <%== render("_agent_tab_cache") %>
63
+ <%== render("_agent_tab_history") %>
64
+ </div>
201
65
  </div>
202
- <% end %>
203
- <div class="actions">
204
- <button type="submit" class="primary" data-turbo-submits-with="Saving…">Save skills</button>
205
- <a class="btn ghost" href="/studio/tools">Edit tools →</a>
206
- </div>
207
- </form>
208
- </section>
209
-
210
- <!-- ── Agent memory (tenant = id) ────────────────────────────────────────── -->
211
- <section class="section" id="memory">
212
- <div class="section-head">
213
- <h2>Memory</h2>
214
- <p class="muted">facts and notes the BIA reads each turn (tenant <code><%= @agent.id %></code>).</p>
215
- </div>
216
- <div class="card">
217
- <h3>Facts</h3>
218
- <% if @facts.empty? %>
219
- <p class="muted">No facts.</p>
220
- <% else %>
221
- <ul class="kv">
222
- <% @facts.each do |f| %>
223
- <li>
224
- <span class="k mono"><%= f.key %></span>
225
- <span class="v"><%= f.value %></span>
226
- <form method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/memory/forget" data-turbo-confirm="Forget &quot;<%= f.key %>&quot;? The agent loses this fact from the next turn on.">
227
- <%== csrf_tag %>
228
- <input type="hidden" name="key" value="<%= f.key %>">
229
- <button type="submit" class="ghost danger" data-turbo-submits-with="Forgetting…">Forget</button>
230
- </form>
231
- </li>
232
- <% end %>
233
- </ul>
234
- <% end %>
235
- <form class="inline-form" method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/memory/fact">
236
- <%== csrf_tag %>
237
- <input type="text" name="key" placeholder="key (e.g. customer_name)" required>
238
- <input type="text" name="value" placeholder="value" required>
239
- <button type="submit" class="primary" data-turbo-submits-with="Saving…">Save fact</button>
240
- </form>
241
- </div>
242
-
243
- <div class="card">
244
- <h3>Notes <span class="muted">(recent)</span></h3>
245
- <% if @notes.empty? %>
246
- <p class="muted">No notes.</p>
247
- <% else %>
248
- <ul class="notes">
249
- <% @notes.each do |n| %>
250
- <li><span class="muted mono"><%= n.created_at %></span> <%= n.text %></li>
251
- <% end %>
252
- </ul>
253
- <% end %>
254
- <form class="inline-form" method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/memory/note">
255
- <%== csrf_tag %>
256
- <input type="text" name="text" placeholder="free-form note…" required>
257
- <button type="submit" class="primary" data-turbo-submits-with="Adding…">Add note</button>
258
- </form>
66
+ </turbo-frame>
67
+ <% unless frame_only %>
259
68
  </div>
260
- </section>
261
-
262
- <!-- ── History ───────────────────────────────────────────────────────────── -->
263
- <section class="section" id="history">
264
- <div class="section-head">
265
- <h2>History</h2>
266
- <p class="muted">recent conversations (all agents — a session doesn't stamp its author).</p>
267
- </div>
268
- <% if @recent_sessions.empty? %>
269
- <div class="empty">No conversations yet. Start one in the <a href="/studio/playground?agent=<%= Rack::Utils.escape(@agent.id) %>">playground</a>.</div>
270
- <% else %>
271
- <div class="card">
272
- <ul class="history">
273
- <% @recent_sessions.each do |s| %>
274
- <li>
275
- <a href="/studio/sessions/<%= Rack::Utils.escape(s.id) %>">
276
- <span class="mono"><%= s.id[0, 12] %></span>
277
- </a>
278
- <span class="muted"><%= Array(s.messages).size %> msg · <%= s.updated_at %></span>
279
- <% if (p = session_preview(s)) %><span class="preview muted"><%= p[0, 80] %></span><% end %>
280
- </li>
281
- <% end %>
282
- </ul>
283
- </div>
284
- <% end %>
285
- </section>
69
+ <% end %>
@@ -1,63 +1,80 @@
1
+ <%# Agents — miller columns: master list | detail frame. A row
2
+ click loads the detail beside the list (no full page load) and advances the
3
+ URL. The detail pane's landing state hosts the template gallery and the
4
+ create form; both submit to the top window so the fresh master list
5
+ renders with the new agent in it. %>
1
6
  <header class="page-head">
2
- <h1>Agents<span class="sub"><%= @agents.size %> configured · one BIA each</span></h1>
7
+ <h1>Agents<span class="sub"><%= @agents.size %> configured · one agent each</span></h1>
3
8
  <a class="btn primary" href="/studio/playground">Open playground →</a>
4
9
  </header>
5
10
 
6
- <% if @agents.empty? %>
7
- <div class="empty">No agents yet. Create your first BIA below (id + model), then give it an identity on its detail page.</div>
8
- <% else %>
9
- <div data-controller="list-filter">
10
- <div class="filter-bar">
11
- <input type="search" class="filter-input" placeholder="Filter agents by id, model or provider…" aria-label="Filter agents"
12
- data-list-filter-target="query" data-action="input->list-filter#filter keydown->list-filter#clear">
13
- <span class="filter-count mono muted" data-list-filter-target="count"></span>
11
+ <div class="drill" data-controller="list-filter">
12
+ <%== render("_agents_master", locals: { active_id: nil, show_outcomes: true }) %>
13
+
14
+ <turbo-frame id="agent-detail" class="drill-pane drill-detail">
15
+ <div class="drill-pane-head">
16
+ <div class="title">
17
+ <strong>Pick an agent</strong>
18
+ <span class="count">select on the left, or start a new one below</span>
19
+ </div>
14
20
  </div>
15
- <div class="agent-grid">
16
- <% @agents.each do |a| %>
17
- <article class="card agent" data-list-filter-target="item" data-filter-text="<%= a.id %> <%= a.model %> <%= a.provider %>">
18
- <a class="agent-head" href="/studio/agents/<%= Rack::Utils.escape(a.id) %>">
19
- <span class="avatar"><%= a.id[0, 2].upcase %></span>
20
- <div>
21
- <div class="agent-id"><%= a.id %></div>
22
- <div class="muted mono"><%= a.model %><%== " · #{h(a.provider)}" if a.provider %></div>
21
+ <div class="drill-pane-body">
22
+ <% if @templates&.any? %>
23
+ <div class="section">
24
+ <div class="section-head">
25
+ <h2>From template</h2>
26
+ <p class="muted">Example agents shipped in the gem — pick one to see a working pattern, then edit it like any agent.</p>
27
+ </div>
28
+ <div class="template-grid">
29
+ <% @templates.each do |t| %>
30
+ <article class="card template">
31
+ <div class="template-head">
32
+ <strong><%= t.title %></strong>
33
+ <span class="pill info"><%= t.trail %></span>
34
+ </div>
35
+ <p class="muted"><%= t.description %></p>
36
+ <% if t.capabilities.any? %>
37
+ <p class="mono muted template-caps"><%= t.capabilities.join(" · ") %></p>
38
+ <% end %>
39
+ <% if t.studio? %>
40
+ <form method="post" action="/studio/agents/from_template/<%= Rack::Utils.escape(t.name) %>" data-turbo-frame="_top">
41
+ <%== csrf_tag %>
42
+ <button type="submit" class="btn" data-turbo-submits-with="Creating…">Create</button>
43
+ </form>
44
+ <% else %>
45
+ <p class="muted">CLI-only for now — run <code>insika new <%= t.name %></code>.</p>
46
+ <% end %>
47
+ </article>
48
+ <% end %>
23
49
  </div>
24
- <% if a.memory %><span class="pill info">memory</span><% end %>
25
- </a>
26
- <dl class="agent-meta">
27
- <div><dt>tools</dt><dd><%= a.tools_allow.nil? ? "all" : a.tools_allow.size %></dd></div>
28
- <div><dt>skills</dt><dd><%= a.skills.nil? ? "all" : Array(a.skills).size %></dd></div>
29
- <div><dt>prompts</dt><dd><%= Array(a.prompt_files).size %></dd></div>
30
- </dl>
31
- <div class="agent-actions">
32
- <a class="btn" href="/studio/agents/<%= Rack::Utils.escape(a.id) %>">Edit</a>
33
- <a class="btn ghost" href="/studio/playground?agent=<%= Rack::Utils.escape(a.id) %>">Test</a>
34
50
  </div>
35
- </article>
36
- <% end %>
37
- </div>
38
- <div class="empty" data-list-filter-target="empty" hidden>No agents match your filter.</div>
39
- </div>
40
- <% end %>
51
+ <% end %>
41
52
 
42
- <details class="card newfile"<%== " open" if @agents.empty? %>>
43
- <summary>+ new agent</summary>
44
- <form method="post" action="/studio/agents">
45
- <%== csrf_tag %>
46
- <div class="grid-2">
47
- <label>id <span class="muted">(unique slug, e.g. bia)</span>
48
- <input type="text" name="id" placeholder="bia" required>
49
- </label>
50
- <label>model
51
- <input type="text" name="model" placeholder="deepseek-v4-flash" required>
52
- </label>
53
+ <div class="section">
54
+ <div class="section-head">
55
+ <h2>New agent</h2>
56
+ <p class="muted">id + model — give it an identity on its detail page.</p>
57
+ </div>
58
+ <form method="post" action="/studio/agents" data-turbo-frame="_top">
59
+ <%== csrf_tag %>
60
+ <div class="grid-2">
61
+ <label>id <span class="muted">(unique slug, e.g. demo)</span>
62
+ <input type="text" name="id" placeholder="demo" required>
63
+ </label>
64
+ <label>model
65
+ <input type="text" name="model" placeholder="deepseek-v4-flash" required>
66
+ </label>
67
+ </div>
68
+ <label>provider <span class="muted">(optional)</span>
69
+ <input type="text" name="provider" placeholder="deepseek">
70
+ </label>
71
+ <label class="check">
72
+ <input type="checkbox" name="memory" value="1" checked>
73
+ cross-session memory (the agent remembers across conversations)
74
+ </label>
75
+ <div class="actions"><button type="submit" class="primary" data-turbo-submits-with="Creating…">Create agent</button></div>
76
+ </form>
77
+ </div>
53
78
  </div>
54
- <label>provider <span class="muted">(optional)</span>
55
- <input type="text" name="provider" placeholder="deepseek">
56
- </label>
57
- <label class="check">
58
- <input type="checkbox" name="memory" value="1" checked>
59
- cross-session memory (the BIA remembers across conversations)
60
- </label>
61
- <div class="actions"><button type="submit" class="primary" data-turbo-submits-with="Creating…">Create agent</button></div>
62
- </form>
63
- </details>
79
+ </turbo-frame>
80
+ </div>
@@ -3,7 +3,10 @@
3
3
  <nav class="crumbs"><span>approvals</span></nav>
4
4
  <h1>Approvals<span class="sub"><%= @approvals.size %> pending</span></h1>
5
5
  </div>
6
- <a class="btn" href="/studio/tasks">Tasks →</a>
6
+ <div class="head-actions">
7
+ <%== agent_filter_form("/studio/approvals", @agent) %>
8
+ <a class="btn" href="/studio/tasks">Tasks →</a>
9
+ </div>
7
10
  </header>
8
11
 
9
12
  <% if @approvals.empty? %>
@@ -0,0 +1,23 @@
1
+ <%# the artifact preview page. The content renders ONLY inside a SANDBOXED
2
+ iframe (no scripts, no same-origin, no forms) on top of the content
3
+ route's own restrictive CSP — artifact content is LLM output, untrusted. %>
4
+ <header class="page-head">
5
+ <div>
6
+ <nav class="crumbs"><a href="/studio/artifacts">Artifacts</a></nav>
7
+ <h1><%= @artifact.title %><span class="sub"><span class="mono"><%= @artifact.id %></span> · <%= @artifact.mime %> · <%= @artifact.created_at %></span></h1>
8
+ </div>
9
+ <div class="head-actions">
10
+ <% if @signed_url %>
11
+ <button type="button" class="btn sm" data-copy="<%= @signed_url %>">copy signed link</button>
12
+ <% end %>
13
+ <a class="btn sm ghost" href="<%= @content_url %>" target="_blank" rel="noopener">open raw</a>
14
+ <form method="post" action="/studio/artifacts/<%= Rack::Utils.escape_path(@artifact.id) %>/delete" class="inline">
15
+ <%== csrf_tag %>
16
+ <button class="btn sm ghost danger">delete</button>
17
+ </form>
18
+ </div>
19
+ </header>
20
+
21
+ <div class="card artifact-frame">
22
+ <iframe sandbox src="<%= @content_url %>" title="<%= @artifact.title %>"></iframe>
23
+ </div>
@@ -0,0 +1,59 @@
1
+ <%# the Artifacts page: the report destination. Reads the ArtifactStore
2
+ directly (the listing IS the history); the only mutation — delete —
3
+ dispatches :delete_artifact on the bus. The preview is a sandboxed iframe
4
+ on top of the content route's own restrictive CSP. %>
5
+ <header class="page-head">
6
+ <div>
7
+ <nav class="crumbs"><span>operate</span></nav>
8
+ <h1>Artifacts<span class="sub">the report destination — saved pages, per agent</span></h1>
9
+ </div>
10
+ <div class="head-actions">
11
+ <%== agent_filter_form("/studio/artifacts", @agent) %>
12
+ </div>
13
+ </header>
14
+
15
+ <% if @artifacts.empty? %>
16
+ <div class="empty">
17
+ <% if @agent %>
18
+ No artifacts for <span class="mono"><%= @agent %></span> — an agent that
19
+ has <span class="mono">save_artifact</span> in its <span class="mono">tools_allow</span>
20
+ writes its reports here.
21
+ <% else %>
22
+ No artifacts yet, for any agent.
23
+ <% end %>
24
+ </div>
25
+ <% else %>
26
+ <h2><%= @agent || "all agents" %><span class="sub"><%= @artifacts.size %> artifact(s)</span></h2>
27
+ <div class="card">
28
+ <table class="grid">
29
+ <thead>
30
+ <tr>
31
+ <% unless @agent %><th>agent</th><% end %>
32
+ <th>title</th><th>mime</th><th>task</th><th>created_at</th><th></th>
33
+ </tr>
34
+ </thead>
35
+ <tbody>
36
+ <% @artifacts.each do |a| %>
37
+ <tr>
38
+ <% unless @agent %><td class="mono"><%= a.agent %></td><% end %>
39
+ <td><a href="/studio/artifacts/<%= Rack::Utils.escape_path(a.id) %>"><%= a.title %></a></td>
40
+ <td><span class="pill"><%= a.mime %></span></td>
41
+ <td class="mono"><%= a.task_id %></td>
42
+ <td class="mono"><%= a.created_at %></td>
43
+ <td class="row-actions">
44
+ <% if @signed_available %>
45
+ <button type="button" class="btn ghost sm" data-copy="<%= signed_artifact_url(a.id) %>">copy link</button>
46
+ <% end %>
47
+ <form method="post" action="/studio/artifacts/<%= Rack::Utils.escape_path(a.id) %>/delete"
48
+ class="inline" data-confirm="Delete this artifact?">
49
+ <%== csrf_tag %>
50
+ <input type="hidden" name="agent" value="<%= a.agent %>">
51
+ <button class="btn ghost sm danger">delete</button>
52
+ </form>
53
+ </td>
54
+ </tr>
55
+ <% end %>
56
+ </tbody>
57
+ </table>
58
+ </div>
59
+ <% end %>
@@ -3,7 +3,10 @@
3
3
  <nav class="crumbs"><a href="/studio/agents">agents</a> / <span>chats</span></nav>
4
4
  <h1>Chats<span class="sub"><%= @sessions.size %> recent conversation(s)</span></h1>
5
5
  </div>
6
- <a class="btn primary" href="/studio/playground">New conversation →</a>
6
+ <div class="head-actions">
7
+ <%== agent_filter_form("/studio/chats", @agent) %>
8
+ <a class="btn primary" href="/studio/playground">New conversation →</a>
9
+ </div>
7
10
  </header>
8
11
 
9
12
  <% if @sessions.empty? %>