Skip to content

feat: add contributes.panelTabs app-manifest contribution - #7975

Merged
bolichen97 merged 1 commit into
kirodotdev:mainfrom
suhasaitham22:feat/panel-tab-registry-seam
Sep 7, 2026
Merged

feat: add contributes.panelTabs app-manifest contribution#7975
bolichen97 merged 1 commit into
kirodotdev:mainfrom
suhasaitham22:feat/panel-tab-registry-seam

Conversation

@suhasaitham22

@suhasaitham22 suhasaitham22 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Problem / Motivation

An installed app has no way to add a tab to the chat side panel. contributes on main carries commands, which a manifest-only app can already use to reach the Command Bar — but nothing lets an app own a panel surface, and the runtime app TabKind is not it: that is a per-tool-call MCP App render keyed by toolCallId, ephemeral and never persisted.

What changed

contributes.panelTabs[] — a second field on the existing Contributes, alongside commands:

"contributes": {
  "panelTabs": [{
    "id": "browser",
    "title": "Docs",
    "menuLabel": "Open docs",
    "menuDescription": "Browse and pull documents",
    "icon": "BookOpen",
    "entry": "ui/panel.mjs"
  }]
}

The tab kind is app:<app_name>:<id>; at most 8 tabs per app.

  • Backend (apps/manifest.py) — PanelTabConfig + panelTabs on Contributes, threaded through its to_dict / from_dict / validate. commands is untouched: both fields parse, serialize and validate side by side. Caps and malformed-input reporting mirror the commands precedent (bad_panel_tabs / dropped_panel_tabs, and _MAX_PANEL_TABS_PER_APP enforced on both sides). Entry paths go through the existing _path_escapes_app_root.
  • Manifest → host (hooks/panelTabRegistry.ts, added) — resolvePanelTabs(apps) over a locally pinned subset of GET /api/apps, the same posture as overlaySlots.ts; usePanelTabDescriptors() reuses the shared ['apps'] query, so no extra request. Enabled apps only, ordered by app name, malformed or duplicate declarations warned and skipped.
  • ESM render (components/AppHost.tsx) — an optional entry override mounts a panel-tab bundle through the host ui.pages already use; active flows to AppApiProvider so a hidden body can pause. No iframe (see below).
  • Tab model (hooks/usePanelTabs.ts) — TabKind gains `app:${string}`; app tabs persist as { kind, appName, appTabId } metadata and re-mount on load; an orphaned tab (app disabled/uninstalled) is hidden at READ and returns when the app does.
  • Panel (pages/chat/SidePanel.tsx) — the app kind is intercepted before the VIEW_KINDS branch; declared tabs appear in the + menu and the empty-panel launcher (which renders menuDescription).
  • Icons (apps/appIcons.tsx, added) — an app-facing name→glyph set. Deliberately not builtinIcons.tsx: that registry is only populated by registerBuiltinIcons() at edition-composition module load, so a runtime-installed app can never register into it and every app icon would silently fall back. Bounded rather than a lookup over lucide's full export, which would bundle ~1k components; the allowed names are documented.

Why it matters

An installed app can own a full side-panel experience — a document browser, a review queue, a live log — next to the chat it belongs to, declared in its manifest and rendered through the same ESM host ui.pages already use. Before this, a panel surface was reachable only by editing the core tab machinery (usePanelTabs / SidePanel), which a downstream fork then re-applies on every sync; the runtime app TabKind is not an alternative, being an ephemeral per-tool-call MCP render keyed by toolCallId and never persisted. A declared tab persists as {appName, appTabId} metadata, re-mounts on reload, and disappears cleanly when its app is disabled — so the edition registers nothing and installing the app is the whole integration. With no app declaring panelTabs, the registry resolves empty and the stock build is byte-identical to main.

Addressing the review

  • contributes already exists on main — correct, and the earlier revision was wrong to add a second class. panelTabs is now a field on main's Contributes; commands keeps parsing, serializing and validating (test_commands_survive_alongside_paneltabs pins that both survive a round trip). Rebased onto main.
  • Icon was not app-reachable — fixed as above. The documented example and the fixtures now use a name that actually resolves (BookOpen); BookMarked is not in the pinned lucide version's verified set here, so it is out of the doc.
  • Silent coercionbad_panel_tabs / dropped_panel_tabs mirror bad_commands / dropped_commands and are reported from validate(). The test that codified the silent path is inverted; non-object contributes and non-object entries are covered too.
  • types.tspanelTabs?: unknown, alongside commands?: unknown. The shape stays in panelTabRegistry.ts.
  • activeId repair — now scoped to the case where the prune actually hid the active tab. A stored activeId naming no tab still yields no active tab, as on main.
  • Read-path capresolvePanelTabs slices to MAX_PANEL_TABS_PER_APP and warns.
  • Tests — added: the app: kind takes the AppHost intercept and never reaches ActivityViewer; the + row and the launcher each open it; dedupe to one instance; the per-app cap; the scoped-vs-unscoped activeId cases.
  • Nits_MAX_PANEL_TABS_PER_APP is module-private on the Python side; isPanelTabKind is a type predicate, so the KIND_ICON cast is gone.

ESM, not an iframe

Keeping ESM, per the review. Two properties of a panel tab are genuinely new versus a page and worth naming: it stays mounted while hidden and is restored on reload (durable background execution a page does not get), and it is co-mounted with the chat composer. Neither is addressed by an iframe here — AppHost on main is same-realm, same-origin with no sandbox, CSP or module allowlist, so exposure is already total for ui.pages. Real app isolation is a separate cross-cutting decision (sandboxing for pages and panel tabs alike, or an explicit "installed apps are trusted code" tenet); filing that rather than changing the mechanism in this PR.

Tests

Backend: flake8 clean, mypy clean on the changed file, the repo black gate passes, docs-lint passes, brand gate passes, and scrub-lint's working-tree scan is clean (its 2 failures are the pre-existing git-history item it defers itself).

Frontend tsc / vitest could not run on the authoring host — no npm egress — so CI is the gate for them. Verified by inspection: Exclude<TabKind, \app:${string}`>leaves'app'inBuiltinTabKindand the type predicate narrows the else branch to exactly that;InstalledAppis structurally assignable toAppHost's app` prop.

Screenshots / video

Why no screenshot: No user-visible change in the stock build — no app on main declares contributes.panelTabs, so the registry resolves empty and every render site is inert. A contributed tab appears only when an installed app declares one, which is out of scope for this PR.

@suhasaitham22
suhasaitham22 requested a review from a team September 2, 2026 19:25
@suhasaitham22
suhasaitham22 requested a review from a team as a code owner September 2, 2026 19:25
@github-actions github-actions Bot added fork Pull request from a fork (external contributor) readiness: checking Automated validation is still running labels Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5.1, fork) — 🟡 CONCERNS

UX-level review of e934f8f89cde45a7769bef818a4cae321e6c7791 via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

I have enough to review. This is a fork-lane UX review: no blind read, no fork checkout, and the PR explicitly ships <!-- no-visual-delta --> (stock build is byte-identical; the new surfaces render only when an installed app declares contributes.panelTabs).

Reconcile — user-visible controls the diff adds (all app-data-driven, none present in the base repo, none in any screenshot, no blind reader has seen them):

  • +-menu row per contributed tab (icon + menuLabel) — SidePanel.tsx:1373-1382
  • empty-panel launcher card (icon + menuLabel + menuDescription) — SidePanel.tsx:1395-1409
  • strip tab chip (icon + title) — iconForKind/TabChip
  • app-list error banner AppPanelTabsErrorNoticeSidePanel.tsx:1507-1515

No BLOCK exits fire: no blind read (so the primary-control exit is disabled by lens 12); the orphaned-tab hide/restore is app-lifecycle (lens 4), not a hard swap of a persistent identified element (lens 13); the error notice carries a real errMessage(error) and an askAgent action, so it neither hedges nor is empty. The copy on every new surface is the app author's own literal, not authored by this PR, so first-time comprehension of those strings is not this PR's to answer — but the chrome and placement are, and none of it has been seen by a cold reader.

UX-Verdict: CONCERNS

No first-time reader has seen any of the contributed-tab surfaces, and none can appear in a screenshot without a demo app declaring panelTabs — the feature's whole UI is unverified by eyes.

Evidence gaps

  • Blind read did not run (fork lane): every contributed-tab surface below is unverified for comprehension.
  • +-menu contributed-tab row (SidePanel.tsx:1373) — in no screenshot; needs a capture with an app declaring panelTabs.
  • Empty-panel launcher card incl. menuDescription (SidePanel.tsx:1395) — in no screenshot.
  • Strip tab chip for an app: kind, including the icon-fallback glyph (iconForKind, TabChip:1545) — in no screenshot.
  • AppPanelTabsErrorNotice top banner on ['apps'] failure (SidePanel.tsx:1507) — the one error state the PR itself authors; in no screenshot.

Suggestions

  • Unrecognized/absent icon falls back to <PanelRight> (appIcons.tsx:747), the exact glyph KIND_ICON.app uses for the MCP app frame — an icon-less contributed tab is visually identical to an MCP app tab in the inactive (icon-only) strip. Consider a distinct app-contributed fallback glyph.

[UX-REVIEWED] e934f8f

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5.1, fork) — ✅ PASS

Design-level review of e934f8f89cde45a7769bef818a4cae321e6c7791 via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design assessment complete. This adds contributes.panelTabs — a declarative contribution point letting an installed app mount its own ESM bundle as a chat side-panel tab through the same in-process AppHost that ui.pages already use, plus an active visibility flag in the app SDK.

The load-bearing design decisions are all sound:

  • No new trust boundary. Panel-tab bodies run in-process in the dashboard origin exactly as ui.pages bundles already do — the PR reuses AppHost, not a new host. It correctly adds panelTabs to the signing payload so a tab's executed entry can't be repointed under a valid signature, applies the same path-traversal containment, and threads the owning-slot sessionKey so the restricted-session guard doesn't fail open.
  • Contributes-as-data. Core reads declarations and never imports app code, matching publishProvider/commands. Both sides enforce the per-app cap; malformed manifests, non-array/non-object shapes, hand-edited localStorage kinds, and app-list fetch failures all degrade rather than throw.
  • Backward-compatible. active and contributes.panelTabs are additive and optional (undefined→visible; pre-contribution manifests hash identically). Docs (manifest-reference, getting-started, feature-map) move in the same commit.

Failure modes (app disabled mid-session, prune-on-read vs. delete, cross-slot mount survival) are handled and pinned by tests. Nothing rises to a design concern; remaining items are line-level and out of scope.

Design-Verdict: PASS

Sound, proportionate reuse of the existing in-process app host; the one real risk (repointing a signed, executed entry) is closed by the signing-payload guard.

[DESIGN-REVIEWED] e934f8f

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review (fork) — ✅ no blocking findings

Reviewed e934f8f89cde45a7769bef818a4cae321e6c7791 via the fork AI-review pipeline; updated in place on each push.

Review details

I've verified the full chain from the diff and the surrounding code. The working tree sits on main, so on-disk files show the pre-PR base — which confirms the diff's "before" for useAnyLiveAppTab (t.kind === 'app' only) and the wiring useAnyLiveAppTab()hasLiveAppTab (useChatPageResourcesController.tsx:144) → shouldMountSidePanel (if (hasLiveAppTab || hasBrowserTab) return true).

Re-deriving the candidate against the diff:

  • (a) User opens an app-contributed panel tab (app:<name>:<id>), then disables/uninstalls that app.
  • (b) The tab persists in the raw bySlot store (loadPersisted comment: "No descriptor prune here… hidden on the READ path"; the read-path prune in usePanelTabs never rewrites storage). The PR's useAnyLiveAppTab now matches it via || isPanelTabKind(t.kind) reading that raw store → returns true → hasLiveAppTab true → shouldMountSidePanel returns true unconditionally.
  • (c) The closed SidePanel subtree stays mounted-and-hidden across reloads; the orphan is hidden from the strip so the user has no control to release it. AppPanelTabBody renders null, so nothing is preserved — a pure mount leak with no recovery path except re-enabling the app.

The mechanism is solid and reachable. Severity is low: the body is null so nothing runs inside, no crash/data-loss/security — advisory only. This is the sole candidate; it does not block.

No blocking issues; one advisory finding.

FINDING — website/src/hooks/usePanelTabs.ts:312 — useAnyLiveAppTab/useAllAppTabs count an orphaned contributed tab via isPanelTabKind(t.kind) in the raw unpruned store, so after an app with an open contributes.panelTabs tab is disabled/uninstalled the tab lingers in the bucket, hasLiveAppTab stays permanently true, and shouldMountSidePanel keeps the closed SidePanel subtree mounted-and-hidden across reloads with no user control to release it (the orphan is hidden from the strip and its AppPanelTabBody renders null, so nothing is preserved) → Fix: have the liveness/mount guards disregard contributed tabs whose descriptor is absent (or prune the orphan from the store on app disable/uninstall) so a disabled app's tab stops pinning the panel mounted.

[OPUS-REVIEWED] e934f8f

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5.1, fork) — 🟡 CONCERNS

Premise-level review of e934f8f89cde45a7769bef818a4cae321e6c7791 via the fork AI-review pipeline — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

I have enough to complete the review. Base facts confirmed: contributes.commands/CommandContribution exist as the sibling declarative-contribution seam (manifest.py:1388,1648), and the app TabKind is the ephemeral MCP render keyed by toolCallId (usePanelTabs.ts:591-630), so the description's "already exists?" analysis is honest.

First-Principles-Verdict: CONCERNS

Large permanent public surface (manifest field + SDK active + persisted app: kind) landed with zero in-repo apps declaring it; the 34-name icon set is the most speculative piece.

What this change ships

Intent: let an installed app own a persistent chat side-panel tab that mounts its own ESM bundle, without forking core tab machinery. This is an ADDITION.

  1. Apps declare contributes.panelTabs[] → a persistent side-panel tab mounting the app's bundle — justified (named fork-resync harm; sibling of contributes.commands).
  2. New persisted TabKind arm app:<app>:<id> — justified (mechanism for test: validate CI workflows on KiroCrew #1).
  3. useAppInfo().active + plumbing through AppHost/AppApiProvider — justified (a mounted-while-hidden body needs a pause signal a page never did).
  4. AppHost gains entry override + sessionKey — justified (mount a non-page bundle; incognito fail-open guard = named security boundary).
  5. appIcons.tsx 34-name icon set — zero declared consumers, generalized.
  6. Orphan app-tab hidden-at-read + setOrder preserves hidden orphan — justified (persist-across-disable mechanism).
  7. activeId repair scoped to pruned-active case — justified.
  8. Signing payload now covers a panelTabs-only manifest — justified (named tamper harm: repoint executed entry).
  9. ui.entry/ui.pages[].entryPoint re-documented "relative to ui/" not app root — rides along (existing-field doc change, undeclared).
  10. manifest-reference / getting-started / feature-map doc additions — justified (doc-in-same-commit).

Watch

  • The whole feature is inert in the stock build ("byte-identical to main"): no app, fixture, or example declares panelTabs, so nothing exercises the manifest field, the active signal, or the app: kind end-to-end. Inherent to an app-kit seam like contributes.commands, so accepted — but it is one-way-door surface shipped ahead of any consumer.
  • appIcons.tsx: 34 hardcoded names, 0 consumed (grepped icon: uses — only the BookOpen doc/fixture example). The appIcon fallback glyph already removes the no-throw/no-icon harm; the set removes only a cosmetic "app picks its glyph" harm.

Subtractions

  • Shrink APP_ICONS in website/src/apps/appIcons.tsx: the fallback panel glyph is the whole harm removal; ship fallback + a handful and widen when an app names one, rather than pinning 34 (each a maintained import + doc-table row) with no consumer.

[FIRST-PRINCIPLES-REVIEWED] e934f8f

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review (fork) — ✅ no blocking findings

Reviewed e934f8f89cde45a7769bef818a4cae321e6c7791 via the fork AI-review pipeline; updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] e934f8f

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Sep 3, 2026

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks — verified the headline claim: ViewKind (website/src/hooks/usePanelTabs.ts:9) is untouched and all four exhaustive Record<ViewKind,…> tables (VIEW_TITLE_KEY, NEW_MENU_LABEL_KEY, NEW_MENU_DESC_KEY, NEW_MENU_GROUPS) stay exhaustive. Body-owning TabKind is the right call. Diff is free of product-specific naming. Requesting changes for the typing decision, one real persistence bug, and a direction change on my side.

Direction

Pippin will ship as a standalone app end to end (own repo + catalog entry), not "app backend + edition thin frontend". So the seam has to be usable by an installed app. As written it is edition-only: the descriptor holds a live ComponentType + ReactNode icon, registered by a build-time ESM side effect through virtual:kirocrew-edition, and read non-reactively at module load — an app bundle loaded at runtime cannot register into it. The runtime app TabKind (McpAppTabBody) does not cover this either: it is a per-tool-call MCP App render keyed by toolCallId, ephemeral and never persisted.

Please make the seam declarative, in the style of contributes.commands / publishProviders: contributes.panelTabs[] in app.json with id, title, menuLabel, icon (lucide name), and route/entry; core renders the tab as an app iframe (the same host ui.pages already use) and persists {kind: 'app-tab', appName, tabId}. The edition then registers nothing — it just installs the app.

Findings on 76a40b4

  • usePanelTabs.ts:21(string & {}) collapses keyof TabKind to string, so KIND_ICON: Record<TabKind, ReactNode> (SidePanel.tsx:42) degrades to an index signature: a future built-in kind that forgets an icon compiles and renders nothing, and tab.kind === 'termnal'-class typos stop being errors. Use a template-literal member instead — type RegisteredTabKind = `edition:${string}` (or app:${string} per the above) — which keeps every built-in literal required in mapped types while admitting registered keys; type PanelTabDescriptor.kind and openPanelTab() with it and keep PANEL_TAB_KIND_RE only for persisted/untrusted input.
  • usePanelTabs.ts:214let store = loadPersisted() runs the descriptor prune at module evaluation, not "at render" as panelTabRegistry.ts says. ESM evaluates an edition module's imports before its top-level registerPanelTab(), so a tab component that transitively imports usePanelTabs/SidePanel gets its persisted tab pruned on every reload — silent tab loss. Move the prune to the read path (project it in the hook's tabs memo next to localiseTitles); that also handles a contributor removed mid-session.
  • SidePanel.tsx:1006<Body projectDir={projectDir} /> drops the active flag TabBody already receives (:817). A registered tab stays mounted while hidden, so its body cannot stop polling or release Escape/Cmd+S. One-line additive fix now; a migration later.
  • panelTabRegistry.ts:36menuDescription has no read site (the + rows render only icon + label; the empty-state launcher that renders descriptions is untouched). Wire the launcher or drop the field.
  • SidePanel.tsx:725 — the empty-panel launcher iterates built-ins only, so a registered tab is reachable only from the + dropdown while the launcher presents itself as the full set.
  • usePanelTabs.ts:418-420 — the activeId repair now runs for every bucket, not just pruned ones; declare it or scope it, since the PR claims byte-identical behavior with an empty registry.
  • website/docs/extension-seams.md:15-17 — "exercises each one except the source-provider seam" is now false; the PR edits this file three lines below.
  • Tests: panelTabRegistry.test.ts covers register/lookup/collision. Missing: a registered kind takes the TabBody intercept (not ActivityViewer), the + row opens it and dedupes to one instance, and register → persist → reload-without-descriptor prunes correctly.
  • CI: Screenshot Evidence is red — add the <!-- no-visual-delta --> + **Why no screenshot:** markers.

@suhasaitham22

Copy link
Copy Markdown
Contributor Author

Thanks @bolichen97 — adopting the direction. Reshaping from the edition-only registry (a live ComponentType registered by a build-time side effect) to a declarative, app-reachable contributes.panelTabs[], so an installed standalone app can add the tab and the edition registers nothing — it just installs the app.

Plan for this PR

  • Manifest-declared contributes.panelTabs[]id, title, menuLabel, menuDescription?, icon, entry; feed manifest → host registry the way ui.overlays already does (overlaySlots.ts); persist as metadata { kind: 'app-tab', appName, tabId } and re-mount on load.

Two grounding notes vs. the review wording

  1. contributes doesn't exist in the manifest yet (the precedent is the top-level singular publishProvider); introducing it fresh here.
  2. ui.pages does not render via an iframe — AppHost mounts app pages in-process via ESM import() (iframes are explicitly banned there; the only iframe path, McpAppFrame, is for ephemeral MCP render payloads). So "the same host ui.pages use" = the ESM AppHost, which is what the panel-tab body will render through, and the kind namespace becomes app:${string}. Happy to switch to an iframe if you want hard isolation for third-party app tabs, but that diverges from how app pages render today — flagging rather than assuming.

Defects I'll fix

  • move the descriptor prune to the read path (no module-eval tab loss); pass the active flag into the body; template-literal app: kind (drop (string & {}) so KIND_ICON exhaustiveness + typo errors survive); wire or drop menuDescription (launcher parity); scope the activeId repair; the intercept/dedupe/prune tests; and the Screenshot Evidence markers.

Will push the reshaped revision shortly.

@suhasaitham22
suhasaitham22 force-pushed the feat/panel-tab-registry-seam branch from 76a40b4 to 793f716 Compare September 3, 2026 22:33
@suhasaitham22 suhasaitham22 changed the title feat: add side-panel tab contribution seam feat: add contributes.panelTabs app-manifest contribution Sep 3, 2026
@suhasaitham22

Copy link
Copy Markdown
Contributor Author

Pushed the declarative reshape (793f716). Summary of how each point is addressed:

  • App-reachable (direction): now a manifest contributes.panelTabs[] block resolved from /api/apps (mirroring overlaySlots.ts); the edition registers nothing. contributes introduced fresh (precedent = singular publishProvider).
  • Render host: the tab body mounts via the ESM AppHost (gained an optional entry/active override), the same in-process host ui.pages use. Flagging vs. the review wording: ui.pages is not an iframe — AppHost bans iframes; the only iframe path (McpAppFrame) is for ephemeral MCP payloads. If you'd rather have hard third-party isolation via an iframe, say so and I'll switch — but that diverges from how app pages render today.
  • Persisted-tab loss: the orphan-descriptor prune moved off module-eval onto the read path; app tabs now persist as metadata {kind, appName, appTabId}.
  • active flag: passed through to the body so a hidden tab can stop polling / release Esc+Cmd+S.
  • Typing: (string & {}) → template-literal app:${string}, so KIND_ICON exhaustiveness + typo errors survive.
  • menuDescription / launcher: declared tabs now appear in the empty-panel launcher grid, which is menuDescription's read site; activeId repair scoped to pruned buckets.
  • Tests: backend contributes.panelTabs; resolver test; openPanelTab dedupe + prune-at-read + title re-projection.

Three TS interactions I couldn't build locally are called out in the description for CI to confirm. The "autolink rules" seam in extension-seams.md is from the stacked base (#7270), left untouched; my earlier 14→15 count bump is reverted.

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks — the direction is now right. The seam is genuinely declarative and app-reachable (app.json contributes.panelTabsPanelTabConfig → persisted manifest → /api/appsresolvePanelTabsTabBody intercept → AppHost), persistence as {appName, appTabId} checks out (survives serializeBucket's kind !== 'app' filter since 'app:pippin:browser' !== 'app', MCP app kind stays excluded, disabled/uninstalled apps' tabs disappear cleanly and return on re-enable), and the code-level findings from last round are addressed except one. Three things block.

Blocking

1. contributes is not net-new — this PR redefines a class that already exists on main, and a naive conflict resolution silently deletes contributes.commands.
Your claim was true when the commit was authored (2026-09-02 18:36Z) and became false ~3h later when #7423 (147010ce) merged class Contributes with commands: list[CommandContribution] at src/kiro_crew/apps/manifest.py:1556 (+ _KNOWN_FIELDS :1675, AppManifest field :1744, discovery.py:64-68 persisting it, types.ts contributes?: { commands?: unknown }). This PR's class Contributes declares panelTabs as the only field. I reproduced the merge: hard conflicts in manifest.py (4 regions), test/test_app_manifest.py, website/src/components/appstore/types.ts. If the PR side wins any of them, Contributes.to_dict() stops emitting commands, discovery.py writes that into the persisted app.json, and every builtin's Command Bar rows vanish with no error.
Fix: rebase onto main and add panelTabs as a second field on the existing Contributes, keeping commands in to_dict/from_dict/validate. The exposure path you'd otherwise need to build already exists, so this is mostly deletion.

2. contributes.panelTabs[].icon is not app-reachable — an installed app can never supply one.
SidePanel.tsx iconForKind resolves the manifest icon through getBuiltinIcon(d.icon), a hardcoded 15-entry allowlist (website/src/apps/builtinIcons.tsx:42) populated only by registerBuiltinIcons(), which its own header documents as "expected at module-load time (edition composition) … not reactive". A runtime-installed app cannot register into it — the same edition-only dependency the last review rejected, relocated to the icon dimension. Your own documented example and test fixture use icon: "BookMarked", which is not in the registry, so the canonical example silently renders the generic <PanelRight> fallback.
Fix: resolve the name against lucide-react's icon map directly (with an allowlist/fallback), or restrict the set and correct the doc/fixture.

3. mergeable_state: dirty, and no CI lane has run.
Head 793f716: combined status pending, total_count: 0; 4 queued + 1 skipped fork-gate checks only. None of build / tsc / vitest / pytest / eslint ceiling / coverage, none of the 5 review bots. The three TypeScript interactions you flagged for CI to confirm (Record<BuiltinTabKind, ReactNode> with the app:${string} arm, AppHost's inline prop type, the suite-wide usePanelTabDescriptors mock) are therefore unverified by anyone. Rebase, push, get a green run.

Should fix

4. Contributes.validate() omits the malformed-input handling its sibling field requires, and a test codifies the omission. Non-object contributes, non-list panelTabs, non-dict entries all degrade silently to [] in from_dict. Main's Contributes carries bad_block / bad_commands / dropped_commands for exactly this (rationale at manifest.py:1567-1580). test_null_paneltabs_degrades_to_empty asserts the silent path as correct — mirror the three flags and invert that test.

5. types.ts types third-party manifest data as concrete and required. The new block declares id/title/menuLabel/entry: string required. Main types the sibling as commands?: unknown with the reason inline ("the only code allowed to decide what a contribution IS is the module that checks it"), and this PR's own AppPanelTabDecl (panelTabRegistry.ts:20-27) makes every field optional for that reason. Use panelTabs?: unknown here; keep the shape in panelTabRegistry.ts.

6. activeId repair is still unscoped — prior finding #7, moved not fixed. usePanelTabs.ts effectiveActiveId runs unconditionally and is now what the hook returns. On main a stored activeId naming a nonexistent tab yields activeTab === null; with this change it silently focuses the last tab, reachable from a drifted localStorage bucket. Gate the fallback on the prune having removed something (tabs.length !== localised.length).

7. Backend cap not mirrored on the read path. MAX_PANEL_TABS = 8 is enforced in validate(), but resolvePanelTabs iterates unbounded; #7423's own comment calls a one-sided cap "silent truncation". Slice to the cap in resolvePanelTabs.

8. Two component-level tests still missing. panelTabRegistry.test.ts (8 cases) and usePanelTabs.test.ts cover the resolver and hook well. Not covered: that an app: kind takes the TabBody intercept rather than falling through to ActivityViewer's closed ViewKind multiplexer, and that the + row / launcher button actually opens it.

On iframe vs ESM: keep ESM.

Your correction is right and my earlier wording was wrong — ui.pages does not use an iframe. AppHost.tsx on main is a bare lazy(() => import(bundlePath)) in the same realm, same origin, same document ("no iframes, no Web Components, no Shadow DOM"); it provides an error boundary, Suspense, dev reload, and AppApiProvider context — no sandbox, CSP, module allowlist, or import map. So on capability the side panel is not materially different from an app's own page; exposure is already total in both, and an iframe here alone would be isolation theater. Two properties are genuinely new and worth one line in the PR body rather than a mechanism change: (a) a panel tab stays mounted while hidden and is restored on reload — durable background execution a page does not get; (b) it is co-mounted with the chat composer. Both point at a separate cross-cutting decision about app isolation (real sandboxing for ui.pages and panel tabs alike, or an explicit "installed apps are trusted code" tenet). Land ESM; file that question separately.

Nits

9. MAX_PANEL_TABS is public; every sibling bound is module-private (_MAX_COMMANDS_PER_APP, …). Rename unless the frontend imports it.
10. iconForKind needs KIND_ICON[kind as BuiltinTabKind] because isPanelTabKind returns boolean; make it a type predicate and the cast disappears.
11. PR body describes deltas against the previous revision, not main ("panelTabRegistry.ts rewritten from in-code registerPanelTab" — the file is added; "removed the extensions.ts register line", "extension-seams.md row dropped" — neither file is in the diff, main has no such row).

Prior comments

Direction, (string & {}) → template literal, module-eval prune → read path, active flag plumbed to AppInfo, menuDescription read site, launcher iterating descriptors, Screenshot Evidence markers — addressed. activeId repair — not addressed (#6). extension-seams.md — moot. Tests — partial (#8).

Not verified by me

Nothing compiled or run; all type-level claims are read-only inference. The four exhaustive ViewKind Records are untouched and ViewKind unchanged, so exhaustive by inspection only. No app on main declares contributes.panelTabs, so runtime mount/unmount/hidden behaviour is untested by anyone.

@github-actions github-actions Bot added readiness: checking Automated validation is still running merge conflict Branch has merge conflicts with its base — author must resolve before merge and removed readiness: passed Eligible automated validation passed for the current revision labels Sep 3, 2026
@suhasaitham22
suhasaitham22 force-pushed the feat/panel-tab-registry-seam branch from 793f716 to d9793bc Compare September 4, 2026 00:28
@suhasaitham22

Copy link
Copy Markdown
Contributor Author

Rebased onto main and pushed (d9793bca). Point by point:

Blocking

  1. contributes is not net-new — correct, and thanks for catching it; feat(command-bar): let an installed app contribute command rows #7423 landed ~3h after this commit was authored. Rebased; panelTabs is now a second field on main's existing Contributes, with the duplicate class, _KNOWN_FIELDS entry and AppManifest field deleted. Five conflict regions in manifest.py were each resolved by keeping main's side verbatim (including bad_block, which this branch didn't have) and appending ours. test_commands_survive_alongside_paneltabs asserts both parse, validate clean and survive a to_dict round trip.
  2. Icon was not app-reachable — right, and it was the same edition-only dependency relocated to the icon dimension. New apps/appIcons.tsx with an app-facing name→glyph set; getBuiltinIcon is no longer used for app icons at any of the three SidePanel sites. I took the bounded allowlist branch rather than lucide's full icons export, which would pull ~1k components to resolve one manifest string. Names are sourced from icons already imported in-repo so each is verifiable against the pinned lucide-react without node_modules — which is why BookMarked is out: it appears nowhere in-repo, so I couldn't confirm it exists in this version. The canonical example, doc table and all fixtures are now BookOpen, and the allowed set is documented. If you know BookMarked is valid in the pinned version, it's a one-line addition.
  3. mergeable_state / no CI — rebase clears dirty; this push is the first run of any lane. tsc/vitest still can't run here (npm ci --offlineENOTCACHED, and node 20 vs the required 22), so your three flagged TS interactions are for CI to confirm. Verified by inspection meanwhile: TabKind does include `app:${string}`, so Exclude leaves 'app' in BuiltinTabKind and the predicate narrows the else branch to exactly it; InstalledApp is structurally assignable to AppHost's inline app prop; the suite-wide mock spreads the real module so new exports survive.

Should fix
4. Silent coercionbad_panel_tabs / dropped_panel_tabs mirroring the commands flags, reported from validate(). test_null_paneltabs_degrades_to_empty is inverted to test_non_list_paneltabs_parses_empty_but_is_reported, plus non-object-entry and non-object-block cases.
5. types.ts — now panelTabs?: unknown alongside commands?: unknown; the shape stays in panelTabRegistry.ts.
6. activeId repair still unscoped — fixed properly this time: the memo returns { tabs, pruned } and the fallback applies only when pruned. A drifted activeId naming no tab again yields no active tab, as on main. Two tests pin both directions.
7. Read-path capresolvePanelTabs slices per app to MAX_PANEL_TABS_PER_APP and warns; two tests (the cap, and that it's per-app).
8. Component tests — new sidePanelAppTab.test.tsx (5 cases): the app: kind mounts through AppHost with its entry and active=true while ActivityViewer never renders; the + row opens it; the launcher opens it and renders menuDescription; it dedupes to one instance.

Nits — 9: _MAX_PANEL_TABS_PER_APP is module-private on the Python side (the frontend constant stays exported because the resolver and its tests import it). 10: isPanelTabKind is now a type predicate (kind is \app:${string}`) and the as BuiltinTabKindcast is deleted. 11: body rewritten against **main** —panelTabRegistry.tsdescribed as added, and no claims aboutextensions.ts/extension-seams.md` (confirmed neither is in the diff vs main).

On ESM — agreed, keeping ESM, and thanks for the detailed read. The two genuinely-new properties are now named in the body: a panel tab stays mounted while hidden and is restored on reload (durable background execution a page doesn't get), and it's co-mounted with the chat composer. I'm treating real app isolation as a separate cross-cutting decision to file rather than a mechanism change here.

@dwu96

dwu96 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated.

Missing sections:

  • ## Why it matters

Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle.

1 similar comment
@iamwhatever

Copy link
Copy Markdown
Collaborator

👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated.

Missing sections:

  • ## Why it matters

Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle.

@suhasaitham22

Copy link
Copy Markdown
Contributor Author

Description updated for the template gate — added a Why it matters section, and renamed Tests / verification to Tests so the required heading matches exactly. All four enforced sections are now present (Problem / Motivation, Why it matters, What changed, Tests), so the workflow runs should auto-approve on the next cycle.

That description gate is also the answer to the "no CI lane has run" finding — the real lanes (build / tsc / vitest / pytest / review bots) were never approved to start, rather than having run and been skipped. Once they execute I'll drive whatever they report to green.

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: checking Automated validation is still running labels Sep 4, 2026
@suhasaitham22
suhasaitham22 force-pushed the feat/panel-tab-registry-seam branch from d9793bc to f0c007a Compare September 4, 2026 01:02
@github-actions github-actions Bot added the readiness: action required A blocking check or review needs attention label Sep 6, 2026
@bolichen97
bolichen97 force-pushed the feat/panel-tab-registry-seam branch from f99cc8b to fd07f7a Compare September 6, 2026 18:35
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 6, 2026
bolichen97
bolichen97 previously approved these changes Sep 6, 2026

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving. The seam is the right shape and every finding from the earlier rounds is addressed in code.

On your second flag — ESM AppHost vs iframe: keep the ESM host. This is a deliberate decision, not an omission. ui.pages already render through the same in-process host, so an app can already run a full page in the dashboard's origin; iframe-ing only the panel tab would take no capability away from a malicious app while making every future app pay for a versioned postMessage contract plus manual theme, height and focus plumbing, and would leave the platform forked (pages in-process, tabs isolated). If we want isolation, the place to add it is AppHost itself as a sandboxed mode covering both surfaces, driven by whether registry apps are ever installable without review — not one surface in one PR.

On your first flag — contributes being net-new: mirroring the top-level publishProvider pattern, including the /api/apps/<app>/ endpoint allowlist, was the right call.

Changes folded in since your last revision, all with regression tests and verified negative controls:

  • Endpoint allowlist hardened. Being inside /api/apps/<self>/ was not sufficient — routes.py mounts core-owned lifecycle handlers in that same namespace, so a manifest could aim a row at its own uninstall and a reader's click would run it with their session. Reserved segments are now derived from the actual registrations (which also turned up manifest, config, migrate-cleanup, token and _jobs), with a drift test that fails when a new core route appears unlisted. Three parser-level bypasses closed along the way — a query string, a stripped control character, and \ as a path separator — which is why the validator now takes a character allowlist rather than a blocklist.
  • Cross-slot hosting for contributed tabs. They render from the cross-slot allAppTabs list alongside MCP frames, and openPanelTab stamps the owning slot, so the React key is stable and a chat switch no longer remounts AppHost and discards in-body state.
  • setOrder preserves hidden orphans. It was writing the visible list over the stored bucket, so one drag while an app was disabled deleted its tab — defeating the "hidden, not deleted" contract the code documents.
  • Contributed rows are attributed to their app, following contributedCommands.ts's appLabel precedent (degrade to name, then clipped, never empty), so a row labelled Download cannot pass as the core action.
  • appIcon uses an own-property checkAPP_ICONS['toString'] was a truthy function and React throws on it, and icon is unvalidated manifest data.
  • isPanelTabKind guards typeof, since the persisted bucket is parsed from localStorage and cast; a non-string kind threw upstream of the whole chat render.
  • entry is ui/-relative, not app-root-relative. The code was right and the prose wrong; corrected here and in the two sibling rows that said the same thing, so the reference table no longer contradicts itself.

@bolichen97
bolichen97 enabled auto-merge (squash) September 6, 2026 19:50
@bolichen97
bolichen97 force-pushed the feat/panel-tab-registry-seam branch from fd07f7a to da811fc Compare September 6, 2026 20:18
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision merge conflict Branch has merge conflicts with its base — author must resolve before merge and removed readiness: passed Eligible automated validation passed for the current revision readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 6, 2026
@bolichen97
bolichen97 force-pushed the feat/panel-tab-registry-seam branch from da811fc to fb66095 Compare September 7, 2026 02:40
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running and removed readiness: passed Eligible automated validation passed for the current revision readiness: action required A blocking check or review needs attention merge conflict Branch has merge conflicts with its base — author must resolve before merge labels Sep 7, 2026
Add panelTabs as a second field on the existing Contributes block,
alongside commands, so an installed app can own a chat side-panel tab.
Core resolves the declarations off the shared /api/apps query, mounts
the body through the ESM host ui.pages already use, and persists the tab
as {appName, appTabId} metadata that re-mounts on load. Caps and
malformed-input reporting mirror the commands precedent; app icons
resolve through an app-facing icon set rather than the edition-only
builtin registry an installed app cannot reach.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fork Pull request from a fork (external contributor)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants