Skip to content

feat(mktg-os): add per-agent chat panel with polling#1035

Draft
joanreyero wants to merge 5 commits into
mainfrom
feat/LFXAI-99
Draft

feat(mktg-os): add per-agent chat panel with polling#1035
joanreyero wants to merge 5 commits into
mainfrom
feat/LFXAI-99

Conversation

@joanreyero

Copy link
Copy Markdown
Contributor

Summary

Implements LFXAI-99 — the final story of the LFX Marketing OS Marketplace epic (LFXAI-95). Ports the guild_embed chat UX into an Angular per-agent chat panel, wired to the existing /api/mktg-agents Guild proxy (shipped in #1032) with REST polling instead of streaming (MVP, no ws dependency). Replaces the marketplace chat stub.

What's included

  • MktgChatPanelComponent — per-agent chat surface: optimistic send, history polling until a new agent reply (~1.5s cadence, ~60s deadline), typing indicator, Past Chats drawer (select/delete), New Chat, back-to-grid, draft-restore + optimistic rollback on send failure, and non-destructive recovery (drops a session only on a genuine 404/410, not on a transient error).
  • MktgAgentsService — thin HTTP client for chat (create/follow-up) + history.
  • MktgChatSessionService — SSR-safe localStorage persistence (guild_active_session_ids / guild_agent_sessions), retaining each session's owner token for follow-ups; a corrupt store degrades to empty.
  • MktgChatSession added to the shared chat contract.
  • Marketplace stub replaced with <lfx-mktg-chat-panel>.

Testing

  • yarn check-types, yarn lint, yarn build (SSR) all green; license headers pass.
  • Verified locally against the live linux-foundation/marketing-os Guild workspace (agent foundation-message, READY / published v1.0.4): create → poll → reply round-trip, follow-ups, session switch, new chat, delete, and expired-session recovery. First replies take ~15–25s (well within the 60s poll deadline).

Documented trade-offs

  • No unit/e2e specs. This app has no component/service unit-test convention (Playwright e2e only), and the chat surface is LaunchDarkly-gated + requires a live Guild backend, so a meaningful e2e cannot run green in CI yet — consistent with LFXAI-98, which shipped the marketplace without e2e. To be added when the flag goes GA. data-testids are in place throughout.
  • getHistory intentionally omits a catchError fallback — the component distinguishes an expired session (recover) from a transient poll error (retry); a service-level fallback would make that recovery path dead code.
  • Branch / JIRA key uses LFXAI, not LFXV2 — this workstream is tracked on the LFXAI board (matches PRs feat(mktg-os): scaffold flag-gated marketplace nav, route, env #1029 / feat(mktg-os): guild proxy, agent catalog, and marketplace landing #1032).

Not included (stays dark-launched)

The local-dev flag-bypass (MKTG_OS_FORCE_ENABLED) is intentionally not committed — Marketing OS stays gated behind the mktg-os-agents-enabled LaunchDarkly flag (default off; visible only via the targeting rule).

Relates to: LFXAI-99 · epic LFXAI-95.

Port the guild_embed chat UX into an Angular per-agent panel wired to the
existing /api/mktg-agents Guild proxy. Replaces the marketplace chat stub.

- MktgAgentsService: thin HTTP client for chat (create/follow-up) + history.
- MktgChatSessionService: SSR-safe localStorage persistence for per-agent
  sessions and active ids (guild_active_session_ids / guild_agent_sessions),
  keeping each session's owner token for follow-ups.
- MktgChatPanelComponent: optimistic send, REST polling of history until a new
  agent message lands (~1.5s cadence, ~60s deadline), Past Chats drawer with
  select/delete, new chat, typing indicator, and graceful expired-session
  recovery. In-flight fetches are cancelled on agent/session switch.
- Add MktgChatSession to the shared chat contract.

Streaming is deferred (MVP uses polling, no ws dependency).

Signed-off-by: Joan Reyero <joan@reyero.io>
Address post-commit reviewer findings on the per-agent chat panel:

- Track the in-flight send POST (take(1) + sendSub) and tear it down in
  cancelInFlight()/ngOnDestroy, so a destroy or session switch mid-send can no
  longer poll into the wrong session or write to a destroyed component.
- Handle the {success:true} response for a new-session request: fail soft via
  handleSendError instead of leaving the input wedged with isTyping stuck on.
- Log before every catchError/error fallback (send, history load, poll retry).
- Extract tagsLabel computed so the header no longer calls join() in template.

Signed-off-by: Joan Reyero <joan@reyero.io>
Address full-branch sweep findings:

- handleSendError now rolls back the optimistic user bubble and restores the
  draft text to the input, so a failed send no longer forces the user to retype.
- Move the tagsLabel computed below the signals block (Forms -> Signals ->
  Computed) per component-organization ordering.
- Document why getHistory intentionally omits a catchError fallback (the caller
  distinguishes expired sessions from transient poll errors).

Test coverage for the new component/services is deferred: the app has no
component/service unit-test convention (Playwright e2e only), and the chat
surface is LaunchDarkly-gated + needs a live Guild backend, so e2e can't run
green in CI yet. Tracked as a trade-off.

Signed-off-by: Joan Reyero <joan@reyero.io>
…nding

Address final full-branch sweep findings:

- loadHistory only drops a session (and its ownerToken) on a genuine 404/410;
  a transient network/5xx error now shows a non-destructive notice and leaves
  the saved session intact, so the user can still post follow-ups.
- Lock the message FormControl in lockstep with the Send button while sending or
  loading (lfx-input-text has no disabled input), making the draft-restore
  invariant in handleSendError actually hold instead of just being claimed.

Signed-off-by: Joan Reyero <joan@reyero.io>
Replace the constructor effect() that synced the message-control disabled
state with setTyping/setHistoryLoading setters that flip the lock alongside the
busy signals, per frontend-checklist \xC2\xA75 (no effect() for side effects).
Behavior is unchanged; the input is still disabled exactly when the Send button
is.

Signed-off-by: Joan Reyero <joan@reyero.io>
Copilot AI review requested due to automatic review settings June 26, 2026 20:33
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 45b8dfb4-3e9d-4488-935e-c5e0bf08326c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/LFXAI-99

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds the real per-agent chat experience to the Marketing OS marketplace by replacing the previous stub with a standalone chat panel that uses REST polling against the existing /api/mktg-agents proxy, plus client-side session persistence for “Past Chats”.

Changes:

  • Introduces MktgChatPanelComponent to handle per-agent chat UX (optimistic send, history polling, typing indicator, past chats UI).
  • Adds frontend services for chat transport (MktgAgentsService) and SSR-safe localStorage persistence (MktgChatSessionService).
  • Extends the shared contract with a persisted-session shape (MktgChatSession) and wires the marketplace page to render the chat panel.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/shared/src/interfaces/mktg-chat.interface.ts Adds MktgChatSession to the shared chat contract for persisted session metadata.
apps/lfx-one/src/app/shared/services/mktg-chat-session.service.ts New SSR-safe localStorage persistence for active session IDs and per-agent session lists.
apps/lfx-one/src/app/shared/services/mktg-agents.service.ts New thin HTTP client for chat send + history fetch via /api/mktg-agents.
apps/lfx-one/src/app/modules/mktg-os-agents/mktg-os-agents/mktg-os-agents.component.ts Registers the new chat panel component for the marketplace page.
apps/lfx-one/src/app/modules/mktg-os-agents/mktg-os-agents/mktg-os-agents.component.html Replaces the marketplace chat stub with <lfx-mktg-chat-panel>.
apps/lfx-one/src/app/modules/mktg-os-agents/mktg-chat-panel/mktg-chat-panel.component.ts Implements the chat panel logic (optimistic send, polling, session handling, expired-session recovery).
apps/lfx-one/src/app/modules/mktg-os-agents/mktg-chat-panel/mktg-chat-panel.component.html Implements the chat panel UI (header, sessions drawer, message list, typing indicator, input).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +74 to +85
private readMap<T = string>(key: string): Record<string, T> {
if (!isPlatformBrowser(this.platformId)) {
return {};
}
try {
const raw = localStorage.getItem(key);
const parsed = raw ? (JSON.parse(raw) as unknown) : null;
return parsed && typeof parsed === 'object' ? (parsed as Record<string, T>) : {};
} catch {
return {};
}
}
Comment on lines +239 to +248
switchMap(() =>
this.mktgAgents.getHistory(sessionId).pipe(
catchError((error) => {
// Swallow transient poll errors so a single hiccup doesn't abort the
// wait; the loop retries on the next tick.
console.warn('[mktg-chat] history poll failed, retrying', error);
return of(null);
})
)
),
Comment on lines +322 to +328
/**
* Recover from a failed send: roll back the optimistic bubble (the server never
* received it), restore the user's draft so they can resend without retyping,
* and surface an inline error. The message control is locked while sending (see
* the constructor effect), so no new input can have accumulated — restoring the
* draft can't clobber anything the user typed.
*/
Comment on lines +114 to +117
const agentMessageBaseline = this.countAgentMessages();
const ownerToken = sessionId ? this.sessionStore.getSession(agentId, sessionId)?.ownerToken : undefined;

this.sendSub = this.mktgAgents
Comment on lines +93 to +99
@for (message of messages(); track message.id) {
<div
class="flex flex-col max-w-[80%]"
[class.self-end]="message.sender === 'user'"
[class.items-end]="message.sender === 'user'"
[attr.data-testid]="'mktg-chat-message-' + message.sender">
<div
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants