Skip to content

feat(apps): app session controls — a composer seam for per-chat app state - #7573

Merged
bolichen97 merged 2 commits into
kirodotdev:mainfrom
omerrubi-amzn:feat/app-session-controls
Sep 6, 2026
Merged

feat(apps): app session controls — a composer seam for per-chat app state#7573
bolichen97 merged 2 commits into
kirodotdev:mainfrom
omerrubi-amzn:feat/app-session-controls

Conversation

@omerrubi-amzn

@omerrubi-amzn omerrubi-amzn commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Problem / Motivation

An app can hold state that is meaningful per conversation and has nowhere to put
the control for it, because nothing tells the app which chat the user is looking
at.

Verified on 1d705a03f:

  • AppInfo is { name, version, permissions } and AppApi is five HTTP verbs.
  • Every session-bearing path in the app SDK takes a slot key the app itself
    supplies
    ChatEmbed / ChatPanel as a prop, and useChatSession derived
    from a caller's workspacePath
    (slotName = appName + '-' + hashStr(workspacePath), useChatSession.ts:67).
    Each names a session; none observes the active one.
  • UIConfig declares entry, pages, overlays, sidebar — all session-blind.
    UIOverlay is documented as "not routed: it floats above whatever the user is
    looking at".

So today an app author has two bad options: bind at a coarser grain it can name
(a workspace or folder — which cannot distinguish two chats sharing one working
directory), or put the control on a routed page and make the user navigate away
from the conversation to configure that conversation, re-stating which chat they
mean to a UI that already knows.

Why it matters

Any app whose value is per-conversation is currently either wrong or awkward. A
folder-grained binding silently applies to chats the user never meant to include,
and a routed page turns a one-click setting into a context switch out of the
conversation and back.

It also blocks a class of app rather than one app: the composer already hosts
per-chat state (agent, model, project chips), so app-contributed per-chat state is
the one obvious surface that does not exist. Left undone, every such app
reimplements a worse version of it on a page.

What changed (motivation → approach → change)

Goal — let an app bind state to the chat the user is in, and set it without
leaving that chat.

Approach, and the alternatives rejected (full reasoning in the RFC included in
this PR, docs/request-for-change/rfc-app-session-controls.md):

  • Reuse ui.overlays — rejected structurally. Its replaces field is required
    and names a host slot the app takes over, so the model is one-app-per-slot;
    session controls must be additive and compose. An overlay is also session-blind,
    so it would not solve the problem even if the contribution model fitted.
  • Reuse ui.pages and pass the session in the route — rejected: it makes the
    user leave the conversation to configure it, and puts a session key in a URL.
  • Give every app the active session through the app SDK — rejected as too broad:
    every app page would receive ambient session identity whether or not it has
    per-session behaviour. A declared seam is narrower — an app that wants session
    identity says so in its manifest and gets it only inside the control it
    declared.

What was builtui.sessionControls[] on the manifest, and the composer host
that reads it. Both land together on purpose: shipping the schema alone would add
another declared-but-unread manifest field of the kind
rfc-navigation-placement-seam.md and rfc-everything-is-an-app.md both
document.

  • Schema is bounded and fail-soft: at most 2 controls per app, kebab-case id,
    charset- and length-bounded statusPath, and malformed entries reported by
    validate() rather than raised — one bad control must not abort a manifest
    parse, matching how an unroutable page route is already handled.
  • The chip loads the app's ESM module through the existing import map (single-React
    guarantee holds, no second bundler) and mounts it inside AppApiProvider, so it
    inherits the app's already-declared permissions.api / permissions.events and
    gains no privilege of its own.
  • The control receives SessionControlContext
    (sessionKey, folderId?, folderName?, cwd)
    plus onClose. It is keyed on the session, so switching chats remounts it and
    per-session state cannot leak across the switch; its error boundary renders
    inline and unmounts nothing but itself, because a control sits on the path of
    every turn.
  • The host renders at most MAX_INLINE_SESSION_CONTROLS = 2 chips across all apps
    and drops the rest rather than overflowing — the bar shares one row with the
    message input. That is a deliberate trade, and the RFC records the overflow menu
    as its open question.
  • Optional statusPath lets a chip report ok / warn / none before it is
    opened, so a configured control does not look unset until first click. It is
    validated at three layers and a path that would leave the app's own prefix, go
    protocol-relative, reach another origin, or corrupt the query string is refused
    before any fetch, not sanitized into one (. is excluded from the charset,
    so .. is unrepresentable rather than filtered). Polling fails closed: a failing
    app is not retried at the composer's expense.

Backward compatible in both directions: from_dict defaults the field to [],
to_dict omits it when empty (so no existing manifest changes on disk), and
because sessionControls nests inside ui, a build predating this change parses
such a manifest without error and renders no chip. An app declaring no controls
produces no chip, no request and no DOM change.

Docs: docs/app-kit/manifest-reference.md gains the field-table rows and a
ui.sessionControls section covering the props contract, both caps and the status
route, so a control can be built from the reference rather than from the source.

Tests

Backend — test/test_app_session_controls.py, 37 tests:

  • Parsing: a control round-trips through to_dict/from_dict; an absent key
    yields an empty list; non-dict entries are skipped rather than fatal.
  • Validation: the per-app cap is enforced (and exactly-at-cap is allowed); a
    non-kebab-case id is rejected; a statusPath failing the charset/length bound
    is rejected; ui.pages validation still passes independently.
  • Serialization: statusPath is absent by default and omitted from output.

Frontend — 8 suites: 96 tests across the seven session-control suites, plus one added HAND_TESTED entry in the existing ApiClient.coverage.test.tsx:

  • useSessionControls.test.ts — resolver: composite <appName>:<id> keying,
    duplicate-key dropping, stable key sort, permission lists forwarded, junk
    entries survived, statusPath normalization (leading slash stripped;
    protocol-relative, traversal, cross-origin and query-corrupting paths refused),
    status normalization (ok/warn/none, unknown → none, tooltip bounded).
  • useSessionControls.query.test.tsx — per-control query keying so two controls
    sharing a statusPath are not deduped into one.
  • SessionControlHost.test.tsx — dialog named for both control and app; error
    boundary stays local; remount on session change.
  • appSessionStatus.test.ts — the status URL is app-scoped and refuses to escape.
  • ChatInput.sessionControls.test.tsx / ChatPage.sessionControls.test.tsx
    chip rendering, click routing by key, and no request before a session exists.

Gate results: mypy clean across 1237 files; isort and flake8 clean; the new
test file is black --target-version py310 clean; docs-lint, the de-Amazon
scrub lint, the brand-name gate and the focus-cue gate all pass on this change's
added lines. The full vitest suite passes (1699 files).

Two failures on main are unaffected by this change and reproduce identically at
the base commit in a clean main worktree: tsc -b reports 4 errors in
DiscoverPage.tsx / McpManagement.tsx (files this PR does not touch), and
test:electron reports 1362 pass / 17 fail / 26 cancelled (updater / port /
overlay suites; this PR touches no electron code).

Manual verification

Backend and resolver behaviour is unit-covered, including every refusal path for
statusPath. What unit tests do not cover, and what is therefore still
outstanding: rendering the chip in a real browser against a gateway with an
installed app that declares a control, and confirming the tint reflects a live
statusPath response. That path was exercised over the API (the manifest survives
/api/apps, the control module serves 200, the status route answers) but not
visually.

Screenshots / video

Captured against a gateway with an example app (demo-env) declaring one control —
{ id: "env-picker", label: "Environment", statusPath: "session-status" }.

The chip in the composer, beside the existing agent/workspace chips:

Session control chip in the composer bar

Opened, showing that the control is handed the active session (demo) and that
the chip marks itself active while its surface is open:

Session control open, showing the active session key

Narrow viewport

Composer at a narrow viewport

Media is committed under temp-screenshots/app-session-controls/ and embedded with
commit-SHA-pinned raw URLs, per the template.

Not shown: the ok/warn status tint. The example app declares a statusPath but
its backend route did not register in this throwaway install, so the chip renders in
its untinted default (none) state — which is the correct rendering for a control
whose status route does not answer. The tint itself is unit-covered
(normalizeStatus, and the resolver's refusal paths).

Related Issues

None — no prior issue exists for this. CONTRIBUTING.md asks for an issue first
on anything significant, and the RFC directory asks for an RFC before the code;
this PR carries the RFC but was not preceded by an issue. Happy to open one for
the design discussion if the maintainers prefer the RFC argued there first.

Checklist

  • At most two commits (one is the norm), with a Conventional Commits title (feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)
  • Existing tests pass and new tests added for new functionality — new tests as above; the two main failures noted under Tests are pre-existing and reproduce at the base commit
  • Self-review completed; code follows project style guidelines
  • Documentation updated (if applicable) — docs/app-kit/manifest-reference.md, plus the RFC and its index row
  • No secrets, credentials, or internal references in the diff — the de-Amazon scrub lint reports no internal markers and no credential leaks on this change

Addressed in this revision (76cad29ec)

Blocking review findings, and the doc drift the advisory reviews flagged:

  • errors-use-error-notice (blocking, Opus 4.8 + GPT 5.6 F2). Both
    SessionControlHost failure surfaces — the control crash boundary and the
    bundle-load fallback — now render through ErrorNotice with askAgent on,
    instead of hand-written text-danger / text-muted divs. The crash fallback
    keeps its Retry button alongside the hand-off.

  • Control drafts survive a global chord (blocking, GPT 5.6 F4). The chat-jump
    shortcut deliberately fires on digits even inside text fields, and a slot
    switch unmounts the host — so Ctrl/Alt+digit while typing in an open
    control discarded the draft. The dialog now isolates every key but Escape,
    which still reaches the document listener that closes it. Two tests pin this at
    the window boundary, which is where the shortcut actually listens.

  • Malformed session controls are refused, not dropped
    (src/kiro_crew/apps/manifest.py).
    A non-dict entry in ui.sessionControls
    was discarded during parsing, so validate() could not report it and the app
    installed clean with the control silently absent — contradicting the documented
    promise that a malformed control is refused at install time. It is now retained
    as an empty placeholder, which fails the required-field checks.

  • One spelling of the statusPath allowlist. The regex was written twice
    (api/client.ts and hooks/useSessionControls.ts). It now lives in
    lib/sessionControlStatusPath.ts, which both import — not in either caller,
    because the hook imports the client (a cycle) and ~490 test files replace the
    client with a vi.mock factory (which would make the constant undefined).

  • Bundle Size Gate — resolved by upstream, not by this branch. The gate failed
    with the all catalog chunk 2,788 B over its 10,490 KB ceiling. Attribution was
    measured: the chunk built at 10,744,548 B, and compact re-serialization put this
    branch's contribution at 8,406 B (eleven translated strings across 13 catalogs),
    leaving main alone at ~10,484.5 KB — under the ceiling. So the breach was ours,
    and this branch briefly raised the ceiling to 10,560 KB. Upstream then raised the
    same entry to 10,975 KB independently, measuring main at 10,450 KB with only 0.4%
    headroom and noting that "any feature PR shipping a normal set of keys across the
    13 catalogs fails the gate on its merge ref". The rebase therefore takes
    upstream's ceiling and drops this branch's edit
    check-bundle-size.mjs is no
    longer in this diff, and the chunk sits well inside the new budget.

  • Session controls carry the session identity to the SERVER (blocking, GPT 5.6,
    security-fenced).
    A control received sessionKey as a prop, but the scoped API
    the SDK hands it fetched with no X-Session-Key header. The backend's
    restricted-session guard reads that header and fails openif not sk: return False in _is_restricted_session — so a control mounted in an incognito or guest
    chat was permitted exactly the persistent memory writes that mode exists to deny.
    AppApiProvider now takes an optional sessionKey and createScopedApi attaches
    it to every scoped request (one jsonFetch funnels all verbs). Optional because a
    full-page app surface is not session-scoped and must send nothing rather than an
    invented key. Four tests pin it: present on GET and POST, absent when the host has
    no session, never clobbering a caller-set header, and not dropping the JSON
    content type. All 47 SDK-touching suites re-run green, since this is shared code.

  • Stale chips after enabling or disabling an app. useSessionControls held a
    private ['session-controls'] query key, which none of the four ['apps']
    invalidation sites reached. It now shares ['apps'] with the same queryFn as
    the app list and resolves through select, which is what the shared-key
    convention in useAppsData requires (one queryFn per key; the last observer to
    register fetches).

  • A folder-query failure no longer claims the controls are unavailable. The
    ['chat-folders'] error is now surfaced only when a control actually exists —
    with no chips on screen it is not a session-control problem, and labelling it one
    would put an unexplained notice on every composer.

  • A crashed control no longer poisons its siblings (blocking, GPT 5.6,
    security-fenced).
    ChatPage rendered SessionControlHost without a key. The
    host's error boundary holds state.error and nothing clears it on a prop change,
    so React reused the one instance across controls: opening control B after control
    A crashed showed B the stale error and never mounted B. Now keyed key={sc.key},
    which makes switching controls a remount — the only thing that resets the
    boundary — with a comment saying so, since the key is load-bearing and looks
    incidental. A test pins the contract the key depends on (a remount clears the
    boundary), so it fails if that error state ever moves somewhere a remount does not
    reset.

  • RFC line citations rewritten as symbol names. Rebasing picked up docs: refresh prompts, skills and docs against the shipped code #8905, which
    added a docs-lint rule against file:line citations in prose ("a name survives
    the refactor that moves the line") and baselined the 743 pre-existing violations.
    This RFC is new, so its 13 citations were not baselined — and they proved the
    rule's point: several had already drifted (manifest.py:476 when UIConfig sits
    at 597, :422 when UIOverlay sits at 451, SessionControlHost.tsx:33 when
    SessionControlContext sits at 37). All 13 now name the symbol instead. The
    baseline file was deliberately NOT touched: it records violations that predate
    the rule, not new ones.

  • Session controls now receive the session identity, not the slot id. ChatPage
    passed activeSlot (chat-2) as sessionKey, where the rest of the app stores
    session-scoped state under dashboard:<slot> — the derivation MobileConnectModal,
    ChatInput's skill slot and workflows/runModel all use. An app would have keyed
    its per-session state on a string nothing else uses, which is the exact
    mis-binding this feature exists to remove. Both the status poll and the control
    props now use the derived key. Raised by GPT 5.6 and downgraded to advisory on
    adjudication
    ; fixed anyway because it is a real defect in the feature's central
    contract, and cheap. Note the suggested currentSlot.linked_session_key was NOT
    used: that field exists only in the backend and is on no frontend type.

  • Escape during IME composition no longer discards a control draft (blocking,
    GPT 5.6, upheld-fenced).
    The host's document Escape handler had no
    composition guard, so an IME user cancelling a candidate closed the popover and
    lost an unsaved draft. It now claims through useDocumentImeLatch, the existing
    hook for exactly this, and closes only when the latch accepts the key. Two tests
    pin both halves: latched Escape does not close, and Escape closes again once the
    composition ends.

  • Folder-query failures surface (blocking, GPT 5.6, upheld). chatFolders
    discarded its query error. On /embed/chat no sidebar is mounted to consume the
    shared ['chat-folders'] cache, so nothing surfaced it anywhere; it now joins
    the session-control ErrorNotice rather than getting a banner of its own.

  • Feature map row added (blocking, GPT 5.6, upheld). docs/feature-map/README.md
    gains an App session controls row under Apps, naming the composer entry
    point, the frontend and backend owners, and both status-route prefixes. The
    mechanical gate does not fire here (no page, handler or route was added), but the
    map's own maintenance contract covers a PR that ADDS a feature, and this one does.

  • Session-control query errors reach the user (blocking, GPT 5.6, upheld).
    useSessionControls and useSessionControlStatuses coerced their query errors
    to [] / {}, so a failed /api/apps rendered no chips and a failed status
    probe rendered a stateless one — both indistinguishable from "no app declares a
    control". Each hook now returns its error alongside its data (the data still
    fails closed), and ChatPage renders one ErrorNotice with askAgent above
    the composer, beside the chips it concerns. Four tests pin it, and the new
    components.sessionControlHost.controls_unavailable string is translated in all
    13 locales with the pseudo-locale regenerated.

  • Cap drift corrected to 2. The code ships MAX_INLINE_SESSION_CONTROLS = 2;
    this description and RFC §5/§7 said three. Both are fixed, along with the
    mangled audit-log sentence in docs/request-for-change/README.md.

Two GPT 5.6 findings are deliberately not actioned, per that review's own
adjudication, which downgraded both as disproportionate-remedy: F1 (a
feature-map row — the rule fires on added pages/handlers/routes, and explicitly
rejects rows for non-structural churn) and F3 (an ErrorNotice on a discarded
folder-label fetch — the established convention, self-correcting over WebSocket
reseeding, and the rule excludes empty states).

@omerrubi-amzn
omerrubi-amzn requested a review from a team September 1, 2026 08:42
@omerrubi-amzn
omerrubi-amzn requested a review from a team as a code owner September 1, 2026 08:42
@github-actions github-actions Bot added fork Pull request from a fork (external contributor) readiness: action required A blocking check or review needs attention labels Sep 1, 2026
@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:

  • ## Problem / Motivation
  • ## Why it matters
  • ## What changed
  • ## Tests

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
@dwu96

dwu96 commented Sep 1, 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:

  • ## Problem / Motivation
  • ## Why it matters
  • ## What changed
  • ## Tests

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.

@omerrubi-amzn
omerrubi-amzn force-pushed the feat/app-session-controls branch from 982e60d to c8dfbbe Compare September 1, 2026 09:53
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 1, 2026
@omerrubi-amzn
omerrubi-amzn force-pushed the feat/app-session-controls branch from c8dfbbe to 2bbc6df Compare September 1, 2026 10:11
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 1, 2026
@omerrubi-amzn
omerrubi-amzn force-pushed the feat/app-session-controls branch from 2bbc6df to a2e23d8 Compare September 1, 2026 11:19
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 1, 2026
@omerrubi-amzn
omerrubi-amzn force-pushed the feat/app-session-controls branch from a2e23d8 to ef89bbf Compare September 1, 2026 12:04
@omerrubi-amzn

Copy link
Copy Markdown
Contributor Author

I've fixed the PR according to the template

@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 1, 2026
@omerrubi-amzn
omerrubi-amzn force-pushed the feat/app-session-controls branch from faae274 to 9082bb4 Compare September 1, 2026 12:16
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 1, 2026
@omerrubi-amzn
omerrubi-amzn force-pushed the feat/app-session-controls branch from 9082bb4 to a533355 Compare September 1, 2026 13:34
@omerrubi-amzn
omerrubi-amzn force-pushed the feat/app-session-controls branch 2 times, most recently from bbfca6f to 160bd0a Compare September 2, 2026 18:15
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention merge conflict Branch has merge conflicts with its base — author must resolve before merge and removed readiness: checking Automated validation is still running labels Sep 2, 2026
@omerrubi-amzn
omerrubi-amzn force-pushed the feat/app-session-controls branch from 160bd0a to 3b1797f Compare September 3, 2026 07:09
@github-actions github-actions Bot added merge conflict Branch has merge conflicts with its base — author must resolve before merge and removed merge conflict Branch has merge conflicts with its base — author must resolve before merge labels Sep 3, 2026
@omerrubi-amzn
omerrubi-amzn force-pushed the feat/app-session-controls branch from 3b1797f to 01b80eb Compare September 3, 2026 07:43
@github-actions github-actions Bot removed the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Sep 3, 2026
@omerrubi-amzn
omerrubi-amzn force-pushed the feat/app-session-controls branch from 01b80eb to d682e5f Compare September 3, 2026 11:35
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention merge conflict Branch has merge conflicts with its base — author must resolve before merge and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 3, 2026
@omerrubi-amzn
omerrubi-amzn force-pushed the feat/app-session-controls branch from d682e5f to 75fe358 Compare September 3, 2026 19:21
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 3, 2026
@bolichen97

Copy link
Copy Markdown
Collaborator

Open PR relationship audit

This is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion.

Relationship findings

  • This PR is OVERLAPPING with PR #3139. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #7573: MERGE_DISCUSSION. Independent features that can both ship; the second to land must rebase the shared shelf-gate line and re-check the button-cap rationale. Files: website/src/components/ChatInput.tsx.
  • This PR is OVERLAPPING with PR #7423. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #7573: MERGE_DISCUSSION. Not a duplicate — nothing merged carries the active session to an app, so the PR's core capability is genuinely absent from main. But the manifest placement contradicts a rule that is already on this PR's own base, and the RFC itself argues (§9.4) that a manifest field cannot be withdrawn once apps write it. Settle ui.sessionControls vs contributes.sessionControls before the schema is frozen. Files: src/kiro_crew/apps/manifest.py, docs/app-kit/manifest-reference.md.
  • This PR is OVERLAPPING with PR #7955. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #7573: MERGE_DISCUSSION. Same namespace question as PR #7975; the three PRs should be reconciled against the contributes block that PR #7423 already merged. Files: src/kiro_crew/apps/manifest.py. The two independent directions used different labels; the matrix conservatively retains OVERLAPPING for coordination.
  • This PR is OVERLAPPING with PR #7975. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #7573: MERGE_DISCUSSION. Complementary surfaces, but two concurrently open PRs plus one merged PR are all defining where an app declares a contribution to a host-owned surface, and only 7573 answers ui. Decide the namespace once across the three rather than per PR. Files: src/kiro_crew/apps/manifest.py. The two independent directions used different labels; the matrix conservatively retains OVERLAPPING for coordination.

No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit.

An app can hold state that is meaningful per conversation and has nowhere to
put the control for it, because nothing tells it which chat the user is
looking at. Verified at 373b220: AppInfo is {name, version, permissions},
and every session-bearing path in the app SDK takes a slot key the app itself
supplies -- ChatEmbed and ChatPanel as a prop, useChatSession derived from a
workspacePath (useChatSession.ts:67). Each names a session; none observes the
active one. UIConfig's four UI fields are all session-blind.

Adds the manifest reference for ui.sessionControls -- the field table, the
props contract a control receives, both caps, and the statusPath route -- so
an app author can build a control from the docs rather than from the source.

Adds the RFC recording why the existing surfaces do not fit: ui.pages is
routed, so configuring a chat means leaving it; ui.overlays requires a
`replaces` slot the app takes over, which cannot express two apps each
contributing a chip. Its migration plan refuses to separate the schema from
the reader, because shipping the field alone would add another
declared-but-unread manifest field of the kind rfc-navigation-placement-seam
and rfc-everything-is-an-app both document.
@bolichen97

Copy link
Copy Markdown
Collaborator

Thanks for the thorough RFC and the review iterations — the seam itself (manifest field + composer host) belongs in core, and I don't want to relitigate that. Two things need to land before this merges, though, both about the manifest being a one-way door:

1. A real consumer in KiroCrewApps first.
This PR ships a third-party contract with zero consumers: the demo-env app in the screenshots is not in the diff, and (as the First Principles lane already noted) folderName has no named consumer anywhere. The built-in app set is closed by convention, so the place for that consumer is KiroCrewApps. Please open a PR there with a real app that declares ui.sessionControls + a statusPath route — demo-env is fine as the starting point — and cross-link the two. That app is also what lets the UX lane's evidence gaps (ok/warn tint, loading/error states, narrow viewport) be closed against a real bundle rather than a throwaway install. We freeze the schema against a consumer, not against a spec.

2. Namespace: contributes.sessionControls, not ui.sessionControls.
#7423 already merged the contributes block as the place an app declares a contribution to a host-owned surface, and #7955 / #7975 are defining fields in the same spot. This PR is the only one of the three answering ui. RFC §9.4 makes the argument itself: a manifest field cannot be withdrawn once apps write it, so the namespace has to be decided once, before the first consumer ships, not per PR. Please move it under contributes (and update the RFC / manifest-reference rows to match).

Once both are in, the remaining items are a rebase for the current conflict and the advisory nits already on the thread. Happy to review the KiroCrewApps side when it's up.

Adds ui.sessionControls to the manifest and the composer host that reads it.
An app declares a compact control; the dashboard renders it as a chip beside
the agent, model and project chips and hands it the active session's identity
-- the reason the slot exists, since an app cannot otherwise discover the chat
it is rendered beside.

Schema is bounded and fail-soft: at most two controls per app, kebab-case id,
charset- and length-bounded statusPath, and malformed entries reported by
validate() rather than raised, matching how an unroutable page route is
already handled. One bad control must not abort a manifest parse.

The chip loads the app's ESM module through the existing import map, so the
single-React guarantee holds, and mounts it inside AppApiProvider so the
control inherits the app's declared api/events allowlist and gains no
privilege of its own. It is keyed on the session, so switching chats remounts
it and per-session state cannot leak across the switch; the open-control state
also captures the slot it was opened in and the host renders only while that
slot is still active, so the committed render after a chat switch mounts
nothing against the next session. Its error boundary renders inline and
unmounts nothing but itself, because a control sits on the path of every
turn. Controls are keyed <appName>:<id>, sorted for a stable order, and the
composer renders at most two across all apps.

statusPath polling is refused before any fetch if the path would leave the
app's own prefix, go protocol-relative, reach another origin, or corrupt the
appended query string -- bounded at install and re-checked in the dashboard.
A control without statusPath is never polled, none is polled before a session
exists, and a failing app is not retried at the composer's expense.

An app declaring no controls produces no chip, no request and no DOM change.
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.

4 participants