Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
source "https://rubygems.org"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 8.1.0"
gem "rails", "~> 8.1.3", ">= 8.1.3.1"
# The modern asset pipeline for Rails [https://github.com/rails/propshaft]
gem "propshaft"
# Use postgresql as the database for Active Record
Expand Down
114 changes: 57 additions & 57 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,68 +1,68 @@
GEM
remote: https://rubygems.org/
specs:
action_text-trix (2.1.18)
action_text-trix (2.1.19)
railties
actioncable (8.1.3)
actionpack (= 8.1.3)
activesupport (= 8.1.3)
actioncable (8.1.3.1)
actionpack (= 8.1.3.1)
activesupport (= 8.1.3.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.1.3)
actionpack (= 8.1.3)
activejob (= 8.1.3)
activerecord (= 8.1.3)
activestorage (= 8.1.3)
activesupport (= 8.1.3)
actionmailbox (8.1.3.1)
actionpack (= 8.1.3.1)
activejob (= 8.1.3.1)
activerecord (= 8.1.3.1)
activestorage (= 8.1.3.1)
activesupport (= 8.1.3.1)
mail (>= 2.8.0)
actionmailer (8.1.3)
actionpack (= 8.1.3)
actionview (= 8.1.3)
activejob (= 8.1.3)
activesupport (= 8.1.3)
actionmailer (8.1.3.1)
actionpack (= 8.1.3.1)
actionview (= 8.1.3.1)
activejob (= 8.1.3.1)
activesupport (= 8.1.3.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.1.3)
actionview (= 8.1.3)
activesupport (= 8.1.3)
actionpack (8.1.3.1)
actionview (= 8.1.3.1)
activesupport (= 8.1.3.1)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (8.1.3)
actiontext (8.1.3.1)
action_text-trix (~> 2.1.15)
actionpack (= 8.1.3)
activerecord (= 8.1.3)
activestorage (= 8.1.3)
activesupport (= 8.1.3)
actionpack (= 8.1.3.1)
activerecord (= 8.1.3.1)
activestorage (= 8.1.3.1)
activesupport (= 8.1.3.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.1.3)
activesupport (= 8.1.3)
actionview (8.1.3.1)
activesupport (= 8.1.3.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (8.1.3)
activesupport (= 8.1.3)
activejob (8.1.3.1)
activesupport (= 8.1.3.1)
globalid (>= 0.3.6)
activemodel (8.1.3)
activesupport (= 8.1.3)
activerecord (8.1.3)
activemodel (= 8.1.3)
activesupport (= 8.1.3)
activemodel (8.1.3.1)
activesupport (= 8.1.3.1)
activerecord (8.1.3.1)
activemodel (= 8.1.3.1)
activesupport (= 8.1.3.1)
timeout (>= 0.4.0)
activestorage (8.1.3)
actionpack (= 8.1.3)
activejob (= 8.1.3)
activerecord (= 8.1.3)
activesupport (= 8.1.3)
activestorage (8.1.3.1)
actionpack (= 8.1.3.1)
activejob (= 8.1.3.1)
activerecord (= 8.1.3.1)
activesupport (= 8.1.3.1)
marcel (~> 1.0)
activesupport (8.1.3)
activesupport (8.1.3.1)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
Expand Down Expand Up @@ -189,13 +189,13 @@ GEM
loofah (2.25.2)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.9.0)
mail (2.9.1)
logger
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
marcel (1.1.0)
marcel (1.2.1)
matrix (0.4.3)
mini_magick (5.3.1)
logger
Expand Down Expand Up @@ -300,20 +300,20 @@ GEM
rack (>= 1.3)
rackup (2.3.1)
rack (>= 3)
rails (8.1.3)
actioncable (= 8.1.3)
actionmailbox (= 8.1.3)
actionmailer (= 8.1.3)
actionpack (= 8.1.3)
actiontext (= 8.1.3)
actionview (= 8.1.3)
activejob (= 8.1.3)
activemodel (= 8.1.3)
activerecord (= 8.1.3)
activestorage (= 8.1.3)
activesupport (= 8.1.3)
rails (8.1.3.1)
actioncable (= 8.1.3.1)
actionmailbox (= 8.1.3.1)
actionmailer (= 8.1.3.1)
actionpack (= 8.1.3.1)
actiontext (= 8.1.3.1)
actionview (= 8.1.3.1)
activejob (= 8.1.3.1)
activemodel (= 8.1.3.1)
activerecord (= 8.1.3.1)
activestorage (= 8.1.3.1)
activesupport (= 8.1.3.1)
bundler (>= 1.15.0)
railties (= 8.1.3)
railties (= 8.1.3.1)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
Expand All @@ -326,9 +326,9 @@ GEM
nio4r
railties
websocket-driver
railties (8.1.3)
actionpack (= 8.1.3)
activesupport (= 8.1.3)
railties (8.1.3.1)
actionpack (= 8.1.3.1)
activesupport (= 8.1.3.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
Expand Down Expand Up @@ -498,7 +498,7 @@ DEPENDENCIES
propshaft
puma (>= 5.0)
rack-attack
rails (~> 8.1.0)
rails (~> 8.1.3, >= 8.1.3.1)
rails_live_reload
redcarpet
rubocop-rails-omakase
Expand Down
51 changes: 46 additions & 5 deletions app/controllers/control_room_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# frozen_string_literal: true

class ControlRoomController < ApplicationController
PROGRAM_KINDS = %w[bot-fix qa-oversight].freeze
QUEUED_STATES = %w[queued ready].freeze
WAITING_STATES = %w[failed review].freeze
TERMINAL_STATES = %w[done cancelled].freeze

before_action :set_task, only: %i[thread message approve retry cancel]
helper_method :intent_result_summary, :pane_display_status, :orchestration_items,
:task_needs_attention?
Expand Down Expand Up @@ -116,12 +121,48 @@ def intent_result_summary(intent)
end

def categorize_tasks
@needs_attention = @tasks.select { |task| task_needs_attention?(task) }
@failures = @tasks.select { |task| source_state(task) == "failed" }
@active = @tasks.select do |task|
%w[queued ready running review].include?(source_state(task)) && !task_needs_attention?(task)
grouped = @tasks.group_by { |task| task_lane(task) }
@waiting_on_you = grouped.fetch(:waiting, [])
@running_now = grouped.fetch(:running, [])
@queued_next = grouped.fetch(:queued, [])
@programs = grouped.fetch(:program, [])
@unclassified_tasks = grouped.fetch(:unclassified, [])
@recent = grouped.fetch(:recent, []).first(25)
@project_work_counts = project_work_counts
end

def task_lane(task)
return :recent if TERMINAL_STATES.include?(source_state(task))
return :program if program_task?(task)
return :waiting if task_needs_attention?(task) || WAITING_STATES.include?(source_state(task))
return :running if source_state(task) == "running"
return :queued if QUEUED_STATES.include?(source_state(task))

:unclassified
end

def program_task?(task)
state = task.state_data.fetch("orchestration", {})
state["work_type"] == "program" || PROGRAM_KINDS.include?(state["kind"])
end

def project_work_counts
lanes = {
waiting: @waiting_on_you,
running: @running_now,
queued: @queued_next,
program: @programs,
unclassified: @unclassified_tasks
}
projects = lanes.values.flatten.filter_map { |task| task_project(task) }.uniq.sort

projects.to_h do |project|
[project, lanes.transform_values { |tasks| tasks.count { |task| task_project(task) == project } }]
end
@recent = @tasks.select { |task| %w[done cancelled].include?(source_state(task)) }.first(25)
end

def task_project(task)
task.state_data.dig("orchestration", "project").to_s.presence
end

def selected_task
Expand Down
16 changes: 15 additions & 1 deletion app/javascript/controllers/control_room_live_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,21 @@ export default class extends Controller {
const name = region.dataset.controlRoomLiveRegion
const current = [...this.element.querySelectorAll("[data-control-room-live-region]")]
.find((candidate) => candidate.dataset.controlRoomLiveRegion === name)
if (current) current.innerHTML = region.innerHTML
if (current) {
this.syncRegionAttributes(current, region)
current.innerHTML = region.innerHTML
}
})
}

syncRegionAttributes(current, next) {
const names = [
"class", "role", "data-source-count", "data-rendered-count",
"data-open-source-count", "data-unclassified-count"
]
names.forEach((name) => {
if (next.hasAttribute(name)) current.setAttribute(name, next.getAttribute(name))
else current.removeAttribute(name)
})
}

Expand Down
87 changes: 60 additions & 27 deletions app/views/control_room/_fleet.html.erb
Original file line number Diff line number Diff line change
@@ -1,30 +1,63 @@
<section class="rounded-xl border border-border bg-bg-surface p-4" data-control-room-live-region="fleet">
<div class="flex items-center justify-between gap-3">
<div>
<h2 class="text-sm font-semibold text-content">Fleet</h2>
<p class="mt-1 text-xs text-content-muted">
<% if @source&.last_seen_at %>
<%= @source.panes.length %> visible panes · last sync <%= time_ago_in_words(@source.last_seen_at) %> ago
<% else %>
No bridge telemetry yet
<% end %>
</p>
<% panes = Array(@source&.panes) %>
<% status_counts = panes.map { |pane| pane_display_status(pane) }.tally %>
<details class="group rounded-xl border border-border bg-bg-surface"
data-control-room-live-region="fleet"
data-source-count="<%= panes.length %>"
data-rendered-count="<%= panes.length %>">
<summary class="flex cursor-pointer list-none flex-wrap items-center justify-between gap-3 px-4 py-3">
<div class="flex flex-wrap items-center gap-3">
<span class="text-sm font-semibold text-content">Fleet · <%= panes.length %> panes</span>
<% {
"working" => ["bg-green-400", "text-green-300"],
"waiting" => ["bg-yellow-400", "text-yellow-200"],
"permission" => ["bg-yellow-400", "text-yellow-200"],
"idle" => ["bg-sky-400", "text-sky-300"],
"present" => ["bg-violet-400", "text-violet-300"],
"unknown" => ["bg-slate-400", "text-slate-300"]
}.each do |status, classes| %>
<% count = status_counts.fetch(status, 0) %>
<% next unless count.positive? %>
<span class="inline-flex items-center gap-1.5 text-xs <%= classes.last %>">
<span class="size-2 rounded-full <%= classes.first %> <%= "animate-pulse" if status == "working" %>"></span>
<%= count %> <%= status %>
</span>
<% end %>
</div>
<% if @source&.health&.dig("fleet") %>
<% a2a_updates = @source.health.dig("fleet", "a2a_envelopes").to_i %>
<span class="text-xs text-content-muted">
<%= a2a_updates.positive? ? "#{a2a_updates} A2A updates in latest sync" : "No A2A updates in latest sync" %>
</span>
<% end %>
</div>
<% if @source&.panes&.any? %>
<div class="mt-3 flex flex-wrap gap-2">
<% @source.panes.each do |pane| %>
<div class="rounded-lg border border-border bg-bg-elevated px-3 py-2 text-xs text-content-secondary">
<span class="font-semibold">pane <%= pane["pane_id"] || pane["id"] %></span>
<span class="ml-1 text-content-muted"><%= pane["project"] %> · <%= pane_display_status(pane) %></span>
<span class="text-xs text-content-muted">
<% if @source&.last_seen_at %>
<% a2a_updates = @source.health.to_h.dig("fleet", "a2a_envelopes").to_i %>
synced <%= time_ago_in_words(@source.last_seen_at) %> ago ·
<%= a2a_updates.positive? ? "#{a2a_updates} A2A updates in latest sync" : "No A2A updates in latest sync" %> ·
expand pane details
<% else %>
no bridge telemetry · expand
<% end %>
</span>
</summary>

<% if panes.any? %>
<div class="grid max-h-64 gap-2 overflow-y-auto border-t border-border p-3 sm:grid-cols-2 xl:grid-cols-4">
<% panes.each do |pane| %>
<% status = pane_display_status(pane) %>
<% status_style = case status
when "working" then "border-green-400/35 bg-green-400/10 text-green-300"
when "waiting", "permission" then "border-yellow-400/40 bg-yellow-400/10 text-yellow-200"
when "idle" then "border-sky-400/30 bg-sky-400/10 text-sky-300"
when "present" then "border-violet-400/30 bg-violet-400/10 text-violet-300"
else "border-border bg-bg-elevated text-content-muted"
end %>
<article class="rounded-lg border px-3 py-2 text-xs <%= status_style %>"
data-pane-status="<%= status %>"
aria-label="pane <%= pane["pane_id"] || pane["id"] %>, <%= pane["project"] %>, <%= status %>">
<div class="flex items-center justify-between gap-2">
<span class="font-semibold text-content">pane <%= pane["pane_id"] || pane["id"] %> · <%= pane["project"] %></span>
<span class="inline-flex items-center gap-1">
<span class="size-2 rounded-full bg-current <%= "animate-pulse" if status == "working" %>"></span>
<%= status %>
</span>
</div>
<% if pane["title"].present? || pane["model"].present? || pane["ctx"].present? %>
<p class="mt-1 text-[11px] text-content-muted">
<p class="mt-1 truncate text-[11px] text-content-muted">
<%= [pane["title"], pane["persona"], pane["model"], pane["ctx"]].compact_blank.join(" · ") %>
</p>
<% end %>
Expand All @@ -33,8 +66,8 @@
Session <%= pane["session_pct"].presence || "—" %>% · week <%= pane["weekly_pct"].presence || "—" %>%
</p>
<% end %>
</div>
</article>
<% end %>
</div>
<% end %>
</section>
</details>
1 change: 0 additions & 1 deletion app/views/control_room/_live_payload.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
<%= render "source_badges" %>
<%= render "source_health" %>
<%= render "fleet" %>
<%= render "request_status" %>
Expand Down
Loading
Loading