Skip to content

feat(ui): RTDL tree view and Studio service tabs - #15

Merged
enkerewpo merged 10 commits into
syswonder:mainfrom
laojiahuo2003:feat/camera-display
Oct 25, 2026
Merged

enkerewpo merged 10 commits into
syswonder:mainfrom
laojiahuo2003:feat/camera-display

Conversation

@laojiahuo2003

@laojiahuo2003 laojiahuo2003 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The sidebar's service panel was a hand-built linear pipeline (perception →
mapping → planning → execution) that held its place even when the robot ran
none of those services, and the RTDL tree it framed was pinned to a small
fixed height, so the behavior tree rendered as a thumbnail. There was also no
way to reach the robot's own web tools (Foxglove, RTAB-Map, the Hub) from the
client.

Changes

RTDL execution tree

  • The tree container now fills the sidebar panel (minmax(0, 1fr) +
    min-height: 0) instead of being squeezed into a fixed height.
  • Drag to pan, wheel to zoom around the cursor, double click to reset. The
    view is stored per planId, so the periodic re-render keeps the user's zoom
    and position.
  • ResizeObservers are pruned once a plan scrolls off the 80-record cap, and
    the view-state map no longer grows one entry per plan ever drawn.
  • Pan/zoom state is namespaced, so a plan literally id'd "tree" can no longer
    collide with the planless tree at the same index.

Studio service console

  • New /api/studio/services: probes the candidate ports concurrently and
    infers each service's name and icon from the title it returns.
  • A discovered service is framed directly. Robonix Hub sets
    X-Frame-Options: DENY, so it falls back to a card that opens it in a new
    tab.
  • A forced rescan aborts the scan still in flight and carries an 8s timeout,
    so a hung robot host no longer leaves the badge spinning with no request
    left to fail.
  • The four shipped tabs are static markup; an empty discovery result used to
    clear them, so switching language left the Studio header with no tabs.

Layout

  • The pipeline tile is replaced by the persistent execution tree panel.
  • Fields in the collapsible Current Goal panel are no longer clipped; longer
    content scrolls horizontally with the scrollbar hidden, per the repo's
    existing convention.

Known limitations

  • bd4865b ("Make the RTDL tree viewport fill the sidebar panel") does not
    follow Conventional Commits. It is already pushed history, so correcting it
    would need a force-push; left as is.
  • Service discovery infers from port and page title only. It does not read a
    capability manifest from the robot.

Validation

  • 51 Python unit tests
  • 16 Playwright tests (desktop and mobile)
  • vitals.bundle.js rebuild is byte-identical

Replace the two-tab cockpit with a compact sidebar: the current goal keeps its panel, the RTDL forest and the event log switch through a small menu, and nodes open in a detail modal instead of an inline pane.

Long execution trees are pannable and zoomable, open centered in their panel even when they first render while hidden, and keep the user's view across executor polls. The latest completed plan stays on screen instead of flickering away, and ambient status lines fold into an expandable activity run so the robot reply stays the only bubble in the transcript.

Also route legacy status rows through the i18n reverse table, re-render an open node popup on a language switch, localize the Studio service tab titles, and keep the configured host value out of innerHTML.
- Move 're' import to module level in app.py
- Remove decorative separator lines for consistency
- Initialize studioDiscoveredServices as empty array
- Remove unused data-studio-service attributes from HTML
- Fix potential bug where default services could show when none exist
- Studio: 工坊 → 控制台
- Integrated Service Cockpit: 一体化微服务座舱 → 集成服务面板
- Scene Service: Scene 感知套件 → 场景服务
- Robonix Hub: 全景门户 → Robonix Hub
- Fleet Hub: 车队门户 → 机群中心
- Audio Server: 声卡诊断 → 音频服务
- Audio Reverse Bridge: 音频反向桥 → 音频桥接
- Mapping Service: SLAM 建图服务 → 建图服务

Remove overly elaborate terminology and use straightforward translations.
The panel already reserved a minmax(0, 1fr) row for the execution table, but
every box below it was content-sized, so the SVG viewport collapsed to the
tree's drawing height and the tree sat in a thin strip at the top of the
panel. Give the chain definite heights: the table becomes a flex column, the
plan tree and plan record take the remaining space, and the tree card row is
minmax(0, 1fr).

The SVG keeps its own width/height (fitView scales and centers it), so a
shallow tree is no longer shrunk to fit the old 160px-tall strip and a wide
one still pans and zooms as before.
.goal-preview was grouped with .map-stage and .camera-feed, so it inherited
the image-stage rules (position: relative, height: calc(100% - 35px),
overflow: hidden). That rule is for a cover-fit image; the goal preview is the
one place in the app that uses the class and it only ever holds the text goal
card, so the card was cut in half: two of the four call fields and the whole
args block were invisible, and the panel's own overflow: auto never got a
chance to scroll because the child clipped first.

Drop .goal-preview from that rule and delete the dead .goal-preview img rule,
so the box is content-sized again. Raise the panel cap to 340px so a typical
card (source pill, call name, four fields, args) fits without scrolling; a
longer contract id or args block now scrolls inside the panel instead of being
cut off, and the scrollbars stay hidden as everywhere else.
Studio:
- The Hub fallback card shares the studio-empty-state class, so a stale
  card counted as "an empty state already exists" and the real one was
  never built: blank viewport, no retry button. Exclude the card from
  the lookup and manage it in both branches.
- Fullscreen targeted the viewport, which hid the header holding the
  button just clicked plus the service tabs. Target the shell instead.
- A forced scan stacked one probe per click against every candidate
  port, and a hung robot host left the badge spinning with no request
  left to fail. Abort the superseded scan, add an 8s timeout, and let
  only the newest scan own the shared state.
- The four shipped tabs are static markup. An empty discovery result
  cleared them and returned early, so a language switch left the Studio
  header with no service tabs at all. Restore the saved markup and
  re-translate it.
- iframe.src reads back the browser-normalized URL, so a robotHost
  typed with capitals never compared equal and the frame reloaded on
  every return to that service, flashing the scrim. Track the URL we
  asked for in a dataset attribute, and keep the reload button in step
  with it.
- Discovered tabs carried no role/aria-selected/aria-controls, and the
  viewport had no tabpanel role or label.
- The audio tab's tooltip key was "Audio Device Service" while the
  catalog holds "Audio Device Server".

RTDL:
- A plan that scrolls off the 80-record cap left its ResizeObserver
  watching a detached viewport, and the view-state map grew one entry
  per plan ever drawn. Prune both on every render.
- A plan literally id'd "tree" collided with the planless tree's key at
  the same index, so the two trees shared one pan/zoom state.

i18n: expose applyDocument on window.RobonixI18N so a re-rendered strip
can be re-translated; drop ambiguous values from the reverse map instead
of letting the last key win; remove 13 unused keys.
styles.css: remove rules for markup that no longer exists -- the map
stage and camera feed, the RTDL snapshot tree and history launcher, and
the execution detail panel. 174 lines; brace balance re-checked.

app.py: hoist urllib imports to module level and drop the desc field the
frontend never read. Also reject extra_ports outside 1-65535: urllib
does not reject them, and 99999 wraps to 34463 and answers, so the scan
would report a service on an unrelated port behind a URL nobody can
open.
Copilot AI balanced review requested due to automatic review settings October 21, 2026 00:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@laojiahuo2003 laojiahuo2003 changed the title feat(ui): 常驻 RTDL 执行树与自适应服务控制台 feat(ui): persistent RTDL execution tree and adaptive Studio service console Oct 21, 2026

@enkerewpo enkerewpo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Both parts are useful: the RTDL tree (fill the panel, drag to pan, cursor-anchored zoom, per-plan view state) and Studio, which gives the client one place to open the robot's web UIs (Foxglove, RTAB-Map and so on). A few changes before merge (details inline):

  1. Naming. Port 4880 is Robonix Console, not "Robonix Hub" or "Robonix Fleet Center", and product names are not translated (机群中心). "Studio" translated as 控制台 also collides with Robonix Console; keep "Studio" or pick another Chinese name.
  2. /api/studio/services probes any host the browser sends with an unbounded port list. Restrict it to the configured robot host and cap extra_ports.
  3. Discovery. A fixed port list plus page-title guessing ("hub" matches "github", "arm" matches "alarm") mislabels and misses services. We will add a directory to Atlas where packages declare their web pages, WebSockets and streams themselves (syswonder/robonix#292); Studio should then read that directory instead of scanning. Until that lands, keep the scan but use whole-word matching and drop the hard-coded default tabs shown when nothing is found.
  4. Console listens on 127.0.0.1 by default, so a client on another machine will not reach 4880; handle that tab accordingly (it already opens in a new tab because of X-Frame-Options).
  5. The GZip middleware is unrelated to both features; please move it out. Title: keep it to 50 characters, e.g. feat(ui): RTDL tree view and Studio service tabs; squash bd4865b (not a Conventional Commit).

Comment thread src/robonix_client/app.py Outdated
return {"name": "Scene Service", "icon": "👁️"}
if port == 8091 or "mapping" in tl or "slam" in tl:
return {"name": "SLAM Mapping", "icon": "🗺️"}
if port == 4880 or "hub" in tl:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Title substrings mislabel services: "hub" matches "github", "arm" matches "alarm". This scan is a stopgap until the Atlas directory (syswonder/robonix#292); meanwhile match whole words.

Comment thread src/robonix_client/app.py

@app.get("/api/studio/services")
async def discover_studio_services(
robot_host: str = "127.0.0.1",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint probes any robot_host the browser sends and an unbounded extra_ports list, so the client can be used to scan arbitrary hosts. Restrict the host to the configured robot host and cap extra_ports.

Comment thread src/robonix_client/static/index.html Outdated
<span class="tab-title" data-i18n="SLAM Mapping">SLAM Mapping</span>
<span class="tab-port">8091</span>
</button>
<button type="button" class="studio-service-tab" role="tab" aria-selected="false" aria-controls="studioViewport" title="Robonix Fleet Center (Port 4880)" data-i18n-title="Robonix Fleet Center (Port 4880)">

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Robonix Fleet Center" — this is Robonix Console. There is no fleet center.

Comment thread src/robonix_client/static/i18n.js Outdated
"Compute node telemetry from Vitals": "来自 Vitals 的计算节点遥测",
"Audio": "音频",
"Settings": "设置",
"Studio": "控制台",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Studio" → "控制台" collides with Robonix Console. Keep "Studio" or choose a different Chinese name.

Comment thread src/robonix_client/static/i18n.js Outdated
"Audio Server": "音频服务",
"Scene Perception Service (Port 50107)": "场景感知服务(端口 50107)",
"RTAB-Map Mapping (Port 8091)": "RTAB-Map 建图(端口 8091)",
"Robonix Fleet Center (Port 4880)": "Robonix 机群中心(端口 4880)",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

机群中心 → Robonix Console (product names are not translated).

Comment thread src/robonix_client/app.py Outdated
CANDIDATE_STUDIO_PORTS = [
50107, # Scene Perception Service
8091, # RTAB-Map SLAM Mapping
4880, # Robonix Hub

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Port 4880 is Robonix Console (the robot-side console), not Robonix Hub.

Address PR review on the Studio service console:

- Port 4880 is Robonix Console, not "Robonix Hub" or "Fleet Center".
  Rename it across the probe table, title inference, the iframe fallback
  card, and the i18n strings. Product names are no longer translated, and
  "Studio" is "工作室" so it no longer collides with the Console's name.
- Restrict /api/studio/services: reject any robot_host that is not a
  single bare host (scheme, path, separators that could smuggle a second
  target) and the cloud metadata addresses, and cap extra_ports at 8 so
  the endpoint can no longer be used as an arbitrary port scanner.
- Match service titles on whole words, so "github" no longer reads as the
  Console and "alarm" no longer reads as an arm.
- Drop the four hard-coded default tabs; the strip is built from the scan
  and an empty result shows the empty state.
- Note on the Console card that a loopback-bound Console is only reachable
  from the robot itself.
- Remove the unrelated GZip middleware.

Add tests for the Console rename, whole-word matching, host rejection,
and the extra_ports cap.
@laojiahuo2003 laojiahuo2003 changed the title feat(ui): persistent RTDL execution tree and adaptive Studio service console feat(ui): RTDL tree view and Studio service tabs Oct 24, 2026
@laojiahuo2003

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Pushed 0515653 addressing the points:

  1. Naming — Port 4880 is now "Robonix Console" across the probe table, title inference, the iframe fallback card, and i18n. Product names are no longer translated; "Studio" is now "工作室" so it no longer collides with the Console.
  2. Host restriction — /api/studio/services now rejects any robot_host that isn't a single bare host (scheme, path, or separators that could smuggle a second target) and the cloud-metadata addresses. extra_ports is capped at 8, so the endpoint can no longer be used as an arbitrary port scanner.
  3. Discovery — title matching is now whole-word (so "github" no longer matches the Console, "alarm" no longer matches an arm), and the four hard-coded default tabs are gone; the strip is built from the scan and an empty result shows the empty state. We'll switch to the Atlas directory once robonix#292 lands.
  4. Console loopback — added a note on the Console card that a loopback-bound Console is only reachable from the robot itself.
  5. GZip — removed. Title shortened to feat(ui): RTDL tree view and Studio service tabs.

On bd4865b: its content is correct, only the subject line isn't Conventional Commits. Rather than force-push a rewrite of already-pushed history, I'd suggest squash-merging the PR, which drops the individual subjects. Happy to rebase it out instead if you'd prefer.

@enkerewpo enkerewpo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, LGTM.

@enkerewpo
enkerewpo merged commit b64b0cb into syswonder:main Oct 25, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants