# Channels page restoration — diagnosis

Branch: `feat/opencode-swappable-backend`
Investigated: 2026-05-27

## Summary

The Channels page looks "reverted" because the per-channel card
enhancement was **never wired in**, not because something was reverted.
Three new files exist on disk but no other module imports them, so the
live render path still uses the old per-provider chrome. The
schema-unavailable fix never landed for the same reason.

## Evidence

### Orphan files (created today, never imported)

| Path | Size | Mtime | Importers |
|------|------|-------|-----------|
| `ui/src/ui/views/channels.card.ts` | 2962 B | 2026-05-27 17:59 | only `channels.logos.ts` (its own dep) |
| `ui/src/ui/views/channels.logos.ts` | 7112 B | 2026-05-27 18:00 | only `channels.card.ts` |
| `ui/src/styles/channels.css` | 11329 B | 2026-05-27 17:59 | none |

Verified with:

```
$ grep -rn --include="*.ts" --include="*.css" -E 'channels\.(card|logos)|/channels\.css' ui/ src/
ui/src/ui/views/channels.card.ts:3:import { getChannelBrand, renderChannelLogo } from "./channels.logos";
```

The CSS is not referenced from `ui/src/styles.css` either:

```
$ cat ui/src/styles.css
@import "./styles/base.css";
@import "./styles/layout.css";
@import "./styles/layout.mobile.css";
@import "./styles/components.css";
@import "./styles/config.css";
@import "./styles/chat.css";
@import "./styles/chat/sidebar-enhanced.css";
@import "./styles/nav-enhanced.css";
@import "./styles/terminal-clock.css";
@import "./styles/onboarding.css";
```

### Git history shows no prior commit

```
$ git log --all --oneline -- 'ui/src/ui/views/channels*' | head
8e9a044 feat(autonomy): shared validateSpawnParams + sanitizeToolResult
52c683c Drop stale .js files shadowing .ts sources
ff6dbc8 feat: mobile app, loops/email integration, notion import + conflicts, satellite config sync
7274f93 Merge pull request #1 from MrYungCEO/prod-cleanup-strip-creds
b5c5129 Full Moltbot repo - all source, extensions, UI, docs, skills, apps

$ git log --all --oneline --grep="channels.*card\|channels.*enhance\|schema.*unavail"
# (no results)
```

`git stash list`:

- `stash@{0}` — WIP across the branch, does NOT contain `channels.card.ts` /
  `channels.logos.ts` / `channels.css` (those are untracked, would need
  `stash -u`). It does contain modifications to all the existing provider
  cards under `channels.<provider>.ts`.
- `stash@{1}` — hyperframes-translator only, unrelated.

**Conclusion**: the work was created today (2026-05-27) and never landed
in a commit. There is no prior version to recover; the orphan files are
the intended new chrome and just need to be wired in.

## Live render path

```
app-render.ts:1387        state.tab === "channels"
  → renderChannels(props)            ui/src/ui/views/channels.ts:34
    → renderChannel(key, …)          ui/src/ui/views/channels.ts:116
      → renderWhatsAppCard(…)        ui/src/ui/views/channels.whatsapp.ts:9
      → renderTelegramCard(…)        ui/src/ui/views/channels.telegram.ts:8
      → renderDiscordCard(…)         ui/src/ui/views/channels.discord.ts:8
      → renderGoogleChatCard(…)      ui/src/ui/views/channels.googlechat.ts:8
      → renderSlackCard(…)           ui/src/ui/views/channels.slack.ts:8
      → renderSignalCard(…)          ui/src/ui/views/channels.signal.ts:8
      → renderIMessageCard(…)        ui/src/ui/views/channels.imessage.ts:8
      → renderNostrCard(…)           ui/src/ui/views/channels.nostr.ts:22
      → renderGenericChannelCard(…)  ui/src/ui/views/channels.ts:201 (fallback for any other key)
        → renderChannelConfigSection({ channelId, props })  ui/src/ui/views/channels.config.ts:98
          → renderChannelConfigForm({ schema, … })           ui/src/ui/views/channels.config.ts:70
```

Each per-provider card opens a `<div class="card">` with a
`<div class="card-title">` (the only branding — text only). No logos, no
brand colour, no status pill. `renderChannelCard` from the orphan file
is the intended replacement chrome.

### Bonus bug spotted on the way through

`ui/src/ui/views/channels.ts:147` passes the GoogleChat status as the
property `googlechat:` (lower-case), but `renderGoogleChatCard` in
`ui/src/ui/views/channels.googlechat.ts:11` destructures `googleChat`
(camel-case). Result: every status field on the GoogleChat card shows
`n/a` even when the snapshot has live data. Easy to fix when we revisit
the call site for the new chrome.

## "Schema unavailable" callout — root cause

The callout text is emitted in two places:

```ts
// ui/src/ui/views/channels.config.ts:73-78
if (!normalized) {
  return html`<div class="callout danger">Schema unavailable. Use Raw.</div>`;
}
const node = resolveSchemaNode(normalized, ["channels", props.channelId]);
if (!node) {
  return html`<div class="callout danger">Channel config schema unavailable.</div>`;
}
```

The pipeline:

1. UI calls RPC `config.schema` (`ui/src/ui/controllers/config.ts:60`) →
   stores `state.configSchema = res.schema ?? null`
   (`controllers/config.ts:76`).
2. Schema is built server-side by `buildConfigSchema(...)`
   (`src/config/schema.ts:1001`). It calls `stripChannelSchema(...)`
   first, which wipes `channels.properties` and sets
   `channels.additionalProperties = true` (`schema.ts:978`). Then
   `applyChannelSchemas(...)` (`schema.ts:946`) re-adds a property under
   `channels.<channelId>` for every channel that has a `configSchema`
   registered on its plugin entry. Channels without a registered
   `configSchema` end up with no property at all.
3. UI runs the response through `analyzeConfigSchema` which preserves
   `additionalProperties: true` on the channels node but marks the path
   as "unsupported".
4. `resolveSchemaNode(normalized, ["channels", channelId])`
   (`channels.config.ts:21`) drills in. When `properties[channelId]`
   doesn't exist AND `additionalProperties` is the boolean `true` (not
   an object), the function returns `null`, triggering the second
   "Channel config schema unavailable" callout.

So the callout fires whenever:

- the gateway hasn't returned a schema yet (loading races / loss of
  connection) → "Schema unavailable. Use Raw."
- a channel plugin doesn't register a `configSchema` (e.g. `voice-call`,
  `twitch`, `tlon`, `lobster`, `nextcloud-talk`, `line`, `matrix`,
  `bluebubbles`, `zalo`, `zalouser`, `msteams`, `mattermost` — most of
  these DO have schemas but the generic-card path will hit them too if
  someone enables a channel without a schema or with a misregistered
  one) → "Channel config schema unavailable."

The chat-channel extensions on the happy path (discord, telegram, slack,
signal, imessage, googlechat, nostr, whatsapp) all do register a
`configSchema`. But the second-callout path is structural: any channel
without a registered schema gets a scary red `callout danger` instead of
a friendly "no editable config for this channel" empty state.

### Fix shape

- `analyzeConfigSchema` already passes through `additionalProperties:
  true` on the `channels` node, so the schema itself is fine.
- The fix is in `resolveSchemaNode` (the walker) + the empty-state UI:
  - When the parent node has `additionalProperties: true` and the named
    property is absent, treat it as "this channel has no editable
    config" — render a muted info note, not a `callout danger`. (Same
    treatment for `properties` absent when the schema endpoint is
    healthy.)
  - When `props.configSchema` is `null` AND `configSchemaLoading` is
    `false`, distinguish "schema endpoint failed" (show a danger
    callout with retry button) from "no schema yet because we haven't
    loaded it" (treat as still-loading and don't render the form area).

## Files that need editing to wire the new cards

| File | Change |
|------|--------|
| `ui/src/styles.css` | Add `@import "./styles/channels.css";` |
| `ui/src/ui/views/channels.whatsapp.ts` | Wrap body in `renderChannelCard` |
| `ui/src/ui/views/channels.telegram.ts` | Same |
| `ui/src/ui/views/channels.discord.ts` | Same |
| `ui/src/ui/views/channels.googlechat.ts` | Same + fix `googlechat`/`googleChat` param name |
| `ui/src/ui/views/channels.slack.ts` | Same |
| `ui/src/ui/views/channels.signal.ts` | Same |
| `ui/src/ui/views/channels.imessage.ts` | Same |
| `ui/src/ui/views/channels.nostr.ts` | Same |
| `ui/src/ui/views/channels.ts` | `renderGenericChannelCard` should also use `renderChannelCard` for non-built-in channels |
| `ui/src/ui/views/channels.config.ts` | Soften "Schema unavailable" callouts; treat `additionalProperties: true` + missing prop as a friendly empty state |
| `ui/src/ui/views/channels.config.test.ts` (new) | Unit-test the resolver + the empty-state behaviour |

`channels.logos.ts` already includes brand entries for the seven
chat-channel extensions plus `bluebubbles`, `mattermost`, `msteams`. Any
other provider on disk (line, matrix, nextcloud-talk, tlon, twitch,
voice-call, zalo, zalouser) needs adding only if it appears in the live
channel snapshot — the generic-card fallback covers them today.
