* feat: add metadata-only message audit events
* chore(protocol): restore generated swift models and config baseline after rebase
* fix(state): gate agent-db ownership check before schema version
* fix(cli): sync audit command description into the root-help catalog
* fix(audit): require destination proof before classifying outbound messages as direct
* refactor(audit): drop dead migration guard and add contract comments
* docs(gateway): add dedicated audit history page and cross-links
* test(e2e): enable direct-mode message audit in the telegram proof SUT config
* test(channels): expect declared conversationKind in durable delivery session context
* fix(audit): record the routing channel id for inbound rows from plugin channels
* fix(audit): match channel-prefixed delivery targets in the destination route gate
* fix(audit): validate explicit target kind against the destination route kind
* fix(audit): use the canonical target-prefix grammar in the destination route gate and fail closed on foreign migration tables
* fix(audit): normalize nested provider and kind target prefixes in the destination gate
* fix(audit): strip registered provider aliases and the direct kind prefix in the destination gate
* chore(docs): refresh config baseline after rebase
* feat(sessions): durable session state events with parent invalidation
Parents no longer act on stale child-session state: a durable, typed
signal log (session_state_events + per-agent heads) records direct human
messages to children, child spawn/terminal outcomes, goal changes, and
compaction at the seams where those facts are created. A frozen-watermark
cursor protocol (session_watch_cursors) delivers one coalesced system
notice to the parent through the existing system-event + heartbeat wake
idiom, acknowledged at the shared generic drain, with a deferred startup
sweep for restart recovery. session_status gains stateVersion and
changesSince (with exact pruned-history gap signaling); sessions_list
rows gain stateVersion.
Closes#104565
* chore: regenerate docs map and prompt snapshots for session_status changesSince
* fix(outbound): preserve backticks on <code> tags with attributes
The plain-text sanitizer only matched bare <code> openers, so attributed
variants such as <code class="language-ts"> lost their backtick wrapping
and were stripped to raw text before channel delivery.
Allow optional attributes on the opening <code> tag, consistent with the
existing handling for <h[1-6]> and <li> in the same function.
Fixes#104117
* fix(outbound): preserve attributed inline formatting
Co-authored-by: chengzhichao-xydt <chengzhichao-xydt@users.noreply.github.com>
* test(outbound): compact attributed tag coverage
* fix(outbound): normalize attributed formatting tags
* docs(outbound): clarify attribute normalization invariant
* fix(outbound): preserve native formatting semantics
* docs(plugin-sdk): document sanitizer markup styles
* docs(plugin-sdk): refresh docs map
---------
Co-authored-by: moguangyu5-design <moguangyu5-design@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: chengzhichao-xydt <chengzhichao-xydt@users.noreply.github.com>
* feat(fleet): add openclaw fleet cell supervisor for multi-tenant hosting
* test(fleet): cover cell profile, registry, containers, and service flows
* docs(fleet): document multi-tenant hosting and the fleet CLI
* fix(fleet): verify upgrade replacements and release foreign-collision reservations
* fix(fleet): gate upgrade commit on replacement health
* docs(fleet): mark fleet experimental and pin its single-host scope
* test(fleet): make incomplete-profile casts explicit for test-type lane
* fix(state): regenerate kysely schema artifacts after rebase conflict
* feat: node-hosted plugins — dynamic tools, MCP servers, and skills
Nodes become declarative plugin hosts:
- node.pluginTools.update: node hosts publish plugin-registered agent tool
descriptors; gateway materializes them as agent tools executing via
node.invoke under the node command allowlist, with tools.effective
invalidation and node online/offline removal.
- Trusted paired-node descriptors: no gateway-side plugin registration
required; gateway.nodes.pluginTools.enabled off-switch (default on);
description/count caps; deterministic node-prefixed collision names.
- Declarative node-hosted MCP: nodeHost.mcp.servers (McpServerConfig shape)
starts MCP clients on the node host, publishes tools as pluginId node-mcp,
executes via built-in mcp.tools.call.v1 with per-layer timeouts, failure
isolation, and orphan-safe shutdown. No re-pairing when servers change.
- Node-hosted skills: node.skills.update publishes ~/.openclaw/skills
content (64 skills/64KB/512KB caps both sides); gateway merges them into
the skills snapshot while connected and exec host=node is available, with
node:// locators, node-prefixed collisions, disabled command dispatch,
and gateway.nodes.skills.enabled + nodeHost.skills.enabled switches.
- Security: node-supplied pluginIds cannot satisfy pluginId-scoped tool
allowlists unless gateway-registered; reserved node-mcp id requires the
core MCP descriptor shape; protocol registry kept out of public
plugin-sdk dts.
- E2E: pond harness proves publication, MCP round-trip, skills locator, and
disconnect/reconnect for all three surfaces.
* style: format node-plugin-tools test
* fix(skills): keep status loader unfiltered when eligibility is passed
skills.status started passing eligibility for the node-skill merge, which
flipped loadWorkspaceSkillEntries into filtered mode and dropped disabled
skills from status reports (QA plugin-lifecycle-hot-reload timeout). Status
now merges node skills explicitly around an unfiltered load. Also: regen
docs_map for new node docs sections; add the intentional node-host MCP
onclose suppression to the lint-suppression allowlist.
* fix (Computer Use) Stabilize Codex Computer Use readiness
Co-authored-by: Ben Badejo <188106718+bdjben@users.noreply.github.com>
* fix (Computer Use) Stabilize Codex Computer Use readiness
* fix (Computer Use) Stabilize Codex Computer Use readiness
* fix (Computer Use) Stabilize Codex Computer Use readiness
* fix(clawsweeper): address review for automerge-openclaw-openclaw-103331 (1)
* fix (Computer Use) Stabilize Codex Computer Use readiness
---------
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: Ben Badejo <188106718+bdjben@users.noreply.github.com>
* feat(dashboard): modular dashboard — workspace store, Workspaces tab, sandboxed custom widgets
Squashes openclaw/openclaw#101094 + #101097 + #101098 onto current main and
applies the maintainer review fixes to the backend control plane.
Co-authored-by: 100yenadmin <239388517+100yenadmin@users.noreply.github.com>
* fix(dashboard): UI review fixes — grid, error boundary, embed sandbox, locale
* fix(dashboard): make the CLI and agent broadcasts actually reachable
Three defects only a live run surfaces, all invisible to the unit suites:
- The plugin claimed the CLI command name `dashboard`, which core already owns
(it opens the Control UI). A plugin CLI group that overlaps a core command is
dropped at registration behind a `logger.debug`, so the entire CLI face was
unreachable while `cli.test.ts` kept passing against its own Commander
program. Renamed to `openclaw workspaces`, matching the tab it drives.
- The manifest never declared `activation.onCommands`, so the CLI root resolved
to no owning plugin even once the name was free.
- `dashboard.widget.approve` needs `operator.approvals`; the CLI asked for
`operator.write` on every call. It now requests the approvals scope only for
the approve call, matching `operator-approvals-client.ts`.
Also: agent tools resolved their broadcast from the plugin runtime's
gateway-request scope, an AsyncLocalStorage set only around gateway RPCs and
plugin HTTP routes. An agent turn started from a channel, cron, or heartbeat
therefore wrote the document without emitting `plugin.dashboard.changed`, so an
open Control UI never saw the edit — the feature's headline promise. The gateway
broadcast is server-lifetime, so the plugin now remembers it in a single slot and
agent tools fall back to it.
* docs(web): document dashboard workspaces, provenance, and the custom-widget sandbox
* fix(dashboard): agent-tool ergonomics + close two approval-boundary gaps
From a source-blind agent driving the dashboard_* tools with nothing but their
schemas, and from a Codex review of the hardening delta.
- dashboard_widget_update could never succeed. It passed its whole parameter
record to the patch reader, whose allowlist rejects the very `tab`/`id` keys
the tool's own schema marks required, so every call died on
"unexpected param: tab". Its test only ran Value.Check against the schema and
never executed the tool.
- dashboard_data_read surfaced an `rpc` binding as a thrown error, though its
description promised `binding_client_resolved`. It now returns that as a
result the model can act on.
- Valid widget kinds and the rpc allowlist were undiscoverable: a model saw only
"builtin:<name> or custom:<name>" and "Allowlisted gateway read method", then
brute-forced ~40 calls against errors that named no alternatives. Both schemas
and both validator errors now enumerate them, and the kind description says
what each builtin renders and which binding id it reads. widget_move documents
that grid and toTab are exclusive; widget_scaffold says an operator must
approve, because no agent tool can.
- workspace.replace could mint a pending registry entry for a name that was
never scaffolded. An operator could then approve a widget whose code did not
exist yet, and the agent could write it afterwards. Registry entries now come
from dashboard_widget_scaffold and nowhere else, and approve refuses a name
with no manifest on disk.
- dashboard.widget.approve answered with the whole workspace document, so a
connection holding only operator.approvals could read it through the approvals
door. It now returns the registry entry it changed.
* fix(dashboard): approval pins the code it approves
Codex review found the scaffold-before-approval gate still nameable rather than
binding: approve only proved that widget.json parsed, and the Control UI loaded a
hardcoded index.html rather than the manifest's entrypoint. An agent could
scaffold a widget, win approval on an innocuous or absent entrypoint, then write
the real payload afterwards — code appearing after the human said yes.
Approval now hashes every servable file in the widget directory and stores the
digests on the registry entry, refusing a manifest whose declared entrypoint is
missing. The asset route re-hashes each file it reads and 404s anything that does
not match, so a file edited or added after approval never reaches a browser. The
Control UI loads the manifest's entrypoint, which is the file that was hashed.
The content-type allowlist moves to manifest.ts so the set of files approval
hashes and the set the route can serve cannot drift apart.
Proof, against a running gateway: scaffold -> 404, approve -> 200, rewrite
index.html -> 404, add late.js -> 404.
* fix(dashboard): parse the approved manifest from the bytes that were hashed
Codex found a TOCTOU in the approval path: it loaded and validated widget.json,
then walked the directory again to compute the digests. An agent could swap
widget.json between the two reads, so the operator validated one entrypoint while
the digest froze — and the Control UI later mounted — a different one.
snapshotApprovedWidget now reads the widget directory once: it hashes every
servable file, parses the manifest out of the same widget.json bytes it hashed,
and requires the declared entrypoint to be among them.
Proof, against a running gateway: approve -> index.html 200; rewrite widget.json
to point at evil.html and drop evil.html in -> both 404.
* fix(dashboard): cap approval asset reads; bound the grid fallback search
Two findings from the fourth Codex pass.
Approval hashes agent-authored files that are untrusted until it runs, and read
each one into memory with no size check — dropping one huge .png into a scaffold
directory would stall or OOM the gateway during approve. Sizes are now checked
before the read, with a 2 MB per-file and 8 MB total cap.
nearestFreeSlot searched one band below the lowest occupied row, so a crowded
layout near the bottom could return y=500 as the closest free slot: a placement
the store rejects, which the UI applies optimistically and then snaps back. The
search now stops at the last row a widget of that height can legally occupy.
* fix(dashboard): refuse oversized widget assets before reading them
Approved widget files stay writable and the asset route is unauthenticated, so
swapping an approved small file for a very large one made every GET buffer the
whole file before the digest check rejected it. The route now refuses anything
past the same per-file cap approval enforces, on the stat it already performs.
* fix(dashboard): enforce widget approval boundaries
* docs(changelog): note modular dashboard workspaces
* fix(dashboard): enforce static custom-widget data boundary
* fix(dashboard): satisfy UI lint
* test(dashboard): avoid legacy proto access
* feat(dashboard): make plugin opt-in
* docs(dashboard): refresh workspaces map
* refactor(workspaces): standardize plugin naming
* fix(workspaces): make widget prompt sends idempotent
* docs(workspaces): fix internal path references
* test(workspaces): make prompt assertion lint-safe
* test(workspaces): type prompt request mock
* fix(workspaces): harden approval and binding boundaries
* test(workspaces): complete stale binding client mock
* fix(workspaces): harden widget file boundaries
* fix(workspaces): scope custom widget capabilities
* fix(workspaces): align approval provenance
* fix(workspaces): close branch contract gaps
* test(workspaces): complete builtin context fixtures
* fix(workspaces): aggregate overview usage
* chore(workspaces): defer release note
* chore(workspaces): refresh i18n metadata
---------
Co-authored-by: 100yenadmin <239388517+100yenadmin@users.noreply.github.com>
* feat(control-ui): add chat selection popup with More details and Ask in side chat
* fix(control-ui): keep BTW pending card visible and clear it on resultless terminal runs
* fix(control-ui): route failed BTW runs to an error side-result card and ignore stale side results
* fix(control-ui): correlate BTW pending cards by pre-generated run id and suppress superseded runs
* fix(control-ui): retire abandoned BTW runs so late side events never reach the transcript
* chore(docs): regenerate docs map for btw selection-popup section; fix selection-popup test lint
* feat: sidebar update card (web + macOS) with app-first mac update flow and Sparkle beta track
Squashed from claude/update-notification-display-c6cfb9 after semantic merge
with #104178 (channel-aware CLI installs). See PR #104171 body for details.
* chore(i18n): resync generated inventories after rebase
* chore(i18n): resync locale metadata after rebase
* feat(ui): add gateway browser panel with annotate and inspect modes
* chore(i18n): translate browser panel strings
* fix(ui): address browser panel review findings (stable tab handles, stale view guard, inspect click suppression)
* fix(ui): make the browser panel reload button reload the remote page
* fix(ui): tile the browser dock against the terminal dock and re-probe evaluate per connection
* fix(ui): label page-reported prompt text as untrusted and accept host:port URL entries
* fix(ui): sanitize inspected selector fragments in the annotation prompt
* fix(i18n): retranslate annotation prompt keys so the page-reported provenance label survives localization
* docs: regenerate docs map for the browser panel section
* chore(i18n): reconcile locale bundles with latest main after rebase
* style(ui): format chat-pane imports after rebase
* chore(i18n): refresh locale metadata after rebase
* feat(ui): sessions sidebar redesign with agent sections, smart groups, and draft sessions
- agents become collapsible top-level sidebar sections; expanding an agent
browses its sessions without navigating, replacing the hidden scope select
- built-in smart groups: one section per channel (rows keep their chat
titles), a Work section for worktree/exec-node sessions with a
repo/branch/node subtitle, custom groups, and Chats
- session names never show raw keys or peer ids; DM fallbacks shorten ids,
dashboard sessions read 'New session', unnamed work sessions read as
their checkout
- one + opens the new-session draft dialog: agent, exec host (paired
system.run nodes), folder, worktree toggle with base-branch picker
(worktrees.branches) and optional name; the first message creates the
session and starts the run in one sessions.create call
- custom group catalog/order moves to the gateway (sessions.groups.*) with
a one-time localStorage migration; rename/delete update members
server-side instead of client-side paging
Part of #103431
* fix(ui): resolve dragged sessions across browsed agent sections
Dropping a row dragged out of a non-active agent section now finds the
session in the per-agent row cache instead of only the active scope, so
the category patch is applied instead of silently doing nothing.
Part of #103431
* fix(ui): propagate group catalog changes to open clients
sessions.groups.put/rename/delete now always broadcast a groups-change
event, and the session capability reloads the gateway-owned catalog when
one arrives, so another browser's group create/reorder/rename/delete no
longer leaves this client on a stale snapshot for the rest of the
connection.
Part of #103431
* docs: restore new session dialog section after rebase
* fix(ui): translate new sidebar/session strings and refresh docs map
Real locale translations for the new-session dialog and sidebar keys
(the fallback gate requires zero recorded English fallbacks), plus the
regenerated docs map for the new Control UI section.
Part of #103431
* fix(ui): repair locale metadata after rebase conflict resolution
* fix(ui): merge mainline locale keys with the sidebar redesign strings
* fix(ui): refresh raw-copy baseline for mainline tool-card strings
* fix(ui): reconcile generated locale artifacts after rebase
* feat(ui): add plugin catalog management
* feat(gateway): add plugins.uninstall and richer plugin catalog metadata
Adds a plugins.uninstall gateway method (operator.admin, control-plane write)
backed by a lock-guarded uninstallManagedPlugin that mirrors the CLI flow:
config cleanup, install-record removal, managed file deletion, and registry
refresh. Bundled plugins stay disable-only. Catalog entries now carry a
manifest-derived category and a removable flag; ClawHub search results expose
download counts and verification tiers.
* feat(ui): redesign plugins page with inventory, store shelves, and cover art
Rebuilds /settings/plugins around three tabs: Installed (category-grouped
inventory with overview stats, state filters, uninstall for external plugins,
and inline MCP server management through the shared config seam), Discover
(featured/official shelves plus one-click MCP connectors and curated ClawHub
searches), and ClawHub (search with download counts and verification badges).
Every catalog entry renders bundled cover art or a deterministic gradient
monogram tile - no more empty boxes. Artwork generated with Codex CLI, shipped
as 512px WebP under ui/public/plugin-art.
* chore(ui): regenerate locale bundles for plugins manager strings
* docs: describe plugins manager tabs, uninstall, and MCP connectors
* fix(plugins): human catalog labels and un-pinned hosted fallback ids
listManagedPlugins now prefers manifest names over registry package-name
backfill, falls back to channel catalog labels and blurbs, and stops pinning
expectedPluginId when a hosted feed entry only exposes its package name
(which rejected every legitimate install of that package). Found via live
gateway testing against ClawHub.
* fix(ui): send minimal RFC 7396 merge patches for MCP server edits
config.patch merges rather than replaces, so key removal needs an explicit
null; sending the full config back made MCP server removal a no-op. Found
via live gateway testing.
* fix(ui): write explicit MCP transports for URL servers
The MCP runtime defaults URL-only servers to SSE, so streamable HTTP
endpoints saved by the add form or connector templates would fail at
connect time. Connector templates now declare their transport and the
add form infers streamable-http unless the URL follows the /sse
convention. Flagged by autoreview against the transport resolver.
* test(ui): wait for deferred plugin requests before resolving in e2e
* feat(ui): plugins detail view, action menus, and unified ClawHub search
Reworks the plugins page from PR #103176 feedback: merges the ClawHub tab into
Discover (typing searches ClawHub inline and appends a quiet From ClawHub
section, with Browse ClawHub demoted to a header text link), makes every row and
store card open a plugin detail overlay (hero art, primary enable/install
action, metadata table), and replaces enable/disable switches with a state chip
plus an overflow menu (Enable/Disable, Remove for external plugins, View
details) matching the ChatGPT-store install+menu pattern. MCP rows use the same
menu; refresh is now icon-only.
* chore(ui): regenerate locale bundles for plugins UI iteration
* feat(ui): vetted, grouped connector catalog for the plugins store
Expands Connect your world to 28 connectors organized into use-case shelves
(Work & productivity, Coding & infrastructure, Home & media, Everyday life).
Every entry passed a three-stage subagent review: official-docs verification
plus live endpoint probes for MCP servers, ClawHub result-quality and
malware/typosquat screening for curated searches, and an adversarial pass
that dynamically registered OAuth clients to prove one-click viability.
That review removed Figma (registration allowlisted, 403) and Atlassian
(OAuth issuer-mismatch bug upstream), downgraded GitHub to PAT-based setup
(no dynamic client registration upstream), fixed Linear (/sse retired) and
Home Assistant (/api/mcp, streamable HTTP) endpoints, retargeted poisoned or
dead searches (youtube, finance, hue dropped; calendar -> google calendar;
stocks replaces finance), and added Todoist, Airtable, Canva, Stripe,
Context7, DeepWiki, Hugging Face one-click MCP servers plus Jira, PDF,
transcription, Kubernetes, Reddit, maps, translation, and notes searches.
Keyless servers get a ready-to-use success message; new cover art included.
* chore(ui): regenerate locale bundles for connector groups
* fix(plugins): suppress hosted catalog rows once their package is installed
Hosted feed entries without a declared runtime id fall back to their package
name as catalog id, which never matches the installed runtime id, so the
Discover shelf kept offering an already-installed package. Installed package
names now also suppress official rows. Flagged by autoreview.
* fix(plugins): pin declared runtime ids and surface connector errors in place
The runtime-id pin now keys off explicitly declared catalog ids (plugin,
channel, or provider) instead of string-comparing against the package name,
so declared ids that equal their package name stay enforced while entry-id
fallbacks stay unpinned. Connector add failures on Discover now render on the
triggering card instead of the Installed tab's MCP section. Both flagged by
autoreview; regression tests included.
* feat(ui): full inventory artwork, pulse header, and two-column plugin list
Every bundled plugin now ships distinctive cover art (113 new Codex CLI
illustrations; 172 total, ~2.1MB WebP), so inventory rows and detail views
never fall back to monogram tiles. The four stat cards give way to a compact
inventory pulse: a segmented enabled/disabled/issues meter whose legend and
counts live inside the filter chips. Inventory, MCP, and search rows flow
into two columns when the panel is wide enough.
* chore(ui): regenerate locale bundles for pulse header
* fix(ui): omit stdio args from the MCP server row target
Stdio MCP args routinely carry tokens, and the inventory is visible to
read-only operators; mirror the config page and show only the command.
Flagged by autoreview; regression test included.
* fix(merge): point crestodian setup at relocated plugin commit/refresh modules
* fix(merge): add bootstrapToken to plugins page test gateway harness
* fix(plugins): name catalog install-action branches so Swift emits the union
* fix(ui): satisfy strict lint on plugins page form parsing and mocks
* chore(build): regen docs map, raise plugin-sdk declaration budget for new protocol surface
* fix(ui): type the plugins page patch mock with its real call signature
Paired devices can now carry a durable operator-assigned label: device.pair.rename { deviceId, label } (schema-bounded, admin/ownership-gated) stores an operatorLabel that persists in the shared SQLite state DB, survives device repair and re-approval, and takes display precedence over the client-reported name in CLI devices list and the Control UI inventory (operatorLabel, then displayName, then clientId, then deviceId). The label was previously dropped on write because the pairing store had no column for it. CLI: openclaw devices rename --device <id> --name <label>. Docs cover the command and precedence.
Fixes#13870
Thanks to @bladin for the contribution.
Co-authored-by: heichl_xydigit <1740879+bladin@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* feat(ui): restore sidebar chrome and remove the pane workspace strip
Reverse course from #103426 on the app chrome: the left sidebar owns
brand, pinned navigation + More, New session, sessions, and the footer
(status dot, Settings, Docs, pairing, theme) again, and the desktop
topbar is gone. What stays from that PR: the dockable workspace rail
(right/bottom, drag or button), the in-flow split-pane headers, and
Cmd+B now hides the sidebar entirely (no 78px icon rail) with a
floating expand control.
The real target of the original request: the vertical icon strip at
each pane's right edge is deleted. A collapsed workspace rail renders
nothing; the toggle (with a changed-file badge) lives in the split-pane
header next to split/close, or floats at the top-right in single-pane
chat. Shift+Cmd+B still toggles.
Narrow native macOS windows (e.g. the in-app link browser splitting
the window) previously stacked 50px of injected titlebar padding on
top of the 58px drawer row; the web CSS now folds that into one
compact 58px row beside the traffic lights, with selectors that
outrank the rules shipped Mac apps inject.
* chore(ui): regenerate locale bundles and docs map for sidebar restore
* fix(ui): bind showPaneHeader explicitly on the classic single pane
* chore(ui): reconcile locale metadata after rebase onto lobster wild cards
* feat(channels): narrated progress drafts + activity receipt on the final answer
Progress mode replaces raw tool lines with short utility-model narration of
what the agent is doing (streaming.progress.narration, default on, requires
an explicit utilityModel). On Discord the final answer now carries the
-# activity receipt and the working draft is deleted once the answer lands,
so busy channels keep no orphaned tool log above the reply.
Formatting verified remotely via Testbox oxfmt --check (hook bypassed:
no node_modules in this worktree).
* fix(channels): keep narration toggle independent of channel-default stream mode
Discord resolves its own progress default, so the resolver must not re-derive
mode with the generic partial fallback (narration was off for unset config).
* fix(auto-reply): honor status-only command text in narration model input
streaming.progress.commandText: "status" hides raw exec/bash text from the
channel draft; narration input now mirrors that policy so the utility model
never receives more command detail than the draft shows (Codex review P2).
Formatting verified remotely via Testbox oxfmt --check.
* fix(auto-reply): share the draft's command-tool set for narration and regen channel metadata
Reuse isCommandToolName (exec|shell|bash) so narration's commandText policy
matches the draft formatter exactly, and regenerate bundled channel config
metadata for the new streaming.progress.narration key (Codex review round 2).
Gates verified on Testbox: config:channels:check, oxfmt --check, 4 test shards.
* fix(channels): clear stale narration when the narrator stops mid-turn
An empty narration update now falls the draft back to raw tool lines, and the
narrator emits that clear when it disables after consecutive failures or the
per-turn cap, so drafts never pin stale status text (Codex review round 3).
Gates verified on Testbox: oxfmt --check + 4 test shards.
* chore(config): regen bundled channel metadata after rebase onto main
* chore: CI fixups — lint nits, test harness types, docs map, SDK surface budget
Two deliberate public SDK additions (resolveChannelStreamingProgressNarration,
isCommandToolName via the streaming wildcard re-export) bump the pinned
public-surface budgets to current counts (exports 10488, callable 5235).
Gates verified on Testbox: oxfmt, targeted oxlint, docs:map:check,
config:channels:check, check:test-types, 5 test shards.
* feat(ui): move app chrome into a topbar and dock the session workspace rail
The desktop shell now uses a slim topbar for brand, primary navigation
(Chat + pinned routes + More menu), command-palette search, pairing,
theme, and Settings; the left column slims down to a sessions-only
panel (Cmd+B hides it entirely — the 78px icon rail is gone). Split
view drops the fixed geometry-mirroring toolbar for in-flow per-pane
headers. The session workspace rail can dock right or bottom inside
its pane — drag its header between edges or use the dock button — and
the collapsed strip's file glyph is now a real button (it used to be a
dead, button-looking span) with a changed-file count badge. The Mac
app's injected chrome CSS slims down accordingly; its drag-region
geometry is unchanged and the topbar brand strip stays passive under
it.
* chore(ui): regenerate locale bundles for topbar and workspace dock strings
* fix(ui): keep the native macOS drawer clear of the titlebar overlay
The narrow-width slide-over drawer sits fixed at the window top, over
the AppKit traffic lights and drag regions. The old Mac-app-injected
CSS padded .sidebar-shell for this; that rule moved web-side for the
desktop topbar, so restore drawer clearance here for both the app
drawer and the in-drawer settings sidebar.
* chore(ui): translate topbar and workspace dock strings; regen docs map
A fun field guide for the Control UI lobster: what it is, when it
visits, how to interact (hover names, pokes, right-click shoo), the
Appearance toggle, the Lobsterdex, reduced-motion and privacy notes.
Deliberately teases rather than documents the rare variants, events,
and the anniversary. Registered in the Web interfaces nav group and the
generated docs map.
* fix(msteams): read file attachments on Teams channel messages
Three bugs blocked reading files attached to channel messages:
- Graph /teams/{id} used channelData.team.id (the 19:..@thread.tacv2 thread id)
instead of team.aadGroupId (the AAD group GUID) -> 400.
- The Graph fetch was gated on an <attachment id> HTML marker that channel
@mention activities don't carry -> fetch skipped.
- A thread reply was fetched at /messages/{replyId} (404) then fell back to the
bare thread root, returning the root's file for every reply. Replies live at
/messages/{root}/replies/{replyId}.
Fixes#89594
* fix(msteams): gate Graph media fallback on text/html stub; run trigger tests in channel context
* fix(msteams): canonicalize Graph attachment recovery
Build one canonical Graph message URL per Teams activity, recover marker-free channel and group-chat file shares, and retain bounded current-main attachment handling.
Co-authored-by: Colton Williams <colton@coltons-apps.tech>
* fix(msteams): canonicalize Graph media recovery
Recover channel and group-chat files through one fail-closed Graph identity path, bound inbound enrichment, and remove invalid app-only Graph fallbacks.
Co-authored-by: Colton Williams <colton@coltons-apps.tech>
Co-authored-by: Joshua Packwood <joshua.packwood@gmail.com>
---------
Co-authored-by: Colton Williams <colton@coltons-apps.tech>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Joshua Packwood <joshua.packwood@gmail.com>