envoy_ai 0.0.1 → 0.0.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +181 -2
  3. data/app/assets/stylesheets/envoy/chat.css +209 -0
  4. data/app/assets/stylesheets/envoy/console.css +50 -176
  5. data/app/controllers/envoy/conversations_controller.rb +1 -4
  6. data/app/controllers/envoy/panels_controller.rb +30 -0
  7. data/app/javascript/envoy/controllers/autoscroll_controller.js +26 -32
  8. data/app/jobs/envoy/run_job.rb +25 -4
  9. data/app/models/envoy/conversation.rb +33 -6
  10. data/app/views/envoy/conversations/_composer_input.html.erb +1 -1
  11. data/app/views/envoy/conversations/index.html.erb +0 -2
  12. data/app/views/envoy/conversations/new.html.erb +1 -7
  13. data/app/views/envoy/conversations/show.html.erb +1 -17
  14. data/app/views/envoy/messages/_error.html.erb +6 -0
  15. data/app/views/envoy/messages/_streaming.html.erb +1 -1
  16. data/app/views/envoy/messages/create.turbo_stream.erb +5 -5
  17. data/app/views/envoy/panels/_panel.html.erb +3 -0
  18. data/app/views/envoy/panels/_transcript.html.erb +21 -0
  19. data/config/routes.rb +1 -1
  20. data/db/migrate/20260716000001_drop_envoy_system_prompts.rb +12 -0
  21. data/db/migrate/20260716000002_add_surface_to_envoy_conversations.rb +15 -0
  22. data/lib/envoy/engine.rb +6 -0
  23. data/lib/envoy/errors.rb +4 -0
  24. data/lib/envoy/guard.rb +13 -1
  25. data/lib/envoy/library.rb +82 -0
  26. data/lib/envoy/llm.rb +1 -1
  27. data/lib/envoy/page_surface.rb +34 -0
  28. data/lib/envoy/prompt.rb +55 -0
  29. data/lib/envoy/runner.rb +17 -3
  30. data/lib/envoy/surface.rb +90 -0
  31. data/lib/envoy/tool_definition.rb +12 -2
  32. data/lib/envoy/version.rb +1 -1
  33. data/lib/envoy.rb +59 -0
  34. metadata +12 -12
  35. data/app/controllers/envoy/system_prompts_controller.rb +0 -52
  36. data/app/models/envoy/system_prompt.rb +0 -24
  37. data/app/models/envoy/system_prompt_version.rb +0 -14
  38. data/app/views/envoy/system_prompts/_form.html.erb +0 -14
  39. data/app/views/envoy/system_prompts/edit.html.erb +0 -2
  40. data/app/views/envoy/system_prompts/index.html.erb +0 -14
  41. data/app/views/envoy/system_prompts/new.html.erb +0 -2
  42. data/app/views/envoy/system_prompts/show.html.erb +0 -15
  43. data/db/migrate/20260711000001_create_envoy_system_prompts.rb +0 -10
  44. data/db/migrate/20260711000002_create_envoy_system_prompt_versions.rb +0 -13
  45. data/db/migrate/20260711000003_add_system_prompt_version_to_envoy_conversations.rb +0 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 65af52593c07af82aa70d2e0f32876bebf053d6cd1cce31cdaf914fe76aac996
4
- data.tar.gz: a0c236d33db995bdbd8e1bcbb6ff7eab428ac6aba5d4294b7b49273efbda16c6
3
+ metadata.gz: 72b0cf7f247e26cf0c6d1aa41f24b29a8e2fee0501476e83ebc0dab5df927a01
4
+ data.tar.gz: a7d6213134ee171ac420047502d31d05fdd562f2c49fa718dad2ee4d0d86981e
5
5
  SHA512:
6
- metadata.gz: 98ff01d5c0134a18f4f39809b5e881418ea8e38d4117e84351ec4dd13e34db1a0b589f8cde715f6c07c147534f3af5fa96022254eceef09c8aec86d36bcaee8b
7
- data.tar.gz: abebd4786665308b9988f3bed11333f34b7603803a57e7cdd98eed26b205e86572626882629467a52fb204664e3d6f0a9760456d367df286185fdf14414743a2
6
+ metadata.gz: 32eb9d905212b9a2a1e63f2aa21d15147a65500015a48e7e07b08760d137fc3fb80507534e875e158f854d8c1c3557412e86b64fd5478736dde09fe2d4bce856
7
+ data.tar.gz: 8e95fe42202f1f457488b7ec2b6f4d7bda439e082ca29c057cafe2c5fbd660f840e99e339597abfe389cf4e29a30094a36dec3ea03c8a9d4dc53ce20e7a108fc
data/README.md CHANGED
@@ -135,14 +135,19 @@ result) — a user can only see and post to their own conversations.
135
135
  ## 5. Migrations
136
136
 
137
137
  Envoy ships its own migrations (`envoy_conversations`, `envoy_messages`,
138
- `envoy_tool_calls`, `envoy_system_prompts`, `envoy_system_prompt_versions`,
139
- plus the FK linking a conversation to the system-prompt version it used):
138
+ `envoy_tool_calls`, plus `surface_key`/`context_key`/`prompt_key` columns on
139
+ `envoy_conversations` for surface-bound and DSL-prompt conversations):
140
140
 
141
141
  ```bash
142
142
  bin/rails envoy:install:migrations
143
143
  bin/rails db:migrate
144
144
  ```
145
145
 
146
+ If you installed Envoy before this version, `envoy:install:migrations` also
147
+ copies over a migration that drops `envoy_system_prompts` and
148
+ `envoy_system_prompt_versions` — see the Unreleased section of the CHANGELOG
149
+ before running `db:migrate` on an app with real rows in those tables.
150
+
146
151
  ## 6. Assets
147
152
 
148
153
  The engine ships two Stimulus controllers under its own `app/javascript`
@@ -341,6 +346,180 @@ bundle exec bin/test test/envoy/guard_test.rb
341
346
  bundle exec bin/test test/envoy/guard_test.rb:12
342
347
  ```
343
348
 
349
+ ## 12. Embedding a chat
350
+
351
+ The console (section 6 onward) is a full page. Most hosts instead want a small
352
+ chat panel bolted onto an existing page — "ask about this record" next to a
353
+ show view, say — without adopting the console's chrome or its mounted URL.
354
+ That's what a **surface** is for: it binds a page to a toolset, a model and a
355
+ prompt, and a mounted `POST` endpoint turns that binding into a Turbo Frame
356
+ you can drop anywhere.
357
+
358
+ ### Define a surface
359
+
360
+ ```ruby
361
+ # config/initializers/envoy.rb, alongside the toolset force-load block
362
+ Envoy.define_surface :today do
363
+ toolset :assistant # a registered Envoy.define_toolset key
364
+ model "claude-sonnet-4-5" # optional; falls back to config.default_model
365
+ system_prompt :today_page # a Prompt key (see below), or a literal String
366
+ read_only true # optional; hides `access :write` tools entirely
367
+
368
+ # Runs in the job for every turn — see the callout below.
369
+ context do |actor:, key:|
370
+ date = key.delete_prefix("day:")
371
+ "The user is looking at the training day for #{date}."
372
+ end
373
+ end
374
+ ```
375
+
376
+ `toolset` is required; `Envoy.validate!` (see below) fails fast if it names an
377
+ unregistered toolset or an unregistered `system_prompt` key. `model`,
378
+ `read_only` and `system_prompt` are all optional.
379
+
380
+ ### Define prompts and libraries
381
+
382
+ `Envoy.define_prompt` registers reusable system-prompt text, composable with
383
+ `use` (like a toolset) so shared framing lives in one place:
384
+
385
+ ```ruby
386
+ Envoy.define_prompt :safety_framing do
387
+ body "Never invent data; say when you don't know."
388
+ end
389
+
390
+ Envoy.define_prompt :today_page do
391
+ use :safety_framing
392
+ body { File.read(Rails.root.join("app/envoy/prompts/today_page.md")) }
393
+ end
394
+ ```
395
+
396
+ A block body stays lazy, so editing the file on disk is picked up on the next
397
+ `to_prepare` reload rather than requiring a boot.
398
+
399
+ `Envoy.define_library` is for reference documents the model should fetch on
400
+ demand rather than have preloaded into every turn's instructions — it
401
+ compiles to an ordinary toolset (so it composes via `use` on a real
402
+ toolset), exposing one `read_<key>` tool with a closed `enum` of document
403
+ keys:
404
+
405
+ ```ruby
406
+ Envoy.define_library :principles do
407
+ directory Rails.root.join("docs/principles") # README.md -> description, *.md -> documents
408
+ end
409
+
410
+ Envoy.define_toolset :assistant do
411
+ description "General assistant tools."
412
+ use :principles
413
+ end
414
+ ```
415
+
416
+ ### Wire a page to it
417
+
418
+ Any host controller can call `envoy_surface` — it's mixed into
419
+ `ActionController::Base` by the engine, no include needed:
420
+
421
+ ```ruby
422
+ class TodayController < ApplicationController
423
+ envoy_surface :today, context_key: -> { "day:#{@date.to_fs(:iso8601)}" }
424
+ end
425
+ ```
426
+
427
+ `context_key` is instance_exec'd in the controller, so instance variables the
428
+ action set (`@date` here) are in scope. The helper method `envoy_page_surface`
429
+ (available in views) returns `{ surface:, context_key: }`, or `nil` if the
430
+ controller never declared a surface — pages opt in, they don't get a bubble by
431
+ default.
432
+
433
+ > **A `context` block runs in the job.** There is no request, no `params` and
434
+ > no session — only `actor` and `key`. Everything it needs must be derivable
435
+ > from the `context_key`. It is also the authorization boundary: `surface` and
436
+ > `context_key` both arrive from the browser, so raise `Envoy::Forbidden` if
437
+ > this actor may not see this subject.
438
+
439
+ ### Render the panel
440
+
441
+ `POST envoy.panel_path` (mounted by `resource :panel, only: :create`) finds
442
+ or creates the conversation for `(actor, surface, context_key)` and renders it
443
+ inside a `turbo_frame_tag "envoy_panel"`. Point a `button_to` at it with a
444
+ matching frame already on the page — same "create-or-resume" semantics as
445
+ opening a tab twice, so re-clicking never duplicates the conversation:
446
+
447
+ ```erb
448
+ <% if (surface = envoy_page_surface) %>
449
+ <%= turbo_frame_tag "envoy_panel" %>
450
+ <%= button_to "Ask about this page", envoy.panel_path,
451
+ params: { surface: surface[:surface], context_key: surface[:context_key] },
452
+ data: { turbo_frame: "envoy_panel" } %>
453
+ <% end %>
454
+ ```
455
+
456
+ An unregistered `surface` param renders `404`; the endpoint is otherwise
457
+ subject to the same `authenticate`/`actor_resolver` config as every other
458
+ Envoy route, so a signed-out visitor never reaches it.
459
+
460
+ ### Style it
461
+
462
+ The panel (and the console) render the same
463
+ `app/views/envoy/panels/_transcript.html.erb` partial, styled entirely by
464
+ `app/assets/stylesheets/envoy/chat.css` — no body rules, no fixed widths, the
465
+ host owns the chrome around it. `console.css` pulls it in automatically
466
+ (`@import url("chat.css")`, so loading the console stylesheet already gives
467
+ you these rules too), or import it directly from the host's own stylesheet if
468
+ you don't want the console's chrome at all:
469
+
470
+ ```css
471
+ @import url("envoy/chat.css");
472
+ ```
473
+
474
+ Use the `url(...)` form, not a bare string — Propshaft only fingerprints
475
+ `url(...)` references in CSS, not `@import` strings, and its asset server
476
+ 404s on an undigested path.
477
+
478
+ Every color it uses is a CSS custom property, and every declaration falls
479
+ back to its own original console value when the var is unset, so it renders
480
+ reasonably with zero host configuration and themes cleanly by setting only
481
+ the vars below:
482
+
483
+ | Var | Affects | Fallback (console light mode) |
484
+ |---|---|---|
485
+ | `--envoy-surface` | message bubble / input background | `#ffffff` |
486
+ | `--envoy-surface-alt` | assistant bubble background *and* tool-call panel background | `#eff6ff` / `#f9fafb` (each rule keeps its own original shade until you set this) |
487
+ | `--envoy-ink` | body text | `#111827` |
488
+ | `--envoy-subtle` | role labels, hints, placeholder text | `#6b7280` |
489
+ | `--envoy-line` | borders | `#e5e7eb` |
490
+ | `--envoy-accent` | primary button background | `#111827` |
491
+ | `--envoy-accent-ink` | primary button text | `#ffffff` |
492
+ | `--envoy-warn` | the failed-turn error bubble | `#b91c1c` |
493
+ | `--envoy-radius` | corner rounding (message/tool/input/button each keep their own default until set) | `0.5rem` / `0.375rem` |
494
+
495
+ Setting `--envoy-surface-alt` or `--envoy-radius` themes every rule that uses
496
+ it identically; the console itself never sets them, so it keeps its original,
497
+ slightly more varied look.
498
+
499
+ ### Assets and boot-time validation
500
+
501
+ Pin the same two Stimulus controllers as the console (section 6) — the
502
+ composer for Enter-to-submit, the autoscroll controller (now scrolls its own
503
+ `.envoy-chat__log` container rather than the window, so it sticks the panel
504
+ without hijacking the host page's scroll position):
505
+
506
+ ```ruby
507
+ # config/importmap.rb
508
+ pin "envoy/controllers/composer_controller", to: "envoy/controllers/composer_controller.js"
509
+ pin "envoy/controllers/autoscroll_controller", to: "envoy/controllers/autoscroll_controller.js"
510
+ ```
511
+
512
+ Finally, call `Envoy.validate!` at the end of the `to_prepare` block that
513
+ force-loads your definitions, so a typo'd toolset or prompt key fails on boot
514
+ — not silently, inside a job, where the user just watches "working…" forever:
515
+
516
+ ```ruby
517
+ Rails.application.config.to_prepare do
518
+ Dir[Rails.root.join("app/envoy/**/*.rb")].each { |f| require_dependency f }
519
+ Envoy.validate!
520
+ end
521
+ ```
522
+
344
523
  ## The debug console
345
524
 
346
525
  Envoy ships a minimal chat UI mounted at whatever path the host chooses
@@ -0,0 +1,209 @@
1
+ /* Embeddable chat rules. No body, no page-level layout, no fixed widths —
2
+ the host owns the chrome. Every colour is a var with a fallback, so the
3
+ stylesheet stands alone in the console and themes cleanly in a host.
4
+
5
+ Public vars (all optional — every rule falls back to the console's own
6
+ light-mode palette when unset): --envoy-surface, --envoy-surface-alt,
7
+ --envoy-ink, --envoy-subtle, --envoy-line, --envoy-accent,
8
+ --envoy-accent-ink, --envoy-warn, --envoy-radius. */
9
+
10
+ .envoy-chat {
11
+ display: flex;
12
+ flex-direction: column;
13
+ min-height: 0;
14
+ }
15
+
16
+ .envoy-chat__log {
17
+ flex: 1 1 auto;
18
+ min-height: 0;
19
+ overflow-y: auto;
20
+ overscroll-behavior: contain;
21
+ }
22
+
23
+ /* ---------------------------------------------------------------------- */
24
+ /* Messages */
25
+ /* ---------------------------------------------------------------------- */
26
+
27
+ .envoy-msg {
28
+ border: 1px solid var(--envoy-line, #e5e7eb);
29
+ border-radius: var(--envoy-radius, 0.5rem);
30
+ padding: 1rem;
31
+ background: var(--envoy-surface, #ffffff);
32
+ }
33
+ .envoy-msg--assistant { background: var(--envoy-surface-alt, #eff6ff); }
34
+
35
+ .envoy-msg__role {
36
+ font-size: 0.75rem;
37
+ text-transform: uppercase;
38
+ color: var(--envoy-subtle, #6b7280);
39
+ margin-bottom: 0.25rem;
40
+ }
41
+
42
+ /* Subtle "working…" shimmer while a reply is being generated. */
43
+ .envoy-working { animation: envoy-working 1.4s ease-in-out infinite; }
44
+ @keyframes envoy-working {
45
+ 0%, 100% { opacity: 0.4; }
46
+ 50% { opacity: 0.95; }
47
+ }
48
+ @media (prefers-reduced-motion: reduce) {
49
+ .envoy-working { animation: none; opacity: 0.7; }
50
+ }
51
+
52
+ /* ---------------------------------------------------------------------- */
53
+ /* Rendered markdown body */
54
+ /* ---------------------------------------------------------------------- */
55
+
56
+ .envoy-md { font-size: 0.9rem; line-height: 1.6; }
57
+ .envoy-md > * + * { margin-top: 0.7rem; }
58
+ .envoy-md h1 { font-size: 1.3em; font-weight: 700; }
59
+ .envoy-md h2 { font-size: 1.15em; font-weight: 700; }
60
+ .envoy-md h3, .envoy-md h4 { font-size: 1em; font-weight: 700; }
61
+ .envoy-md ul { padding-left: 1.25rem; list-style: disc; }
62
+ .envoy-md ol { padding-left: 1.25rem; list-style: decimal; }
63
+ .envoy-md li + li { margin-top: 0.2rem; }
64
+ .envoy-md a { text-decoration: underline; }
65
+ .envoy-md blockquote { border-left: 3px solid rgba(128, 128, 128, 0.4); padding-left: 0.75rem; }
66
+ .envoy-md hr { border: 0; border-top: 1px solid rgba(128, 128, 128, 0.3); }
67
+ /* Fenced code (comrak carries its own dark background + inline styles). */
68
+ .envoy-md pre { padding: 0.75rem 1rem; border-radius: 0.5rem; }
69
+ /* Inline code gets a subtle theme-agnostic chip. */
70
+ .envoy-md :not(pre) > code {
71
+ font-size: 0.9em;
72
+ overflow-wrap: anywhere;
73
+ background: rgba(128, 128, 128, 0.15);
74
+ padding: 0.1em 0.3em;
75
+ border-radius: 0.25rem;
76
+ }
77
+
78
+ /* Code blocks scroll within the message instead of spilling past it, with a
79
+ thin, theme-agnostic overlay scrollbar (mid-gray at 50% reads on both the
80
+ light tool-call panels and the dark highlighted blocks). Scoped to the
81
+ transcript log (.envoy-chat__log, see panels/_transcript.html.erb) rather
82
+ than an id, since message-list ids are conversation-scoped
83
+ (dom_id(conversation, :messages)) and no element has a literal
84
+ #envoy_messages id. */
85
+ .envoy-chat__log pre {
86
+ overflow-x: auto;
87
+ max-width: 100%;
88
+ scrollbar-width: thin;
89
+ scrollbar-color: rgba(128, 128, 128, 0.5) transparent;
90
+ }
91
+ .envoy-chat__log pre::-webkit-scrollbar { height: 8px; }
92
+ .envoy-chat__log pre::-webkit-scrollbar-track { background: transparent; }
93
+ .envoy-chat__log pre::-webkit-scrollbar-thumb {
94
+ background: rgba(128, 128, 128, 0.5);
95
+ border-radius: 9999px;
96
+ }
97
+ .envoy-chat__log pre::-webkit-scrollbar-thumb:hover { background: rgba(128, 128, 128, 0.8); }
98
+
99
+ /* ---------------------------------------------------------------------- */
100
+ /* Tool calls */
101
+ /* ---------------------------------------------------------------------- */
102
+
103
+ .envoy-tool {
104
+ margin-top: 0.5rem;
105
+ font-size: 0.875rem;
106
+ border: 1px solid var(--envoy-line, #e5e7eb);
107
+ border-radius: var(--envoy-radius, 0.375rem);
108
+ padding: 0.5rem;
109
+ background: var(--envoy-surface-alt, #f9fafb);
110
+ }
111
+
112
+ .envoy-tool__summary {
113
+ cursor: pointer;
114
+ display: flex;
115
+ align-items: center;
116
+ gap: 0.5rem;
117
+ }
118
+
119
+ .envoy-tool pre {
120
+ font-size: 0.75rem;
121
+ margin-top: 0.5rem;
122
+ overflow-x: auto;
123
+ max-width: 100%;
124
+ }
125
+
126
+ .envoy-mono {
127
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
128
+ }
129
+
130
+ /* Tool-call outcome badge (rendered by Envoy::Badges.outcome). Colors match
131
+ the original Tailwind red-100/amber-100/green-100 pairs; intentionally
132
+ scheme-independent (not vars), as the original badge had no dark variant. */
133
+ .envoy-badge {
134
+ display: inline-block;
135
+ font-size: 0.75rem;
136
+ padding: 0.125rem 0.5rem;
137
+ border-radius: var(--envoy-radius, 0.375rem);
138
+ font-weight: 500;
139
+ }
140
+ .envoy-badge--ok { background: #dcfce7; color: #166534; }
141
+ .envoy-badge--failed { background: #fef3c7; color: #92400e; }
142
+ .envoy-badge--denied { background: #fee2e2; color: #991b1b; }
143
+
144
+ /* ---------------------------------------------------------------------- */
145
+ /* Composer */
146
+ /* ---------------------------------------------------------------------- */
147
+
148
+ .envoy-composer {
149
+ display: flex;
150
+ gap: 0.5rem;
151
+ align-items: flex-end;
152
+ }
153
+
154
+ .envoy-input {
155
+ flex: 1;
156
+ border: 1px solid var(--envoy-line, #e5e7eb);
157
+ border-radius: var(--envoy-radius, 0.375rem);
158
+ padding: 0.5rem;
159
+ background: var(--envoy-surface, #ffffff);
160
+ color: var(--envoy-ink, #111827);
161
+ }
162
+ .envoy-input::placeholder { color: var(--envoy-subtle, #6b7280); opacity: 1; }
163
+
164
+ .envoy-hint {
165
+ font-size: 0.75rem;
166
+ color: var(--envoy-subtle, #6b7280);
167
+ margin-top: 0.25rem;
168
+ }
169
+
170
+ /* ---------------------------------------------------------------------- */
171
+ /* Buttons */
172
+ /* ---------------------------------------------------------------------- */
173
+
174
+ .envoy-btn {
175
+ padding: 0.375rem 0.75rem;
176
+ border-radius: var(--envoy-radius, 0.375rem);
177
+ background: var(--envoy-accent, #111827);
178
+ color: var(--envoy-accent-ink, #ffffff);
179
+ border: 0;
180
+ cursor: pointer;
181
+ text-decoration: none;
182
+ display: inline-block;
183
+ flex-shrink: 0;
184
+ font: inherit;
185
+ }
186
+ .envoy-btn--secondary {
187
+ background: transparent;
188
+ color: inherit;
189
+ border: 1px solid var(--envoy-line, #e5e7eb);
190
+ }
191
+
192
+ /* ---------------------------------------------------------------------- */
193
+ /* Layout / state helpers */
194
+ /* ---------------------------------------------------------------------- */
195
+
196
+ .envoy-stack > * + * { margin-top: 1rem; }
197
+ .envoy-stack--sm > * + * { margin-top: 0.75rem; }
198
+
199
+ .envoy-muted {
200
+ color: var(--envoy-subtle, #6b7280);
201
+ font-size: 0.875rem;
202
+ }
203
+
204
+ /* A .envoy-msg modifier (see app/views/envoy/messages/_error.html.erb): turns
205
+ the assistant bubble's border and text red when a turn failed. */
206
+ .envoy-error {
207
+ border-color: var(--envoy-warn, #b91c1c);
208
+ color: var(--envoy-warn, #b91c1c);
209
+ }
@@ -1,3 +1,17 @@
1
+ /* Console chrome only. The reusable chat rules live in chat.css so a host can
2
+ * load them without inheriting body-level styling.
3
+ *
4
+ * url(), not a bare string: Propshaft only fingerprints CSS url(...)
5
+ * references (lib/propshaft/compiler/css_asset_urls.rb), not @import
6
+ * strings. A plain `@import "chat.css"` would ship the literal, undigested
7
+ * filename straight into the served CSS, and Propshaft's asset server 404s
8
+ * on undigested paths -- so the browser would silently never load chat.css.
9
+ * The path is relative to this file's own directory (both live in
10
+ * app/assets/stylesheets/envoy/), which is what the compiler's resolver
11
+ * expects.
12
+ */
13
+ @import url("chat.css");
14
+
1
15
  /*
2
16
  * Envoy debug console — self-contained stylesheet.
3
17
  *
@@ -23,6 +37,23 @@
23
37
  --envoy-btn-ink: #ffffff;
24
38
  --envoy-danger: #b91c1c;
25
39
  --envoy-header-bg: rgba(255, 255, 255, 0.8);
40
+
41
+ /* chat.css's public vars (see its header comment). These mirror the
42
+ * console-only names above one-for-one -- chat.css was split out of this
43
+ * file and renamed its custom properties in the process, so without these
44
+ * the chat rules fall back to chat.css's own light-mode literals even
45
+ * under `prefers-color-scheme: dark` below. --envoy-radius is deliberately
46
+ * NOT defined: pre-split, radius was hardcoded per rule (0.5rem for
47
+ * .envoy-msg, 0.375rem everywhere else) rather than themed, so a single
48
+ * value here would change one or the other instead of leaving both as
49
+ * they were. Radius doesn't vary with color scheme, so it needs no dark
50
+ * override either.
51
+ */
52
+ --envoy-line: #e5e7eb; /* = --envoy-border */
53
+ --envoy-subtle: #6b7280; /* = --envoy-muted */
54
+ --envoy-accent: #111827; /* = --envoy-btn-bg */
55
+ --envoy-accent-ink: #ffffff; /* = --envoy-btn-ink */
56
+ --envoy-warn: #b91c1c; /* = --envoy-danger (same in both schemes) */
26
57
  }
27
58
 
28
59
  @media (prefers-color-scheme: dark) {
@@ -38,7 +69,25 @@
38
69
  --envoy-btn-bg: #f3f4f6;
39
70
  --envoy-btn-ink: #111827;
40
71
  --envoy-header-bg: rgba(17, 24, 39, 0.8);
72
+
73
+ --envoy-line: #374151; /* = --envoy-border (dark) */
74
+ --envoy-subtle: #9ca3af; /* = --envoy-muted (dark) */
75
+ --envoy-accent: #f3f4f6; /* = --envoy-btn-bg (dark) */
76
+ --envoy-accent-ink: #111827; /* = --envoy-btn-ink (dark) */
77
+ /* --envoy-warn is not redefined here: --envoy-danger, its predecessor,
78
+ * was never overridden for dark mode either, so it keeps its light
79
+ * value in both schemes. */
41
80
  }
81
+
82
+ /* --envoy-surface-alt has two distinct pre-split predecessors --
83
+ * --envoy-surface-assistant for chat bubbles, --envoy-surface-muted for
84
+ * tool panels -- that chat.css's rename collapsed into one property (see
85
+ * its differing per-rule fallbacks, #eff6ff vs #f9fafb). One :root value
86
+ * can't restore both original colors at once, so scope the override to
87
+ * each element that chat.css actually uses it on instead.
88
+ */
89
+ .envoy-msg--assistant { --envoy-surface-alt: #1f2937; } /* = --envoy-surface-assistant (dark) */
90
+ .envoy-tool { --envoy-surface-alt: #111827; } /* = --envoy-surface-muted (dark) */
42
91
  }
43
92
 
44
93
  /* ---------------------------------------------------------------------- */
@@ -141,174 +190,10 @@ body.envoy-body {
141
190
  .envoy-mb-4 { margin-bottom: 1rem; }
142
191
  .envoy-mt-4 { margin-top: 1rem; }
143
192
 
144
- .envoy-stack > * + * { margin-top: 1rem; }
145
- .envoy-stack--sm > * + * { margin-top: 0.75rem; }
146
-
147
- /* ---------------------------------------------------------------------- */
148
- /* Messages */
149
- /* ---------------------------------------------------------------------- */
150
-
151
- .envoy-msg {
152
- border: 1px solid var(--envoy-border);
153
- border-radius: 0.5rem;
154
- padding: 1rem;
155
- background: var(--envoy-surface);
156
- }
157
- .envoy-msg--assistant { background: var(--envoy-surface-assistant); }
158
-
159
- .envoy-msg__role {
160
- font-size: 0.75rem;
161
- text-transform: uppercase;
162
- color: var(--envoy-muted);
163
- margin-bottom: 0.25rem;
164
- }
165
-
166
- /* Subtle "working…" shimmer while a reply is being generated. */
167
- .envoy-working { animation: envoy-working 1.4s ease-in-out infinite; }
168
- @keyframes envoy-working {
169
- 0%, 100% { opacity: 0.4; }
170
- 50% { opacity: 0.95; }
171
- }
172
- @media (prefers-reduced-motion: reduce) {
173
- .envoy-working { animation: none; opacity: 0.7; }
174
- }
175
-
176
- /* ---------------------------------------------------------------------- */
177
- /* Rendered markdown body */
178
- /* ---------------------------------------------------------------------- */
179
-
180
- .envoy-md { font-size: 0.9rem; line-height: 1.6; }
181
- .envoy-md > * + * { margin-top: 0.7rem; }
182
- .envoy-md h1 { font-size: 1.3em; font-weight: 700; }
183
- .envoy-md h2 { font-size: 1.15em; font-weight: 700; }
184
- .envoy-md h3, .envoy-md h4 { font-size: 1em; font-weight: 700; }
185
- .envoy-md ul { padding-left: 1.25rem; list-style: disc; }
186
- .envoy-md ol { padding-left: 1.25rem; list-style: decimal; }
187
- .envoy-md li + li { margin-top: 0.2rem; }
188
- .envoy-md a { text-decoration: underline; }
189
- .envoy-md blockquote { border-left: 3px solid rgba(128, 128, 128, 0.4); padding-left: 0.75rem; }
190
- .envoy-md hr { border: 0; border-top: 1px solid rgba(128, 128, 128, 0.3); }
191
- /* Fenced code (comrak carries its own dark background + inline styles). */
192
- .envoy-md pre { padding: 0.75rem 1rem; border-radius: 0.5rem; }
193
- /* Inline code gets a subtle theme-agnostic chip. */
194
- .envoy-md :not(pre) > code {
195
- font-size: 0.9em;
196
- overflow-wrap: anywhere;
197
- background: rgba(128, 128, 128, 0.15);
198
- padding: 0.1em 0.3em;
199
- border-radius: 0.25rem;
200
- }
201
-
202
- /* Code blocks scroll within the message instead of spilling past it, with a
203
- thin, theme-agnostic overlay scrollbar (mid-gray at 50% reads on both the
204
- light tool-call panels and the dark highlighted blocks). */
205
- #envoy_messages pre {
206
- overflow-x: auto;
207
- max-width: 100%;
208
- scrollbar-width: thin;
209
- scrollbar-color: rgba(128, 128, 128, 0.5) transparent;
210
- }
211
- #envoy_messages pre::-webkit-scrollbar { height: 8px; }
212
- #envoy_messages pre::-webkit-scrollbar-track { background: transparent; }
213
- #envoy_messages pre::-webkit-scrollbar-thumb {
214
- background: rgba(128, 128, 128, 0.5);
215
- border-radius: 9999px;
216
- }
217
- #envoy_messages pre::-webkit-scrollbar-thumb:hover { background: rgba(128, 128, 128, 0.8); }
218
-
219
- /* ---------------------------------------------------------------------- */
220
- /* Tool calls */
221
- /* ---------------------------------------------------------------------- */
222
-
223
- .envoy-tool {
224
- margin-top: 0.5rem;
225
- font-size: 0.875rem;
226
- border: 1px solid var(--envoy-border);
227
- border-radius: 0.375rem;
228
- padding: 0.5rem;
229
- background: var(--envoy-surface-muted);
230
- }
231
-
232
- .envoy-tool__summary {
233
- cursor: pointer;
234
- display: flex;
235
- align-items: center;
236
- gap: 0.5rem;
237
- }
238
-
239
- .envoy-tool pre {
240
- font-size: 0.75rem;
241
- margin-top: 0.5rem;
242
- overflow-x: auto;
243
- max-width: 100%;
244
- }
245
-
246
- .envoy-mono {
247
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
248
- }
249
-
250
- /* Tool-call outcome badge (rendered by Envoy::Badges.outcome). Colors match
251
- the original Tailwind red-100/amber-100/green-100 pairs; intentionally
252
- scheme-independent, as the original badge had no dark variant. */
253
- .envoy-badge {
254
- display: inline-block;
255
- font-size: 0.75rem;
256
- padding: 0.125rem 0.5rem;
257
- border-radius: 0.375rem;
258
- font-weight: 500;
259
- }
260
- .envoy-badge--ok { background: #dcfce7; color: #166534; }
261
- .envoy-badge--failed { background: #fef3c7; color: #92400e; }
262
- .envoy-badge--denied { background: #fee2e2; color: #991b1b; }
263
-
264
- /* ---------------------------------------------------------------------- */
265
- /* Composer */
266
- /* ---------------------------------------------------------------------- */
267
-
268
- .envoy-composer {
269
- display: flex;
270
- gap: 0.5rem;
271
- align-items: flex-end;
272
- }
273
-
274
- .envoy-input {
275
- flex: 1;
276
- border: 1px solid var(--envoy-border);
277
- border-radius: 0.375rem;
278
- padding: 0.5rem;
279
- background: var(--envoy-surface);
280
- color: var(--envoy-ink);
281
- }
282
- .envoy-input::placeholder { color: var(--envoy-muted); opacity: 1; }
283
-
284
- .envoy-hint {
285
- font-size: 0.75rem;
286
- color: var(--envoy-muted);
287
- margin-top: 0.25rem;
288
- }
289
-
290
193
  /* ---------------------------------------------------------------------- */
291
- /* Buttons / links */
194
+ /* Links */
292
195
  /* ---------------------------------------------------------------------- */
293
196
 
294
- .envoy-btn {
295
- padding: 0.375rem 0.75rem;
296
- border-radius: 0.375rem;
297
- background: var(--envoy-btn-bg);
298
- color: var(--envoy-btn-ink);
299
- border: 0;
300
- cursor: pointer;
301
- text-decoration: none;
302
- display: inline-block;
303
- flex-shrink: 0;
304
- font: inherit;
305
- }
306
- .envoy-btn--secondary {
307
- background: transparent;
308
- color: inherit;
309
- border: 1px solid var(--envoy-border);
310
- }
311
-
312
197
  .envoy-link {
313
198
  color: var(--envoy-link);
314
199
  text-decoration: none;
@@ -350,12 +235,6 @@ body.envoy-body {
350
235
  font-size: 0.875rem;
351
236
  }
352
237
 
353
- .envoy-error {
354
- color: var(--envoy-danger);
355
- font-size: 0.875rem;
356
- margin-bottom: 0.75rem;
357
- }
358
-
359
238
  .envoy-prose-box {
360
239
  border: 1px solid var(--envoy-border);
361
240
  border-radius: 0.375rem;
@@ -364,8 +243,3 @@ body.envoy-body {
364
243
  white-space: pre-wrap;
365
244
  font-size: 0.875rem;
366
245
  }
367
-
368
- .envoy-muted {
369
- color: var(--envoy-muted);
370
- font-size: 0.875rem;
371
- }