Skip to content

feat: cross-session send (split view button + send_to_session MCP tool) - #5819

Open
alonzazo wants to merge 1 commit into
kirodotdev:mainfrom
alonzazo:feature/cross-session-send
Open

feat: cross-session send (split view button + send_to_session MCP tool)#5819
alonzazo wants to merge 1 commit into
kirodotdev:mainfrom
alonzazo:feature/cross-session-send

Conversation

@alonzazo

Copy link
Copy Markdown

Problem / Motivation

Sessions are fully isolated — there is no way for a user in split view (session grid) to route a composed message to another visible session, nor for an agent to hand context to a sibling session. Coordinating parallel sessions today requires copy-paste between panes.

Why it matters

Split view (dashboard.session_grid) makes running parallel sessions a first-class workflow, but without a message path between panes the grid is view-only coordination. Cross-session send closes that loop: draft in one pane, dispatch to another; or let an agent delegate follow-up work to a sibling session — with explicit user opt-in.

What changed (motivation → approach → change)

Goal: a message path between sessions that (a) cannot be used by an agent without the user's explicit opt-in, and (b) cannot become a feedback loop or a cross-workspace leak.

Approach: no new endpoint — /api/chat already routes by slot and queues when the target is busy, so the tool POSTs there. A single feature flag (dashboard.cross_session_send, default false) gates both surfaces: the UI button does not render without it, and the MCP tool rejects server-side, so an agent cannot move messages between sessions unless the user enabled it in Settings > Chat.

Backend (src/kiro_crew/):

  • New MCP tool send_to_session(slot, message) in mcp_tools/messaging.py (descriptor + handler, registered in HANDLERS and MCP_CORE_SCHEMAS).
  • Messages carry a [Cross-session message from <origin>] provenance prefix.
  • Security hardening, each guard fixing a concrete hole found during review:
    • LLM output is scanned (redact_exfiltration_urls + redact_credentials) before injection into another session.
    • Deny-by-default origin resolution (strict resolver, fail-closed), restricted to dashboard: origins so cron/hook callers cannot bypass the self-target guard.
    • Namespace-aware self-target guard (feedback-loop prevention).
    • Anchored slot-key schema pattern (^chat-\d+-\d+$) rejecting namespaced or path-like values.
    • Target-session existence probe (has_log) before the workspace comparison — a missing session has empty metadata that buckets to the default workspace and would otherwise pass the check open, and /api/chat would then silently create a session for the typo'd slot.
    • Workspace isolation mirroring the chat-history read paths (_caller_workspace / _ws_bucket).
    • SEL audit events on denied/error/success paths with consistent strict-resolver identity.
  • dashboard.cross_session_send flag: dataclass field, loader parse, PUT validation + GET exposure in the dashboard config handler, config-baseline.json regenerated.

Frontend (website/src/):

  • Send-to-session button (lucide SendHorizontal) in split view pane headers with a keyboard-operable listbox dropdown (useListboxKeyboard: focus-on-open, arrow nav, Escape-to-trigger, ARIA roles, roving tabindex, outside-click dismissal), portaled to document.body so it is not clipped by pane overflow.
  • The composer clears only after a confirmed 2xx; failures preserve the user's text and show a per-cause inline indicator (send failure vs. unsupported attachments). Focus returns to the trigger.
  • SessionGridView passes the other visible grid sessions as send targets, flag-gated.
  • Settings > Chat toggle; settingsRegistry.gen.ts regenerated via scripts/gen-settings-registry.mjs.

Known limitation (documented in code): _caller_workspace falls back to the default bucket for sessions whose metadata has not been written yet — same limitation as the existing chat-history read paths.

Tests

17 new tests in test/test_send_to_session.py:

  • Flag gate: disabled flag refuses without hitting the API, denial is SEL-audited; enabled flag posts to /api/chat?ws=1 with the target slot.
  • Provenance prefix and credential redaction of the injected message.
  • Validation chain: self-target rejected, unresolvable origin rejected (fail-closed), non-dashboard origin rejected, malformed slot rejected.
  • Workspace isolation: nonexistent target rejected before the workspace check (never spawns a session), cross-workspace target rejected.
  • Outcomes: queued/error/success/unexpected responses each reported and audited with the right SEL outcome.
  • Registration: tool advertised in schemas() and dispatchable via HANDLERS; config flag defaults to false.

Regression: test_config_loader.py + test_mcp_tool_registry.py (474 passed, 5 skipped).

Manual verification

Validated end-to-end in the internal (pre-fork) codebase this is ported from, in an isolated gateway with both flags enabled: UI button send between grid panes and agent-initiated send_to_session both worked, including the busy-target queueing path. Website tsc -b and vite build clean on this branch.

Screenshots / video

N/A — happy to add a short clip if reviewers want one; the UI surface is a small header button + dropdown in split view.

Add dashboard.cross_session_send (default false, Settings > Chat) and the
send_to_session MCP tool: route a composed message from one chat session
to another via the existing /api/chat slot routing. Double gate: the UI
button is hidden and the tool rejects server-side without the flag.

Hardening: LLM output redaction (exfil URLs + credentials) before
injection, deny-by-default origin resolution restricted to dashboard
sessions, namespace-aware self-target guard (feedback-loop prevention),
anchored slot-key schema pattern, target-session existence probe before
the workspace check, workspace isolation mirroring the chat-history read
paths, and SEL audit events on denied/error/success paths.

Frontend: send-to-session listbox dropdown in split view pane headers
(keyboard-operable, portaled, focus-restoring), per-cause failure
indicator preserving the composer text, Settings > Chat toggle.

17 new backend tests; website tsc -b and vite build clean.
@alonzazo
alonzazo requested a review from a team August 25, 2026 07:00
@alonzazo
alonzazo requested a review from a team as a code owner August 25, 2026 07:00
@alonzazo
alonzazo requested a review from Zedmor August 25, 2026 07:00
@github-actions github-actions Bot added fork Pull request from a fork (external contributor) readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running 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 Aug 25, 2026
@bolichen97
bolichen97 enabled auto-merge August 30, 2026 00:01
@bolichen97 bolichen97 added needs-pr-triage PR scanner: awaiting automated triage drive-to-green PR claimed by drive-to-green pipeline and removed needs-pr-triage PR scanner: awaiting automated triage labels Aug 31, 2026
@bolichen97

Copy link
Copy Markdown
Collaborator

🤖 Kiro Crew [operator: bolichen97#bb3ad1ca]: This PR has been inactive for 7+ days with failing CI. I've assessed the blockers and they appear resolvable — I'll push fixes directly to this branch as a co-author.

Assessment: Merge conflict from base drift plus mechanical CI failures: black formatting on the PR's two backend files, two i18n literal-string lint errors in ChatPane.tsx, and one new error response in files.py missing the machine-readable code field required by the error-code contract test. Plan: rebase onto main, resolve conflicts preserving the PR's intent, apply the formatting/i18n/error-code fixes, and drive CI to green.

If you'd prefer I don't touch this PR, add the pr-no-autofix label.

@bolichen97

Copy link
Copy Markdown
Collaborator

Audit note — part of this has already landed; the rest has not

This PR is not a duplicate and is not finished by anything on main. The audit checked it part by part against main, and some of what it does is already there. Flagging it so a reviewer does not have to rediscover the overlap, and so the PR is not mistaken for fully-covered work.

Already landed

Which parts main already has

Every agent-facing part. Main's session_send MCP tool (src/kiro_crew/mcp_dashboard.py:113/:315/:1008) -> POST /api/session-control/send (dashboard/handlers/session_control.py:182) -> send_to_target() (dashboard/session_control.py:1449) already delivers a message into another live session as its next turn behind an opt-in gate (session_control_enabled(), session_control.py:111), with a provenance prefix (_SEND_PROVENANCE, :1446, applied :1508), outbound sanitisation (sanitize_outbound, :1508), a length cap (MAX_SEND_MESSAGE_CHARS), a distinct queued-vs-started outcome, SEL audit on both the deny and allow paths, and authorize_target() (:987) refusals that are a strict superset of this PR's: self_target, workspace_mismatch, target_not_found, caller_unidentified, unattended_caller/target, ephemeral_caller/target, app_scoped_caller/target, linked_session_caller/target, mirrored_caller/target, crew_mode_target — plus re-validation when a queued entry drains (test/test_queue_drain_revalidation.py). Arg validation exists as validation.py:2680 SESSION_SEND_SCHEMA registered at :2912, and the behaviours are tested in test/test_session_control.py and test/test_session_control_boundaries.py. So this PR's mcp_tools/messaging.py handler + descriptor + HANDLERS entry, its validation.py SEND_TO_SESSION_SCHEMA, and the agent-half assertions in test/test_send_to_session.py are redundant with main and weaker.

What is still genuinely yours

The whole human-facing half, and the config plumbing that carries it. (1) website/src/components/ChatPane.tsx's pane-header SendHorizontal button + portaled role=listbox dropdown + doSendToSession (composer cleared only on a 2xx, attachments refused, focus returned) — 0 hits for SendHorizontal/sendTargets in origin/main website/src, and main's session-control routes are STRICT-internal (dashboard/server.py:439-443, 'no browser calls them'), so main has no browser path for cross-session send at all. (2) website/src/components/SessionGridView.tsx's flag-gated sendTargets from grid.occupiedSlots. (3) The Settings > Chat toggle (pages/settings/ChatPanel.tsx), settingsRegistry.gen.ts row, and the ChatSettings.tsx DashboardConfig field. (4) The dashboard.cross_session_send key itself — config/loader.py field + parse, dashboard/handlers/files.py PUT allowlist/validation/GET exposure, config-baseline.json — nonexistent on main (0 grep hits); it is redundant with agent.session_control for the agent half but there is no landed switch for a UI half that does not exist.

Suggested action: CONTINUE_DEV — the remainder is real work; rebase onto the landed part rather than closing.


From a repository-wide duplicate/overlap audit of every pull request open against main (2026-09-02, 330 PRs, one reviewer per PR). Each PR was read as its full merge-base diff plus its description and every comment and review, then compared against each candidate PR's own diff and against origin/main at 1a765b88ceb7. This PR is not being closed — the note is informational. If the reading is wrong, please correct the reasoning rather than just the conclusion.

@bolichen97
bolichen97 disabled auto-merge September 3, 2026 21:32
@bolichen97
bolichen97 enabled auto-merge (squash) September 3, 2026 21:32
@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

  • PR #5909 is OVERLAPPING relative to this PR. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #5909: MERGE_DISCUSSION. No conflict and no duplicated goal; the two complement each other, with 5819's new send site best rebased onto sendTurn once this lands. Files: website/src/components/ChatPane.tsx.
  • PR #6237 is OVERLAPPING relative to this PR. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #6237: REBASE. Two independent cross-session delivery surfaces with different gates; whoever lands second should say why both exist and whether channel containment covers the new one. Files: src/kiro_crew/validation.py.
  • This PR is PARTIALLY_COVERED with PR #5650. Coverage is explicitly incomplete; this finding is not a completion or closure claim. Recommended action for PR #5819: CONTINUE_DEVELOPMENT. Main covers the agent-facing half completely and with stronger containment, so the PR's messaging.py handler, its validation schema, and the agent-half assertions in test/test_send_to_session.py should be dropped in favour of session_send; the split-view UI, the grid flag gate, the Settings toggle and the config key remain uncovered work worth landing. Files: src/kiro_crew/mcp_tools/messaging.py, src/kiro_crew/validation.py, website/src/components/ChatPane.tsx.

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

@bolichen97

Copy link
Copy Markdown
Collaborator

@alonzazo Thanks for this, and please keep it open: the split-view half is genuinely uncovered work.

Already on main. The agent-facing half landed as session_send in #5650, which merged one day after this branch's merge base. Its guards are a strict superset of yours: main's sanitize_outbound is the same redact_exfiltration_urls plus redact_credentials chain, _SEND_PROVENANCE replaces your inline prefix, and src/kiro_crew/dashboard/session_control.py additionally refuses unattended, incognito/temporary, app-scoped, channel-linked and crew-mode callers, caps message length, and re-validates on queue drain. Please drop send_to_session from src/kiro_crew/mcp_tools/messaging.py, SEND_TO_SESSION_SCHEMA from src/kiro_crew/validation.py, and the agent-half assertions in test/test_send_to_session.py.

Still missing. Main has no browser path for cross-session send: SendHorizontal, sendTargets and dashboard.cross_session_send have zero hits, and /api/session-control/* is strict-internal, so the UI half needs a browser-reachable route or must keep using /api/chat. Please narrow the PR to that scope: the pane-header button, the website/src/components/SessionGridView.tsx flag gate, and the Settings > Chat toggle. Two blockers from the audit: the branch is far behind main with a dirty merge state, and #5909 moved ChatPane's send path onto the sendTurn transport, so doSendToSession should call sendTurn rather than api.sendChat. Please also say why a second flag is needed alongside agent.session_control.

Overlap with #8947 (cc @CrysisDeu): mechanical only. Both insert a prop at the same SessionGridView.renderLeaf call site, openSideChat there and sendTargets here, and both add ChatPane header UI. The goals differ and no behaviour is duplicated. #8947 is further along, so let it land first and rebase this one on top.

Posted from the 2026-09-08 open-PR relationship audit (read-only, one auditor per PR); reply here if any of this is wrong.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

drive-to-green PR claimed by drive-to-green pipeline fork Pull request from a fork (external contributor) merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: action required A blocking check or review needs attention

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants