Tickets for FE and actor split

This commit is contained in:
Hugh Kaznowski
2026-09-07 14:54:21 +01:00
parent 920629dbad
commit 738e90c45c
11 changed files with 346 additions and 48 deletions
@@ -0,0 +1,72 @@
# 109 — Stop binding `ottyl mcp` to one session for its whole lifetime
## Summary
`ottyl mcp --session <name>` resolves the session name to a port
once, at process startup, and then serves stdio JSON-RPC for the
life of the process against that fixed URL. In practice this means
every ottyl session an agent wants to talk to needs its own
registered MCP server entry (e.g. `ottyl-hygienic-gyrfalcon`), and
switching which running `ottyl` instance a client is looking at
requires killing that entry and re-registering a new one — which,
for MCP clients that only read server config at startup, means the
whole host process needs restarting before the new tools are even
callable. This got hit directly: an agent needed to switch from an
old dead session (`enormous-warthog`) to a new one
(`hygienic-gyrfalcon`) mid-conversation and had no way to do it
without editing MCP config and waiting for a restart.
## Where
- `src/cli/mod.rs:43-49` — `Command::Mcp { session: Option<String>,
port: Option<u16> }`, the CLI surface.
- `src/cli/mod.rs:171-195` (`run_mcp`) — resolves `session` to a port
via `sessions::resolve_session` exactly once, builds one
`OttylMcp::new(base_url)`, then blocks forever on
`service.waiting()`. There is no re-resolution after startup.
- `src/api/mcp_server.rs:254-282` — `OttylMcp` stores `base_url:
String` as a plain field set once in `new()`; every `#[tool]`
method builds request URLs via `self.url(path)`
(`mcp_server.rs:282-284`), so the fixed base URL is threaded
through every single tool call.
- `src/cli/sessions.rs` — `SessionInfo { name, port, pid }` and
`resolve_session`/`list_sessions`, the registry this would need to
re-query if the bound session can change after startup.
## Direction
Two ways to remove the "pick a session forever at startup" behavior;
pick one during implementation rather than building both:
1. **Request/response CLI, no long-running process.** Add a CLI
subcommand (e.g. `ottyl call <session> <tool> <json-args>`) that
resolves the session, makes the one HTTP call, prints the result,
and exits — no `rmcp`/stdio JSON-RPC server at all. Simplest fix
for the "MCP config never changes, session does" mismatch, but
loses whatever the MCP client gives you over a bare CLI call
(tool discovery/schema, a persistent connection) — check whether
that matters to how this is actually invoked before going this
route.
2. **Long-running MCP server, session switchable at runtime.** Keep
`ottyl mcp` as a stdio server, but stop fixing `base_url` at
construction: either (a) accept a `session` (or `port`) argument
on every tool call, resolved against `sessions::resolve_session`
per-call instead of once in `run_mcp`, with the constructor's
`--session`/`--port` becoming just a default; or (b) add an
explicit `switch_session` tool that mutates the stored target
(`base_url` would need to move from a plain field to something
mutable, e.g. `Mutex<String>` or resolved fresh from the registry
on every call from a stored session name instead of a
pre-resolved URL).
## Scope
- Land one of the two directions above for `ottyl mcp` /
`src/api/mcp_server.rs`; out of scope to build both.
- If going with direction 2, update `SKILL.md`'s "Connect an MCP
client" section, since it currently documents the
resolve-once-at-startup model.
- Out of scope: changing anything about the underlying `/api/...`
HTTP routes themselves, or the session registry format in
`src/cli/sessions.rs` — this is purely about how the MCP-facing
process picks and holds (or doesn't hold) its target session.
@@ -0,0 +1,72 @@
# 110 — Clicking a legend entry should focus that series, fading the rest
## Summary
`PanelChart`'s line and pie charts render a recharts `<Legend>`
(`webapp/src/components/PanelChart.tsx:134`, `:190`) but it's purely
decorative — clicking an entry does nothing. Bar charts have no
`<Legend>` at all (`:151-168`), since each bar is its own labeled
category on the x-axis rather than a separate series.
Add click-to-focus: clicking a legend entry marks that series
"focused"; any focused series stay at full opacity/stroke while every
other series fades. Clicking the same entry again (or clicking empty
space in the legend, per whatever recharts exposes) clears focus and
returns everyone to equal opacity — i.e. "no focused series" means
"all at the same opacity", not "all faded".
## Where
- `webapp/src/components/PanelChart.tsx:116-148` — line chart. Series
are recharts `<Line>` elements keyed by `keys` (from `toLineData`,
`:60-78`), colored via `COLORS[i % COLORS.length]` (`:140`). Fading
a line means varying `strokeOpacity` (or swapping `stroke` for a
`color-mix`'d faded variant) per key based on focus state.
- `webapp/src/components/PanelChart.tsx:170-193` — pie chart. Slices
are `<Cell>` elements (`:179-181`) keyed by row index into `rows`
(from `squashRows`, `:50-57`); fading a slice means varying its
`fillOpacity`.
- `webapp/src/components/PanelChart.tsx:134`, `:190` — the two
`<Legend>` usages. recharts' `<Legend>` takes an `onClick` handler
on its payload entries (`(entry) => void`, where `entry.dataKey` /
`entry.value` identifies which series was clicked) — this is the
hook point rather than building a custom legend from scratch.
- Bar charts (`:151-168`) have no legend to click, since bars aren't
named series here — out of scope for this ticket unless the
direction chosen below turns out to need bar-chart parity (see
Scope).
## Direction
- Add focus state as a `Set<string>` (or single nullable string, if
only one series can be focused at a time — decide during
implementation) of series keys, local to `PanelChart` via
`useState`. Reset it when `series` changes identity to avoid stale
focus surviving a query change onto a chart with different series.
- Wire the `<Legend>`'s `onClick` to toggle membership in that set.
- Compute per-series opacity from the set: full opacity if the set is
empty or the series is a member, faded (e.g. `0.25`) otherwise.
Thread that into `<Line stroke=...>` / `<Cell fill=...>` via
`strokeOpacity`/`fillOpacity` rather than swapping the base color,
so the existing `COLORS` palette and its `color-mix` theming stay
untouched.
- The legend entries themselves should visually reflect focus too
(e.g. dim the faded entries' swatch/text) so it's clear what's
focused without hunting across the plot — recharts' legend
`formatter` prop can restyle each entry's label.
## Scope
- Line and pie charts only, matching where `<Legend>` already
renders. Bar charts stay as-is — no legend, no per-bar fade — unless
investigation shows recharts' bar `<Legend>` support is trivial to
add alongside this; if so, note it as a follow-up rather than
folding scope creep into this ticket.
- Out of scope: persisting focus state across re-renders driven by
polling (`Dashboards.tsx`'s `PanelPreview`, `:46-92`, re-fetches on
every poll tick) — focus should survive a data refresh as long as
the series identity (legend key) is unchanged, but there's no need
to persist it to `localStorage` or across a page reload.
- Out of scope: multi-select "compare exactly these two series" UX
beyond simple toggle-membership — keep it to click-to-add/click-to-
remove from the focused set.
+51
View File
@@ -0,0 +1,51 @@
# 111 — Legend must scroll instead of overflowing the panel
## Summary
`PanelChart`'s recharts `<Legend>` (`webapp/src/components/
PanelChart.tsx:134`, `:190`) has no height constraint or scroll
container. A panel with many series (e.g. a high-cardinality PromQL
query, or the post-`squashRows` `MAX_KEPT_SERIES` = 7 kept rows plus
"Other") wraps the legend across as many lines as it needs, growing
the panel's rendered height and pushing surrounding dashboard layout
around rather than staying within the chart's fixed footprint.
## Where
- `webapp/src/components/PanelChart.tsx:117`, `:154`, `:176` — each
chart type wraps its `<ResponsiveContainer>` at a fixed `height={220}`,
but the `<Legend>` inside it (`:134`, `:190`) isn't given its own
bounded, scrollable region — recharts lays it out in the space left
after the plot area, which can push the container's effective
rendered height past 220 when the legend wraps to multiple rows.
- `webapp/src/components/Dashboards.tsx:46-92` (`PanelPreview`) is the
only caller of `PanelChart` today — worth checking that whatever
fixed-height/scroll wrapper gets added here doesn't fight with
`PanelPreview`'s own loading/error states (`:85-90`), which render
outside `PanelChart` at a different fixed height (`h-48`).
## Direction
- recharts' `<Legend>` supports a `wrapperStyle` prop (already used
for `fontSize`, `:134`/`:190`) — extending it with a fixed
`maxHeight` and `overflowY: 'auto'` is the natural fix and avoids
building a custom legend renderer just for this.
- Verify this doesn't conflict with the click-to-focus work in
[[110-legend-click-to-focus-series]] — that ticket also touches the
`<Legend>` props (`onClick`, `formatter`) on these same two call
sites, so land whichever lands second by rebasing onto the other's
`<Legend>` props rather than both independently restructuring it.
- Decide a concrete max height (e.g. two or three legend rows' worth)
that keeps the panel's total footprint predictable in a dashboard
grid; check `Dashboards.tsx`'s panel grid/card layout for whatever
existing panel height convention should inform the number.
## Scope
- Line and pie charts only, matching where `<Legend>` already
renders — same boundary as [[110-legend-click-to-focus-series]].
- Out of scope: virtualizing the legend list for very large series
counts — `squashRows` (`PanelChart.tsx:50-57`) already caps bar/pie
series at `MAX_KEPT_SERIES` (7) + "Other", and line charts have no
equivalent cap today, but adding one is a separate concern from
making the legend scrollable.
+80
View File
@@ -0,0 +1,80 @@
# 112 — Let a single dashboard panel pop out into its own window
## Summary
There's already a pop-out mechanism for whole primary sections (log,
params, metrics, dashboards) — `PopOutButton` opens a new tab at
`?section=<id>` and `App.tsx` reads that query param at startup to
render a standalone, full-page version of just that section (see
`webapp/src/components/CollapsibleSection.tsx:193-232` and
`webapp/src/App.tsx:129-140`, `:372-376`). That grain is a whole
section, not a single panel — the Dashboards section can only be
popped out in its entirety (all panels of the active dashboard, or
all dashboards stacked via `AllDashboardsView`,
`Dashboards.tsx:107-135`). This ticket adds the finer-grained version:
popping out one panel from one dashboard into its own window,
independent of the rest of that dashboard.
## Where
- `webapp/src/components/Dashboards.tsx:46-92` (`PanelPreview`) is
the component that would need to render standalone, full-page, the
same way `StandaloneLog`/`StandaloneParams`/`StandaloneRanged` do
today for whole sections (referenced in `App.tsx:373-376`, exact
component locations elsewhere in `Dashboards.tsx`/`Metrics.tsx` per
the skeleton read during this ticket's writeup — re-read
`Dashboards.tsx` in full before implementing, since it was read via
repowise's skeleton view here and several ranges (`:1-45`, `:136-
434`) were elided).
- `webapp/src/components/CollapsibleSection.tsx:193-206`
(`PopOutSectionId`, `popOutUrl`) — the existing section-grain
pop-out routing. A panel-grain pop-out needs to identify a specific
`(dashboardId, panelId)` pair in the URL, not just a section name —
either extend this query-param scheme with `dashboardId`/`panelId`
params alongside `section=dashboards`, or give panels their own
distinct query shape if reusing `PopOutSectionId` doesn't fit
cleanly.
- `webapp/src/App.tsx:129-140` (`parsePopOutQuery`), `:372-376` (the
dispatch on parsed `section`) — where the new panel-grain case needs
a branch, reading whatever new params are added above.
- Each `Panel` (`webapp/src/lib/api.ts`'s `Panel` type, imported at
`Dashboards.tsx:19`) needs a stable identifier already, or one to
add, so a popped-out URL can point back at "this exact panel of
this exact dashboard" — check whether `Panel` already carries an
`id` field before inventing a new addressing scheme.
## Direction
- Mirror the existing section pop-out exactly: a small pop-out button
per panel (using or extending `PopOutButton`,
`CollapsibleSection.tsx:209-232`) that opens `?section=dashboards&
dashboardId=<id>&panelId=<id>`, plus range params the same way
`popOutUrl` already carries over a rolling preset or fixed range
(`CollapsibleSection.tsx:196-206`).
- The standalone page for this case renders just that one panel's
`PanelPreview` (or a thin wrapper around it) full-page — no
dashboard chrome, no other panels — analogous to how
`StandaloneRanged` (`App.tsx:179-...`) drives `LogTimeline`'s
picker UI on its own poll cadence for the metrics/dashboards
standalone views.
- The popped-out panel window should keep polling independently
(same `POLL_MS` pattern `PanelPreview` already uses,
`Dashboards.tsx:52-55`) rather than freezing at open time.
## Scope
- One panel popping into a new browser tab/window, matching the
existing section pop-out's mechanism (new tab via `window.open`,
not an in-app floating/detached window widget) — stay consistent
with the pattern already established for log/params/metrics/
dashboards rather than introducing a second, different pop-out
mechanism.
- Out of scope: editing a panel's query/chart type from the popped-out
view — read-only preview only, matching what pop-out gives you for
the other sections today.
- Out of scope: [[110-legend-click-to-focus-series]] and
[[111-scrollable-panel-legend]] — this ticket is purely about the
window/routing mechanism; the popped-out panel should get those
legend behaviors for free once they land in `PanelChart`, since
it's the same component, but implementing them isn't this ticket's
job.