Skip to content

Messages PWA Phase 1 — mobile UX + install prompt#238

Merged
jaylfc merged 14 commits intomasterfrom
feat/messages-pwa
Apr 20, 2026
Merged

Messages PWA Phase 1 — mobile UX + install prompt#238
jaylfc merged 14 commits intomasterfrom
feat/messages-pwa

Conversation

@jaylfc
Copy link
Copy Markdown
Owner

@jaylfc jaylfc commented Apr 20, 2026

Summary

Phase 1 of the Messages PWA — mobile-native UX for taOS talk. Service worker / offline caching is deferred to Phase 2.

Mobile UX

  • Full-screen thread takeover on mobile (stack nav with back button); desktop keeps right-side slide-over.
  • Bottom sheet for the message overflow menu (Edit / Delete / Copy link / Pin / Mark unread) — drag-to-dismiss, backdrop click, Esc.
  • Keyboard-aware composer — padding uses visualViewport.height so the input floats above the soft keyboard; message list adjusts in kind.
  • Safe-area inset padding applied to thread takeover, bottom sheet, composer.

PWA install

  • InstallPromptBanner listens for beforeinstallprompt and shows a dismissible top banner on mobile. 30-day suppression after "Not now". Hidden when already installed (display-mode: standalone).
  • iOS splash startup image meta tag in chat.html.

New shell primitives

  • desktop/src/hooks/use-visual-viewport.ts — reactive keyboard inset.
  • desktop/src/shell/BottomSheet.tsx — modal with drag-to-dismiss, safe-area, focus-first, Esc.
  • desktop/src/shell/InstallPromptBanner.tsx — `beforeinstallprompt` + 30-day dismissal.

Integration (all gated on useIsMobile() < 768px)

  • ThreadPanel gains isFullscreen prop.
  • MessagesApp conditionally swaps overflow menu to bottom sheet and adjusts composer padding.
  • ChatStandalone mounts <InstallPromptBanner> at the root.

Test plan

  • 14 new vitest tests (hook 3, BottomSheet 6, InstallPromptBanner 5). All pass.
  • Full desktop suite 278/281 (3 pre-existing snap-zones failures unrelated).
  • Backend suite: unchanged, all green.
  • Desktop bundle rebuilt.
  • Playwright mobile-viewport E2E stubs written (375×667); gated on `TAOS_E2E_URL`.
  • Manual smoke on a phone: open /chat-pwa on mobile Safari and Chrome, install banner appears + installs, thread takeover works, bottom sheet actions, keyboard doesn't cover composer.

Summary by CodeRabbit

  • New Features

    • Added iOS splash screen support for improved app appearance.
    • Introduced install-to-home-screen banner with 30-day dismissal persistence.
    • Mobile thread panel now expands full-screen when opened.
    • Message overflow menu displays as a dismissible bottom sheet on mobile.
    • Composer automatically adjusts padding when on-screen keyboard appears.
  • Tests

    • Added mobile PWA end-to-end tests covering thread takeover, bottom sheet menus, and install banner.
    • Added unit tests for new mobile components and keyboard-viewport detection.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 20, 2026

📝 Walkthrough

Walkthrough

This pull request introduces mobile PWA enhancements for the Messages chat application, adding new UI primitives (BottomSheet, InstallPromptBanner, useVisualViewport hook), integrating mobile-specific behavior into MessagesApp and ThreadPanel (fullscreen thread panels, bottom-sheet overflow menus, keyboard-aware padding), mounting an install banner in ChatStandalone, adding iOS splash screen meta tags, and including documentation and E2E tests for mobile workflows.

Changes

Cohort / File(s) Summary
New React Hook for Viewport
desktop/src/hooks/use-visual-viewport.ts, desktop/src/hooks/__tests__/use-visual-viewport.test.ts
Added useVisualViewport() hook that reads window.visualViewport and calculates keyboard inset. Includes unit tests for mocked visualViewport behavior and fallback to innerHeight.
New Shell Primitives
desktop/src/shell/BottomSheet.tsx, desktop/src/shell/BottomSheet.test.tsx, desktop/src/shell/InstallPromptBanner.tsx, desktop/src/shell/InstallPromptBanner.test.tsx
Added BottomSheet modal component with drag-to-dismiss, focus trapping, and Escape key handling. Added InstallPromptBanner that listens for beforeinstallprompt event and renders install UI with 30-day dismissal persistence. Both include comprehensive test suites.
App-Level Integration
desktop/src/apps/MessagesApp.tsx, desktop/src/apps/chat/ThreadPanel.tsx, desktop/src/ChatStandalone.tsx
Updated MessagesApp to conditionally render thread panel as fullscreen on mobile and overflow menu in BottomSheet; apply keyboard-aware bottom padding via useVisualViewport(). Updated ThreadPanel with isFullscreen prop to toggle between fixed side panel and full-screen overlay. Updated ChatStandalone to render InstallPromptBanner at top level.
HTML & Meta Tags
desktop/chat.html, static/desktop/chat.html
Added iOS startup splash screen <link rel="apple-touch-startup-image"> tag and updated asset references.
Static Assets
static/desktop/assets/MessagesApp-BG8aIEeG.js, static/desktop/assets/chat-BrDx525_.js, static/desktop/assets/tokens-C-PTlQoZ.css, static/desktop/index.html, static/desktop/assets/ProvidersApp-BY6yJVKl.js, static/desktop/assets/SettingsApp-F200Wg__.js, static/desktop/assets/MCPApp-Cq-bUfV5.js, static/desktop/assets/tsconfig.tsbuildinfo
Rebuilt compiled bundles with new mobile PWA features integrated; updated asset hashes and removed old bundles; replaced Tailwind CSS stylesheet with newer version.
Documentation & Tests
docs/superpowers/plans/2026-04-20-messages-pwa-phase-1.md, docs/superpowers/specs/2026-04-20-messages-pwa-phase-1-design.md, tests/e2e/test_messages_pwa.py
Added implementation plan and design specification for Messages PWA Phase 1. Added Playwright E2E tests covering mobile thread takeover, bottom-sheet overflow menu, and install banner visibility.
Test Infrastructure
desktop/vitest.setup.ts
Added localStorage and sessionStorage polyfill for Vitest environments.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ChatApp as Chat App
    participant Viewport as useVisualViewport
    participant MessagesApp
    participant ThreadPanel
    participant MessageOverflow as Message Overflow

    User->>ChatApp: Opens mobile view (375px)
    ChatApp->>Viewport: Initialize hook, read visualViewport.height
    Viewport-->>MessagesApp: Return {height, keyboardInset: 0}
    MessagesApp->>MessagesApp: Apply base padding

    User->>MessagesApp: Taps message "Reply in thread"
    MessagesApp->>ThreadPanel: Render with isFullscreen=true
    ThreadPanel->>ThreadPanel: Render full-screen overlay (inset-0, z-50)
    ThreadPanel-->>User: Display thread panel fullscreen

    User->>User: Opens mobile keyboard
    ChatApp->>Viewport: Trigger resize event, recalculate
    Viewport-->>MessagesApp: Return {height: reduced, keyboardInset: 120}
    MessagesApp->>MessagesApp: Apply keyboardInset to composer padding
    MessagesApp-->>User: Composer floats above keyboard

    User->>ThreadPanel: Click "Back" button
    ThreadPanel->>MessagesApp: Close thread panel
    MessagesApp->>MessagesApp: Re-render without fullscreen overlay

    User->>MessageOverflow: Click "More" menu on message
    MessagesApp->>MessageOverflow: Construct overflow menu
    MessageOverflow->>MessageOverflow: Render in BottomSheet on mobile
    MessageOverflow-->>User: Display bottom sheet with menu options
Loading
sequenceDiagram
    participant User
    participant Browser
    participant InstallPromptBanner as Install Banner
    participant LocalStorage
    participant InstallEvent as beforeinstallprompt Event

    User->>Browser: Navigate to /chat-pwa (first visit)
    Browser->>Browser: Fire beforeinstallprompt event
    Browser->>InstallPromptBanner: Event captured (preventDefault called)
    InstallPromptBanner->>LocalStorage: Check taos-install-dismissed
    LocalStorage-->>InstallPromptBanner: Not found (never dismissed)
    InstallPromptBanner-->>User: Render install banner

    User->>InstallPromptBanner: Click "Install"
    InstallPromptBanner->>InstallEvent: Call prompt()
    InstallEvent->>Browser: Display native install UI
    Browser-->>User: User accepts/dismisses
    InstallPromptBanner->>InstallPromptBanner: Clear stored event

    alt User accepts
        InstallPromptBanner->>Browser: App installed to homescreen
    else User dismisses
        InstallPromptBanner->>InstallPromptBanner: Hide banner
    end

    User->>InstallPromptBanner: Click "Not now"
    InstallPromptBanner->>LocalStorage: Store dismissal timestamp (now)
    InstallPromptBanner->>InstallPromptBanner: Hide banner

    User->>Browser: Navigate away then back within 30 days
    Browser->>InstallPromptBanner: Re-mount component
    InstallPromptBanner->>LocalStorage: Check taos-install-dismissed
    LocalStorage-->>InstallPromptBanner: Timestamp exists, < 30 days old
    InstallPromptBanner-->>User: Banner stays hidden
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Hops through mobile views with glee,
Thread panels fullscreen, as can be!
Bottom sheets slide up to say hello,
Install banners bloom with gentle flow,
PWA magic makes your chat app grow!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Messages PWA Phase 1 — mobile UX + install prompt' clearly and concisely summarizes the main objective of the changeset: implementing Phase 1 of a progressive web app with mobile UX improvements and install prompt functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/messages-pwa

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

@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot bot commented Apr 20, 2026

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Incremental Review: All new changes reviewed. No issues found.

Files Reviewed (15 files total | 2 changed)

Changed Files:

  • desktop/src/apps/MessagesApp.tsx
  • desktop/src/shell/BottomSheet.tsx

Previously Reviewed:

  • desktop/chat.html
  • desktop/src/ChatStandalone.tsx
  • desktop/src/apps/chat/ThreadPanel.tsx
  • desktop/src/hooks/__tests__/use-visual-viewport.test.ts
  • desktop/src/hooks/use-visual-viewport.ts
  • desktop/src/shell/__tests__/BottomSheet.test.tsx
  • desktop/src/shell/InstallPromptBanner.tsx
  • desktop/src/shell/__tests__/InstallPromptBanner.test.tsx
  • desktop/tsconfig.tsbuildinfo
  • desktop/vitest.setup.ts
  • docs/superpowers/plans/2026-04-20-messages-pwa-phase-1.md

Reviewed by seed-2-0-pro-260328 · 195,292 tokens

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (6)
desktop/src/apps/chat/ThreadPanel.tsx (1)

96-100: Consider using an icon component instead of text characters.

The back button uses "◀" text character while the close button uses "✕". For visual consistency with the rest of the codebase (which uses lucide-react icons), consider using <ChevronLeft /> and <X /> icons.

♻️ Proposed change
+import { ChevronLeft, X } from "lucide-react";
 // ...
         <button
           aria-label={isFullscreen ? "Back" : "Close thread"}
           onClick={onClose}
           className="p-1 hover:bg-white/5 rounded"
-        >{isFullscreen ? "◀" : "✕"}</button>
+        >{isFullscreen ? <ChevronLeft size={16} /> : <X size={16} />}</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/chat/ThreadPanel.tsx` around lines 96 - 100, Replace the
text characters used in the close/back button inside ThreadPanel (the button
using isFullscreen, onClose and aria-label) with lucide-react icon components:
render <ChevronLeft /> when isFullscreen is true and <X /> otherwise, update the
aria-labels accordingly, and add the necessary import for ChevronLeft and X from
"lucide-react" at the top of the file so the visual style matches the codebase.
desktop/src/hooks/__tests__/use-visual-viewport.test.ts (1)

35-42: Consider adding a test for offsetTop in keyboard inset calculation.

The hook's read() function uses vv.offsetTop in the calculation: Math.max(0, window.innerHeight - vv.height - vv.offsetTop). The current test keeps offsetTop: 0, so this branch isn't fully exercised.

🧪 Additional test case
it("accounts for offsetTop in keyboardInset calculation", () => {
  const { result } = renderHook(() => useVisualViewport());
  act(() => {
    vv.height = 500;
    vv.offsetTop = 50; // e.g., address bar visible
    listeners.get("resize")?.forEach((l) => l());
  });
  // 800 - 500 - 50 = 250
  expect(result.current.keyboardInset).toBe(250);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/hooks/__tests__/use-visual-viewport.test.ts` around lines 35 -
42, Add a new unit test that covers the branch using vv.offsetTop in
useVisualViewport's read() calculation: renderHook(() => useVisualViewport()),
then in act set vv.height and set vv.offsetTop to a non-zero value before
triggering the "resize" listeners, and assert result.current.keyboardInset
equals window.innerHeight - vv.height - vv.offsetTop (clamped to >=0). This
ensures the keyboardInset logic in read() (which computes Math.max(0,
window.innerHeight - vv.height - vv.offsetTop)) is exercised when offsetTop is
non-zero.
desktop/src/apps/MessagesApp.tsx (1)

1298-1302: Consider documenting the magic number in keyboard padding calculation.

The + 60 offset in the keyboard padding likely accounts for the input area height. A comment would clarify this for future maintainers.

             className="flex-1 overflow-y-auto px-4 py-3 space-y-0.5"
-            style={isMobile && keyboardInset > 0 ? { paddingBottom: `${keyboardInset + 60}px` } : undefined}
+            // Extra 60px accounts for input area height when keyboard is open
+            style={isMobile && keyboardInset > 0 ? { paddingBottom: `${keyboardInset + 60}px` } : undefined}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/MessagesApp.tsx` around lines 1298 - 1302, The paddingBottom
calculation uses a magic number "+ 60" when computing style in the element with
ref={messageListRef} and onScroll={handleScroll}; add a concise inline comment
explaining what 60 represents (e.g., input area height / toolbar + safe margin)
or replace it with a well-named constant (e.g., INPUT_AREA_HEIGHT or
KEYBOARD_PADDING_OFFSET) and use that constant in the style expression so future
maintainers understand the rationale.
desktop/src/shell/__tests__/InstallPromptBanner.test.tsx (1)

66-74: Consider referencing the constant instead of hardcoding 31 days.

The test uses 31 * 24 * 60 * 60 * 1000 directly. If DISMISS_MS in the component changes, this test could silently become inaccurate. Consider importing and using the constant, or adding + 1 to the threshold explicitly.

♻️ More maintainable approach
+// Import or define the same constant for clarity
+const DISMISS_MS = 30 * 24 * 60 * 60 * 1000;
+
 it("reappears after 30 days", async () => {
-  const thirtyOneDaysAgo = Date.now() - 31 * 24 * 60 * 60 * 1000;
+  const pastThreshold = Date.now() - DISMISS_MS - 1; // Just past the threshold
   localStorage.setItem("taos-install-dismissed", String(thirtyOneDaysAgo));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/shell/__tests__/InstallPromptBanner.test.tsx` around lines 66 -
74, The test hardcodes 31 days in milliseconds which can drift from the
component's DISMISS_MS; update the test in InstallPromptBanner.test.tsx to
import the DISMISS_MS constant from the component/module that defines it and
compute the stored timestamp as Date.now() - (DISMISS_MS + 1) so the value is
explicitly just past the dismissal threshold (or equivalently Date.now() -
DISMISS_MS - 1); reference the InstallPromptBanner component and DISMISS_MS when
locating where to change the localStorage setItem call.
desktop/src/shell/InstallPromptBanner.tsx (2)

28-34: localStorage read on every render is inefficient.

The localStorage.getItem(KEY) call on lines 33-34 executes during each render cycle. Consider moving this check into state initialization or a useMemo to avoid repeated synchronous storage access.

♻️ Proposed optimization
 export function InstallPromptBanner() {
   const isMobile = useIsMobile();
   const [event, setEvent] = useState<BeforeInstallPromptEvent | null>(null);
   const [dismissed, setDismissed] = useState(false);
+  const [recentlyDismissed] = useState(() => {
+    if (typeof window === "undefined") return false;
+    const prev = localStorage.getItem(KEY);
+    return prev ? Date.now() - Number(prev) < DISMISS_MS : false;
+  });

   useEffect(() => {
     const onPrompt = (e: Event) => {
       e.preventDefault();
       setEvent(e as BeforeInstallPromptEvent);
     };
     window.addEventListener("beforeinstallprompt", onPrompt);
     return () => window.removeEventListener("beforeinstallprompt", onPrompt);
   }, []);

-  if (!isMobile || !event || dismissed) return null;
+  if (!isMobile || !event || dismissed || recentlyDismissed) return null;

   if (typeof window !== "undefined") {
     const mql = window.matchMedia("(display-mode: standalone)");
     if (mql.matches) return null;
   }

-  const prev = localStorage.getItem(KEY);
-  if (prev && Date.now() - Number(prev) < DISMISS_MS) return null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/shell/InstallPromptBanner.tsx` around lines 28 - 34, Move the
synchronous localStorage lookup out of the component render path: instead of
calling localStorage.getItem(KEY) directly in InstallPromptBanner's body, read
it once during state initialization (e.g., in useState initializer) or inside a
useMemo/useEffect and store the result in state; then use that state to decide
whether to return null based on DISMISS_MS. Update any logic that uses KEY and
DISMISS_MS to reference the cached value so subsequent renders don't
synchronously access localStorage.

36-44: Consider handling prompt rejection more explicitly.

The install() function silently ignores all errors including user cancellation. While this is intentional per the comment, you might want to differentiate between user dismissal (normal) vs. actual errors for observability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/shell/InstallPromptBanner.tsx` around lines 36 - 44, The
install() handler currently swallows all errors; change it to await
event.prompt() and then await event.userChoice into a variable (e.g. const
choice = await event.userChoice), inspect choice.outcome for 'dismissed' vs
'accepted' to handle user cancellation separately (optional debug/info log) and
wrap only the async calls in a try/catch that logs real errors (console.error or
your logger) including the caught error object, then always call setEvent(null)
at the end; reference install(), event.prompt(), event.userChoice, and
setEvent(null) when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@desktop/src/shell/BottomSheet.tsx`:
- Around line 28-34: The modal sheet currently only autofocuses once and allows
focus to escape; update the BottomSheet component to implement a proper focus
trap and focus restoration: in the useEffect that runs when open (the existing
effect using sheetRef and open), store document.activeElement as
previousActiveElement, move focus to the first focusable inside sheetRef, add
event listeners for keydown (handle Tab/Shift+Tab to cycle between first and
last focusable) and focusin (if focus moves outside sheetRef, redirect it back
to the first/last), and on cleanup remove listeners and restore focus to
previousActiveElement; apply the same pattern to the other useEffect region
referenced (lines ~74-96) so both entry points enforce the trap, and ensure
aria-modal="true" remains set.

In `@desktop/vitest.setup.ts`:
- Around line 8-19: localStorage and sessionStorage currently share the same Map
and sessionStorage is created via object spread which turns the getter length
into a static property; fix this by creating a factory function (e.g.,
createStorage()) that instantiates a new Map and returns an object with get
length(), key(), getItem(), setItem(), removeItem(), and clear() methods closing
over that Map, then assign globalThis.localStorage = createStorage() and
globalThis.sessionStorage = createStorage() (do not use object spread) so each
storage has an isolated backend and a live dynamic length getter.

In `@docs/superpowers/specs/2026-04-20-messages-pwa-phase-1-design.md`:
- Around line 91-99: The splash-screen section is out of sync: the
implementation ships a single generic apple-touch-startup-image rather than
multiple device-specific startup images and no asset-generation step is run;
update the spec in desktop/chat.html to reflect the actual behavior by removing
the device-specific <link rel="apple-touch-startup-image"> entries and the
asset-generation step, or alternatively adjust the implementation reference if
you intend to keep per-device images—specifically mention the single startup
image filename used (e.g., splash.png or the actual shipped name), its
background color (`#1a1b2e`) and the source icon size (1024×1024) so the doc
matches the code paths that set apple-touch-startup-image.

In `@static/desktop/assets/MessagesApp-BlnZE9ab.js`:
- Line 1: The message-row actions rely only on hover-driven state (Tt with Le
via onMouseEnter/onMouseLeave) so mobile/touch has no reliable way to reveal
MessageHoverActions (ks) or reaction picker (ze/Pe). Update the message
rendering block where onMouseEnter/onMouseLeave call Le to also support touch:
add onTouchStart/onTouchEnd (or onContextMenu for long-press) handlers that
set/toggle Tt via Le (and set Pe/ze for reactions) so touch users can reveal the
same UI, and also add a small persistent mobile-visible affordance button inside
the message row that opens the overflow menu by calling the same handler that
B/Le use (so ks can be opened on tap). Ensure you modify the message container
logic and the render path that checks Tt===t.id and ze===t.id to reuse the same
handlers so both hover and touch produce identical behavior.

In `@tests/e2e/test_messages_pwa.py`:
- Around line 31-35: The test uses hard-coded UI selections
(mobile_page.get_by_text("roundtable"),
mobile_page.locator("[data-message-id]").first, mobile_page.get_by_role(...))
which ties E2Es to seeded content; instead create or select deterministic test
data in setup via the API (create a channel and message) and use those
resources' unique IDs or test-only markers to drive the UI interactions
(navigate to the channel created, find the specific message by its test id
instead of .first, then tap() and click the reply button). Apply the same change
for the similar block around lines 46-50 so tests no longer depend on external
seeded data.

---

Nitpick comments:
In `@desktop/src/apps/chat/ThreadPanel.tsx`:
- Around line 96-100: Replace the text characters used in the close/back button
inside ThreadPanel (the button using isFullscreen, onClose and aria-label) with
lucide-react icon components: render <ChevronLeft /> when isFullscreen is true
and <X /> otherwise, update the aria-labels accordingly, and add the necessary
import for ChevronLeft and X from "lucide-react" at the top of the file so the
visual style matches the codebase.

In `@desktop/src/apps/MessagesApp.tsx`:
- Around line 1298-1302: The paddingBottom calculation uses a magic number "+
60" when computing style in the element with ref={messageListRef} and
onScroll={handleScroll}; add a concise inline comment explaining what 60
represents (e.g., input area height / toolbar + safe margin) or replace it with
a well-named constant (e.g., INPUT_AREA_HEIGHT or KEYBOARD_PADDING_OFFSET) and
use that constant in the style expression so future maintainers understand the
rationale.

In `@desktop/src/hooks/__tests__/use-visual-viewport.test.ts`:
- Around line 35-42: Add a new unit test that covers the branch using
vv.offsetTop in useVisualViewport's read() calculation: renderHook(() =>
useVisualViewport()), then in act set vv.height and set vv.offsetTop to a
non-zero value before triggering the "resize" listeners, and assert
result.current.keyboardInset equals window.innerHeight - vv.height -
vv.offsetTop (clamped to >=0). This ensures the keyboardInset logic in read()
(which computes Math.max(0, window.innerHeight - vv.height - vv.offsetTop)) is
exercised when offsetTop is non-zero.

In `@desktop/src/shell/__tests__/InstallPromptBanner.test.tsx`:
- Around line 66-74: The test hardcodes 31 days in milliseconds which can drift
from the component's DISMISS_MS; update the test in InstallPromptBanner.test.tsx
to import the DISMISS_MS constant from the component/module that defines it and
compute the stored timestamp as Date.now() - (DISMISS_MS + 1) so the value is
explicitly just past the dismissal threshold (or equivalently Date.now() -
DISMISS_MS - 1); reference the InstallPromptBanner component and DISMISS_MS when
locating where to change the localStorage setItem call.

In `@desktop/src/shell/InstallPromptBanner.tsx`:
- Around line 28-34: Move the synchronous localStorage lookup out of the
component render path: instead of calling localStorage.getItem(KEY) directly in
InstallPromptBanner's body, read it once during state initialization (e.g., in
useState initializer) or inside a useMemo/useEffect and store the result in
state; then use that state to decide whether to return null based on DISMISS_MS.
Update any logic that uses KEY and DISMISS_MS to reference the cached value so
subsequent renders don't synchronously access localStorage.
- Around line 36-44: The install() handler currently swallows all errors; change
it to await event.prompt() and then await event.userChoice into a variable (e.g.
const choice = await event.userChoice), inspect choice.outcome for 'dismissed'
vs 'accepted' to handle user cancellation separately (optional debug/info log)
and wrap only the async calls in a try/catch that logs real errors
(console.error or your logger) including the caught error object, then always
call setEvent(null) at the end; reference install(), event.prompt(),
event.userChoice, and setEvent(null) when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f01922ed-6a9e-46c6-bde1-c7e9abeaad36

📥 Commits

Reviewing files that changed from the base of the PR and between 8aa6875 and f0c71f0.

📒 Files selected for processing (28)
  • desktop/chat.html
  • desktop/src/ChatStandalone.tsx
  • desktop/src/apps/MessagesApp.tsx
  • desktop/src/apps/chat/ThreadPanel.tsx
  • desktop/src/hooks/__tests__/use-visual-viewport.test.ts
  • desktop/src/hooks/use-visual-viewport.ts
  • desktop/src/shell/BottomSheet.tsx
  • desktop/src/shell/InstallPromptBanner.tsx
  • desktop/src/shell/__tests__/BottomSheet.test.tsx
  • desktop/src/shell/__tests__/InstallPromptBanner.test.tsx
  • desktop/tsconfig.tsbuildinfo
  • desktop/vitest.setup.ts
  • docs/superpowers/plans/2026-04-20-messages-pwa-phase-1.md
  • docs/superpowers/specs/2026-04-20-messages-pwa-phase-1-design.md
  • static/desktop/assets/MCPApp-Cmbj2-Z_.js
  • static/desktop/assets/MessagesApp-BkrxHSyC.js
  • static/desktop/assets/MessagesApp-BlnZE9ab.js
  • static/desktop/assets/ProvidersApp-Bag-W8Oo.js
  • static/desktop/assets/SettingsApp-BdbbrvJ2.js
  • static/desktop/assets/chat-BFUaevuL.js
  • static/desktop/assets/chat-D-JXvZwU.js
  • static/desktop/assets/main-CMjABSro.js
  • static/desktop/assets/tokens-BrwHLUZg.css
  • static/desktop/assets/tokens-C-PTlQoZ.css
  • static/desktop/assets/tokens-C63es8oZ.js
  • static/desktop/chat.html
  • static/desktop/index.html
  • tests/e2e/test_messages_pwa.py
💤 Files with no reviewable changes (3)
  • static/desktop/assets/chat-D-JXvZwU.js
  • static/desktop/assets/tokens-BrwHLUZg.css
  • static/desktop/assets/MessagesApp-BkrxHSyC.js

Comment thread desktop/src/shell/BottomSheet.tsx
Comment thread desktop/vitest.setup.ts
Comment on lines +8 to +19
const store = new Map<string, string>();
const impl = {
get length() { return store.size; },
key(index: number) { return [...store.keys()][index] ?? null; },
getItem(key: string) { return store.has(key) ? store.get(key)! : null; },
setItem(key: string, value: string) { store.set(key, String(value)); },
removeItem(key: string) { store.delete(key); },
clear() { store.clear(); },
};
Object.defineProperty(globalThis, "localStorage", { value: impl, configurable: true, writable: true });
Object.defineProperty(globalThis, "sessionStorage", { value: { ...impl, clear() { store.clear(); } }, configurable: true, writable: true });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the actual file to confirm the current state
cat -n desktop/vitest.setup.ts

Repository: jaylfc/tinyagentos

Length of output: 1238


🏁 Script executed:

# Let's also check the Git history to understand what changed
git log --oneline -5 desktop/vitest.setup.ts 2>/dev/null || echo "Git not available or file not tracked"

Repository: jaylfc/tinyagentos

Length of output: 157


Separate storage backends in the test polyfill.

localStorage and sessionStorage currently share the same Map, violating Web Storage API semantics that require isolation between storage types. Additionally, sessionStorage is created via object spread, which converts the get length() getter into a static property—breaking tests that expect dynamic length updates.

The proposed fix creates independent stores for each storage type using a factory function, ensuring proper isolation and live length behavior.

🔧 Proposed fix
 if (typeof localStorage === "undefined" || typeof localStorage.clear !== "function") {
-  const store = new Map<string, string>();
-  const impl = {
-    get length() { return store.size; },
-    key(index: number) { return [...store.keys()][index] ?? null; },
-    getItem(key: string) { return store.has(key) ? store.get(key)! : null; },
-    setItem(key: string, value: string) { store.set(key, String(value)); },
-    removeItem(key: string) { store.delete(key); },
-    clear() { store.clear(); },
-  };
-  Object.defineProperty(globalThis, "localStorage", { value: impl, configurable: true, writable: true });
-  Object.defineProperty(globalThis, "sessionStorage", { value: { ...impl, clear() { store.clear(); } }, configurable: true, writable: true });
+  const createStorage = () => {
+    const store = new Map<string, string>();
+    return {
+      get length() { return store.size; },
+      key(index: number) { return [...store.keys()][index] ?? null; },
+      getItem(key: string) { return store.has(key) ? store.get(key)! : null; },
+      setItem(key: string, value: string) { store.set(key, String(value)); },
+      removeItem(key: string) { store.delete(key); },
+      clear() { store.clear(); },
+    };
+  };
+  Object.defineProperty(globalThis, "localStorage", { value: createStorage(), configurable: true, writable: true });
+  Object.defineProperty(globalThis, "sessionStorage", { value: createStorage(), configurable: true, writable: true });
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const store = new Map<string, string>();
const impl = {
get length() { return store.size; },
key(index: number) { return [...store.keys()][index] ?? null; },
getItem(key: string) { return store.has(key) ? store.get(key)! : null; },
setItem(key: string, value: string) { store.set(key, String(value)); },
removeItem(key: string) { store.delete(key); },
clear() { store.clear(); },
};
Object.defineProperty(globalThis, "localStorage", { value: impl, configurable: true, writable: true });
Object.defineProperty(globalThis, "sessionStorage", { value: { ...impl, clear() { store.clear(); } }, configurable: true, writable: true });
}
const createStorage = () => {
const store = new Map<string, string>();
return {
get length() { return store.size; },
key(index: number) { return [...store.keys()][index] ?? null; },
getItem(key: string) { return store.has(key) ? store.get(key)! : null; },
setItem(key: string, value: string) { store.set(key, String(value)); },
removeItem(key: string) { store.delete(key); },
clear() { store.clear(); },
};
};
Object.defineProperty(globalThis, "localStorage", { value: createStorage(), configurable: true, writable: true });
Object.defineProperty(globalThis, "sessionStorage", { value: createStorage(), configurable: true, writable: true });
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/vitest.setup.ts` around lines 8 - 19, localStorage and sessionStorage
currently share the same Map and sessionStorage is created via object spread
which turns the getter length into a static property; fix this by creating a
factory function (e.g., createStorage()) that instantiates a new Map and returns
an object with get length(), key(), getItem(), setItem(), removeItem(), and
clear() methods closing over that Map, then assign globalThis.localStorage =
createStorage() and globalThis.sessionStorage = createStorage() (do not use
object spread) so each storage has an isolated backend and a live dynamic length
getter.

Comment on lines +91 to +99
**`desktop/chat.html`** — add iOS splash screen meta tags:
```html
<link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/static/splash-iphone-15-pro-max.png" />
<link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/static/splash-iphone-15-pro.png" />
<link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/static/splash-iphone-14-plus.png" />
<link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/static/splash-iphone-14.png" />
```

Splash images generated from the 1024×1024 icon, centered on the app background color `#1a1b2e`. Plan includes the generation step.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

This splash-screen section no longer matches the implementation.

The spec still calls for device-specific startup images and an asset-generation step, but the PR ships a single generic apple-touch-startup-image. Leaving this stale will send the next iteration toward work the current implementation does not use.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/specs/2026-04-20-messages-pwa-phase-1-design.md` around
lines 91 - 99, The splash-screen section is out of sync: the implementation
ships a single generic apple-touch-startup-image rather than multiple
device-specific startup images and no asset-generation step is run; update the
spec in desktop/chat.html to reflect the actual behavior by removing the
device-specific <link rel="apple-touch-startup-image"> entries and the
asset-generation step, or alternatively adjust the implementation reference if
you intend to keep per-device images—specifically mention the single startup
image filename used (e.g., splash.png or the actual shipped name), its
background color (`#1a1b2e`) and the source icon size (1024×1024) so the doc
matches the code paths that set apple-touch-startup-image.

@@ -0,0 +1 @@
import{r as o,j as e,p as Zt,b as es}from"./vendor-react-l6srOxy7.js";import{B as U,r as it,T as ts,L as ee,I as xe,C as ss,a as ns,b as as,c as rs}from"./toolbar-UW6q5pkx.js";import{M as is}from"./MobileSplitView-CtNEF6zb.js";import{u as ls}from"./use-is-mobile-v5lglusa.js";import{W as fe,z as lt,D as $e,E as oe,R as ot,y as ct,G as dt,J as ht,U as Te,K as ut,v as pt,N as mt,X as ge,O as os,Q as cs,g as xt}from"./vendor-icons-wm645Jsx.js";import"./vendor-radix-BhM7AEEG.js";import"./vendor-layout-B-pp9n1f.js";function ft(){if(typeof window>"u")return{height:0,keyboardInset:0};const s=window.visualViewport;if(!s)return{height:window.innerHeight,keyboardInset:0};const a=Math.max(0,window.innerHeight-s.height-s.offsetTop);return{height:s.height,keyboardInset:a}}function ds(){const[s,a]=o.useState(ft);return o.useEffect(()=>{if(typeof window>"u")return;const i=window.visualViewport;if(!i)return;const l=()=>a(ft());return i.addEventListener("resize",l),i.addEventListener("scroll",l),()=>{i.removeEventListener("resize",l),i.removeEventListener("scroll",l)}},[]),s}async function hs(s){try{return await s.json()}catch{return null}}async function de(s){if(s.ok)return;const a=await hs(s);throw new Error((a==null?void 0:a.error)||`HTTP ${s.status}`)}async function us(s,a){const i=await fetch(`/api/chat/channels/${encodeURIComponent(s)}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)});await de(i)}async function ps(s,a){const i=await fetch(`/api/chat/channels/${encodeURIComponent(s)}/members`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"add",slug:a})});await de(i)}async function wt(s,a){const i=await fetch(`/api/chat/channels/${encodeURIComponent(s)}/members`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"remove",slug:a})});await de(i)}async function jt(s,a){const i=await fetch(`/api/chat/channels/${encodeURIComponent(s)}/muted`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"add",slug:a})});await de(i)}async function vt(s,a){const i=await fetch(`/api/chat/channels/${encodeURIComponent(s)}/muted`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"remove",slug:a})});await de(i)}function ms({channel:s,knownAgents:a,onClose:i,onChanged:l}){const[d,x]=o.useState(s.name),[h,y]=o.useState(s.topic||""),[v,w]=o.useState(s.settings.response_mode??"quiet"),[N,g]=o.useState(s.settings.max_hops??3),[C,z]=o.useState(s.settings.cooldown_seconds??5),[m,E]=o.useState(null);o.useEffect(()=>{x(s.name),y(s.topic||""),w(s.settings.response_mode??"quiet"),g(s.settings.max_hops??3),z(s.settings.cooldown_seconds??5)},[s]);const _=async(u,M)=>{E(null);try{await us(s.id,u),l()}catch(V){M(),E(V instanceof Error?V.message:"failed")}},S=s.members||[],p=s.settings.muted||[],T=a.map(u=>u.name).filter(u=>!S.includes(u)),k=S.filter(u=>u!=="user"&&!p.includes(u));return e.jsxs("aside",{role:"complementary","aria-label":"Channel settings",className:"fixed top-0 right-0 h-full w-[360px] bg-shell-surface border-l border-white/10 shadow-xl flex flex-col z-40",children:[e.jsxs("header",{className:"flex items-center justify-between px-4 py-3 border-b border-white/10",children:[e.jsx("h2",{className:"text-sm font-semibold",children:"Channel settings"}),e.jsx("button",{onClick:i,"aria-label":"Close",className:"text-lg leading-none",children:"×"})]}),e.jsxs("div",{className:"flex-1 overflow-y-auto px-4 py-4 flex flex-col gap-5 text-sm",children:[e.jsxs("section",{"aria-label":"Overview",className:"flex flex-col gap-3",children:[e.jsx("h3",{className:"text-xs uppercase tracking-wider text-shell-text-tertiary",children:"Overview"}),e.jsxs("label",{className:"flex flex-col gap-1",children:[e.jsx("span",{className:"text-xs text-shell-text-secondary",children:"Name"}),e.jsx("input",{value:d,maxLength:100,onChange:u=>x(u.target.value),onBlur:()=>d!==s.name&&_({name:d},()=>x(s.name)),className:"bg-white/5 border border-white/10 rounded px-2 py-1.5 text-sm"})]}),e.jsxs("label",{className:"flex flex-col gap-1",children:[e.jsx("span",{className:"text-xs text-shell-text-secondary",children:"Topic"}),e.jsx("textarea",{value:h,maxLength:500,rows:3,onChange:u=>y(u.target.value),onBlur:()=>h!==(s.topic||"")&&_({topic:h},()=>y(s.topic||"")),className:"bg-white/5 border border-white/10 rounded px-2 py-1.5 text-sm resize-none"})]}),e.jsxs("div",{className:"text-[11px] text-shell-text-tertiary",children:["Type: ",e.jsx("span",{className:"uppercase tracking-wide",children:s.type})]})]}),e.jsxs("section",{"aria-label":"Members",className:"flex flex-col gap-2",children:[e.jsx("h3",{className:"text-xs uppercase tracking-wider text-shell-text-tertiary",children:"Members"}),e.jsx("ul",{className:"flex flex-col gap-1",children:S.map(u=>e.jsxs("li",{className:"flex items-center justify-between px-2 py-1 rounded hover:bg-white/5",children:[e.jsxs("span",{children:["@",u]}),u!=="user"&&e.jsx("button",{className:"text-xs text-red-300 hover:text-red-200",onClick:async()=>{try{await wt(s.id,u),l()}catch(M){E(M instanceof Error?M.message:"failed")}},children:"Remove"})]},u))}),T.length>0&&e.jsx(gt,{label:"Add agent",options:T,onPick:async u=>{try{await ps(s.id,u),l()}catch(M){E(M instanceof Error?M.message:"failed")}}})]}),e.jsxs("section",{"aria-label":"Moderation",className:"flex flex-col gap-3",children:[e.jsx("h3",{className:"text-xs uppercase tracking-wider text-shell-text-tertiary",children:"Moderation"}),e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("span",{className:"text-xs text-shell-text-secondary",children:"Mode:"}),e.jsx("button",{className:`px-2 py-1 rounded text-xs ${v==="quiet"?"bg-sky-500/30 text-sky-200":"bg-white/5"}`,onClick:()=>_({response_mode:"quiet"},()=>w(v)),children:"quiet"}),e.jsx("button",{className:`px-2 py-1 rounded text-xs ${v==="lively"?"bg-emerald-500/30 text-emerald-200":"bg-white/5"}`,onClick:()=>_({response_mode:"lively"},()=>w(v)),children:"lively"})]}),e.jsxs("div",{className:"flex flex-col gap-1",children:[e.jsx("span",{className:"text-xs text-shell-text-secondary",children:"Muted"}),e.jsxs("div",{className:"flex flex-wrap gap-1",children:[p.map(u=>e.jsxs("span",{className:"inline-flex items-center gap-1 bg-white/5 rounded px-2 py-0.5 text-xs",children:["@",u,e.jsx("button",{"aria-label":`Unmute ${u}`,onClick:async()=>{try{await vt(s.id,u),l()}catch(M){E(M instanceof Error?M.message:"failed")}},children:"×"})]},u)),p.length===0&&e.jsx("span",{className:"text-[11px] text-shell-text-tertiary",children:"none"})]}),k.length>0&&e.jsx(gt,{label:"Mute agent",options:k,onPick:async u=>{try{await jt(s.id,u),l()}catch(M){E(M instanceof Error?M.message:"failed")}}})]})]}),e.jsxs("section",{"aria-label":"Advanced",className:"flex flex-col gap-3",children:[e.jsx("h3",{className:"text-xs uppercase tracking-wider text-shell-text-tertiary",children:"Advanced"}),e.jsxs("label",{className:"flex flex-col gap-1",children:[e.jsxs("span",{className:"text-xs text-shell-text-secondary",children:["Max hops: ",N]}),e.jsx("input",{type:"range",min:1,max:10,value:N,onChange:u=>g(Number(u.target.value)),onMouseUp:()=>_({max_hops:N},()=>g(s.settings.max_hops??3))})]}),e.jsxs("label",{className:"flex flex-col gap-1",children:[e.jsxs("span",{className:"text-xs text-shell-text-secondary",children:["Cooldown: ",C,"s"]}),e.jsx("input",{type:"range",min:0,max:60,value:C,onChange:u=>z(Number(u.target.value)),onMouseUp:()=>_({cooldown_seconds:C},()=>z(s.settings.cooldown_seconds??5))})]})]}),m&&e.jsx("div",{role:"alert",className:"text-xs text-red-300 bg-red-500/10 border border-red-500/30 rounded px-2 py-1",children:m})]})]})}function gt({label:s,options:a,onPick:i}){return e.jsxs("select",{"aria-label":s,defaultValue:"",onChange:l=>{l.target.value&&(i(l.target.value),l.target.value="")},className:"bg-white/5 border border-white/10 rounded px-2 py-1 text-xs",children:[e.jsxs("option",{value:"",disabled:!0,children:[s,"…"]}),a.map(l=>e.jsxs("option",{value:l,children:["@",l]},l))]})}function xs({slug:s,channelId:a,channelType:i,isMuted:l,x:d,y:x,onClose:h,onDm:y,onViewInfo:v,onJumpToSettings:w}){const N=o.useRef(null);o.useEffect(()=>{const m=_=>{N.current&&!N.current.contains(_.target)&&h()},E=_=>{_.key==="Escape"&&h()};return document.addEventListener("mousedown",m),document.addEventListener("keydown",E),()=>{document.removeEventListener("mousedown",m),document.removeEventListener("keydown",E)}},[h]);const g=i==="dm",C=async()=>{if(a)try{l?await vt(a,s):await jt(a,s)}finally{h()}},z=async()=>{if(a)try{await wt(a,s)}finally{h()}};return e.jsxs("div",{ref:N,role:"menu","aria-label":`Actions for @${s}`,className:"fixed z-50 min-w-[200px] bg-shell-surface border border-white/10 rounded-lg shadow-xl py-1 text-sm",style:{top:x,left:d},children:[e.jsxs(ce,{onClick:()=>{y==null||y(s),h()},children:["DM @",s]}),a&&!g&&e.jsxs(ce,{onClick:C,children:[l?"Unmute":"Mute"," in this channel"]}),a&&!g&&e.jsx(ce,{onClick:z,children:"Remove from channel"}),e.jsx("div",{className:"my-1 h-px bg-white/10"}),e.jsx(ce,{onClick:()=>{v==null||v(s),h()},children:"View agent info"}),e.jsx(ce,{onClick:()=>{w==null||w(s),h()},children:"Jump to agent settings"})]})}function ce({onClick:s,children:a}){return e.jsx("button",{role:"menuitem",onClick:s,className:"w-full text-left px-3 py-1.5 hover:bg-white/5 focus:bg-white/5 focus:outline-none",children:a})}function fs({commands:s,queryAfterSlash:a,members:i,onPick:l,onClose:d}){const[x,h]=o.useState(0),y=o.useMemo(()=>gs(s,i,a),[s,i,a]),v=y.filter(w=>w.kind==="cmd");return o.useEffect(()=>{h(0)},[a]),o.useEffect(()=>{const w=N=>{if(N.key==="Escape"){N.preventDefault(),d();return}if(N.key==="ArrowDown"){N.preventDefault(),h(g=>Math.min(v.length-1,g+1));return}if(N.key==="ArrowUp"){N.preventDefault(),h(g=>Math.max(0,g-1));return}if(N.key==="Enter"){N.preventDefault();const g=v[x];g&&l(g.slug,g.cmd.name)}};return document.addEventListener("keydown",w,!0),()=>document.removeEventListener("keydown",w,!0)},[v,x,l,d]),v.length===0&&y.length===0?null:e.jsx("div",{role:"listbox","aria-label":"Slash commands",className:"absolute bottom-full left-0 mb-2 w-full max-w-md bg-shell-surface border border-white/10 rounded-lg shadow-xl max-h-60 overflow-y-auto text-sm",children:y.length===0?e.jsx("div",{className:"px-3 py-2 text-xs text-shell-text-tertiary",children:"(no commands available)"}):y.map(w=>{if(w.kind==="header")return e.jsxs("div",{className:"px-3 py-1 text-[11px] uppercase tracking-wider text-shell-text-tertiary bg-white/5",children:["@",w.slug]},`h-${w.slug}`);const N=v.indexOf(w),g=N===x;return e.jsxs("button",{role:"option","aria-selected":g,onMouseEnter:()=>h(N),onClick:()=>l(w.slug,w.cmd.name),className:`w-full text-left px-3 py-1.5 flex items-center justify-between gap-3 ${g?"bg-white/10":"hover:bg-white/5"}`,children:[e.jsxs("span",{className:"font-mono text-[13px]",children:["/",w.cmd.name]}),e.jsx("span",{className:"text-xs text-shell-text-tertiary truncate",children:w.cmd.description})]},`${w.slug}-${w.cmd.name}`)})})}function gs(s,a,i){const l=i.toLowerCase(),d=a.filter(y=>y!=="user"&&s[y]),x=d.length===1,h=[];for(const y of d){const v=(s[y]||[]).filter(w=>bs(y,w,l));if(v.length!==0){x||h.push({kind:"header",slug:y});for(const w of v)h.push({kind:"cmd",slug:y,cmd:w})}}return h}function bs(s,a,i){if(!i)return!0;const l=`${s} ${a.name} ${a.description}`.toLowerCase();let d=0;for(const x of i.split(/\s+/).join("")){const h=l.indexOf(x,d);if(h===-1)return!1;d=h+1}return!0}function ys({humans:s,agents:a,selfId:i="user"}){const l=s.filter(v=>v!==i),d=l.length>0,x=a.length>0;if(!d&&!x)return null;const h=ws(l),y=js(a);return e.jsxs("div",{"aria-live":"polite",className:"px-4 pt-1 text-xs text-shell-text-tertiary flex flex-col gap-0.5",children:[h&&e.jsx("span",{children:h}),y&&e.jsx("span",{className:"italic",children:y})]})}function ws(s){return s.length===0?null:s.length===1?`${s[0]} is typing…`:s.length===2?`${s[0]} and ${s[1]} are typing…`:`${s[0]} and ${s.length-1} others are typing…`}function js(s){return s.length===0?null:s.map(a=>`${a} is thinking…`).join(" · ")}function vs(s,a,i=1e3){const l=o.useRef(0);return o.useCallback(()=>{if(!s)return;const d=Date.now();d-l.current<i||(l.current=d,fetch(`/api/chat/channels/${encodeURIComponent(s)}/typing`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({author_id:a})}).catch(()=>{}))},[s,a,i])}function ks({onReact:s,onReplyInThread:a,onOverflow:i}){return e.jsxs("div",{role:"toolbar","aria-label":"Message actions",className:"inline-flex items-center gap-0.5 bg-shell-surface border border-white/10 rounded-md shadow-sm px-1",children:[e.jsx("button",{"aria-label":"Add reaction",onClick:s,className:"p-1 hover:bg-white/5",children:"😀"}),e.jsx("button",{"aria-label":"Reply in thread",onClick:a,className:"p-1 hover:bg-white/5",children:"💬"}),e.jsx("button",{"aria-label":"More",onClick:i,className:"p-1 hover:bg-white/5",children:"⋯"})]})}function Ns({replyCount:s,lastReplyAt:a,onOpen:i}){if(s===0)return null;const l=a?`💬 ${s} repl${s===1?"y":"ies"} · last reply ${Ss(a)}`:`💬 ${s} repl${s===1?"y":"ies"}`;return e.jsx("button",{onClick:i,className:"mt-1 px-2 py-0.5 text-xs text-sky-200 hover:bg-white/5 rounded","aria-label":"Open thread",children:l})}function Ss(s){const a=Date.now()/1e3,i=Math.max(0,a-s);return i<60?"just now":i<3600?`${Math.floor(i/60)}m ago`:i<86400?`${Math.floor(i/3600)}h ago`:`${Math.floor(i/86400)}d ago`}function Cs({channelId:s,parentId:a,onClose:i,onSend:l,isFullscreen:d=!1}){const[x,h]=o.useState(null),[y,v]=o.useState([]),[w,N]=o.useState(""),[g,C]=o.useState(null),[z,m]=o.useState(null),[E,_]=o.useState(!1),S=o.useRef(null);o.useEffect(()=>{const k=new AbortController;return C(null),fetch(`/api/chat/messages/${a}`,{signal:k.signal}).then(u=>{if(!u.ok)throw new Error(`parent fetch failed (${u.status})`);return u.json()}).then(u=>h(u)).catch(u=>{u.name!=="AbortError"&&C("couldn't load this thread")}),()=>k.abort()},[a]),o.useEffect(()=>{const k=new AbortController;return fetch(`/api/chat/channels/${s}/threads/${a}/messages`,{signal:k.signal}).then(u=>u.ok?u.json():{messages:[]}).then(u=>v(u.messages||[])).catch(u=>{u.name!=="AbortError"&&C("couldn't load this thread")}),()=>k.abort()},[s,a]);async function p(){const k=w.trim();if(!(!k||E)){_(!0),m(null);try{await l(k,[]),N("")}catch(u){m(u.message||"couldn't send reply")}finally{_(!1)}}}function T(k){k.key==="Enter"&&!k.shiftKey&&(k.preventDefault(),p())}return e.jsxs("div",{className:d?"fixed inset-0 z-50 bg-shell-surface flex flex-col":"fixed top-0 right-0 h-full w-[360px] bg-shell-surface border-l border-white/10 flex flex-col z-40",role:"complementary","aria-label":"Thread panel",style:d?{paddingTop:"env(safe-area-inset-top, 0px)"}:void 0,children:[e.jsxs("div",{className:"flex items-center justify-between px-4 py-3 border-b border-white/10",children:[e.jsx("span",{className:"font-semibold text-sm",children:"Thread"}),e.jsx("button",{"aria-label":d?"Back":"Close thread",onClick:i,className:"p-1 hover:bg-white/5 rounded",children:d?"◀":"✕"})]}),e.jsxs("div",{className:"flex-1 overflow-y-auto px-4 py-3 flex flex-col gap-3",children:[x&&e.jsxs("div",{className:"pb-3 border-b border-white/10",children:[e.jsx("div",{className:"text-xs text-white/50 mb-1",children:x.author_id}),e.jsx("div",{className:"text-sm",children:x.content})]}),y.map(k=>e.jsxs("div",{children:[e.jsx("div",{className:"text-xs text-white/50 mb-0.5",children:k.author_id}),e.jsx("div",{className:"text-sm",children:k.content})]},k.id)),g&&e.jsx("div",{role:"alert",className:"text-xs text-red-300",children:g})]}),e.jsxs("div",{className:"px-4 py-3 border-t border-white/10",children:[z&&e.jsx("div",{role:"alert",className:"text-xs text-red-300 mb-2",children:z}),e.jsx("textarea",{ref:S,value:w,onChange:k=>N(k.target.value),onKeyDown:T,placeholder:"Reply in thread…","aria-label":"Thread reply",rows:2,disabled:E,className:"w-full bg-white/5 rounded px-3 py-2 text-sm resize-none outline-none border border-white/10 focus:border-sky-400 disabled:opacity-50"})]})]})}function Es({items:s,onRemove:a,onRetry:i}){return s.length===0?null:e.jsx("div",{"aria-label":"Pending attachments",className:"px-4 py-2 border-t border-white/10 flex gap-2 flex-wrap",children:s.map(l=>e.jsxs("div",{className:"flex items-center gap-2 bg-white/5 rounded px-2 py-1 text-xs max-w-[220px]",children:[e.jsx("span",{className:"truncate",children:l.filename}),e.jsxs("span",{className:"opacity-50",children:[Math.max(1,Math.round(l.size/1024))," KB"]}),l.uploading&&e.jsx("span",{className:"opacity-70",children:"…"}),l.error&&e.jsx("button",{"aria-label":"Retry upload",onClick:()=>i(l.id),className:"text-red-300",children:"retry"}),e.jsx("button",{"aria-label":`Remove ${l.filename}`,onClick:()=>a(l.id),className:"opacity-70 hover:opacity-100",children:"×"})]},l.id))})}function _s({images:s,startIndex:a,onClose:i}){const[l,d]=o.useState(a);o.useEffect(()=>{const h=y=>{y.key==="Escape"&&i(),y.key==="ArrowLeft"&&d(v=>Math.max(0,v-1)),y.key==="ArrowRight"&&d(v=>Math.min(s.length-1,v+1))};return document.addEventListener("keydown",h),()=>document.removeEventListener("keydown",h)},[s.length,i]);const x=s[l];return e.jsxs("div",{role:"dialog","aria-label":"Image viewer",className:"fixed inset-0 z-50 bg-black/80 flex items-center justify-center",onClick:i,children:[e.jsx("img",{src:x.url,alt:x.filename,className:"max-w-[90vw] max-h-[90vh]",onClick:h=>h.stopPropagation()}),e.jsxs("div",{className:"absolute top-4 right-4 flex gap-2",children:[e.jsx("a",{href:x.url,download:x.filename,onClick:h=>h.stopPropagation(),className:"bg-white/10 hover:bg-white/20 rounded px-3 py-1 text-sm",children:"Download"}),e.jsx("button",{onClick:i,className:"bg-white/10 hover:bg-white/20 rounded px-3 py-1 text-sm",children:"Close"})]}),s.length>1&&e.jsxs("div",{className:"absolute bottom-4 text-white/70 text-xs",children:[l+1," / ",s.length]})]})}function $s({attachments:s}){const[a,i]=o.useState(null);if(!(s!=null&&s.length))return null;const l=s.filter(h=>{var y;return(y=h.mime_type)==null?void 0:y.startsWith("image/")}),d=s.filter(h=>{var y;return!((y=h.mime_type)!=null&&y.startsWith("image/"))}),x=l.length>1?"grid grid-cols-2 gap-1 max-w-md":"";return e.jsxs("div",{className:"flex flex-col gap-2 mt-1",children:[l.length>0&&e.jsx("div",{className:x,children:l.slice(0,4).map((h,y)=>e.jsxs("button",{onClick:()=>i(y),className:"relative block",children:[e.jsx("img",{src:h.url,alt:h.filename,className:l.length===1?"max-w-[560px] max-h-[400px] rounded":"object-cover w-full h-32 rounded"}),l.length>4&&y===3&&e.jsxs("span",{className:"absolute inset-0 bg-black/60 flex items-center justify-center text-white",children:["+",l.length-4," more"]})]},h.url))}),d.length>0&&e.jsx("div",{className:"flex flex-col gap-1",children:d.map(h=>e.jsxs("a",{href:h.url,target:"_blank",rel:"noreferrer",className:"flex items-center gap-2 bg-white/5 hover:bg-white/10 rounded px-2 py-1 text-sm max-w-sm",children:[e.jsx("span",{"aria-hidden":!0,children:"📄"}),e.jsx("span",{className:"truncate",children:h.filename}),e.jsxs("span",{className:"ml-auto text-xs opacity-60",children:[Math.max(1,Math.round(h.size/1024))," KB"]})]},h.url))}),a!==null&&e.jsx(_s,{images:l,startIndex:a,onClose:()=>i(null)})]})}async function kt(s){if(s.ok)return;let a=null;try{a=await s.json()}catch{}throw new Error((a==null?void 0:a.error)||`HTTP ${s.status}`)}async function Ae(s,a){const i=new FormData;i.append("file",s),a&&i.append("channel_id",a);const l=await fetch("/api/chat/upload",{method:"POST",body:i});await kt(l);const d=await l.json();return{filename:d.filename,mime_type:d.mime_type||d.content_type||"application/octet-stream",size:d.size,url:d.url,source:"disk"}}async function Ts(s){const a=await fetch("/api/chat/attachments/from-path",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});return await kt(a),a.json()}function As(){const[s,a]=o.useState(null);return{openThread:s,openThreadFor:(i,l)=>a({channelId:i,parentId:l}),closeThread:()=>a(null)}}function Ms(s,a){const i=a?`?path=${encodeURIComponent(a)}`:"";if(s==="/workspaces/user")return`/api/workspace/files${i}`;if(s.startsWith("/workspaces/agent/")){const l=s.slice(18);return`/api/agents/${encodeURIComponent(l)}/workspace/files${i}`}return`/api/workspace/files${i}`}function bt({root:s,onSelect:a,multi:i=!1}){const[l,d]=o.useState(""),[x,h]=o.useState([]),[y,v]=o.useState(new Set),[w,N]=o.useState(!1),[g,C]=o.useState(null);o.useEffect(()=>{let S=!1;return N(!0),C(null),fetch(Ms(s,l)).then(async p=>{if(!p.ok)throw new Error(`HTTP ${p.status}`);return p.json()}).then(p=>{if(S)return;const k=(Array.isArray(p)?p:[]).map(u=>({name:u.name,type:u.is_dir?"folder":"file",size:u.size,modified:u.modified?new Date(u.modified*1e3).toISOString():void 0}));k.sort((u,M)=>u.type!==M.type?u.type==="folder"?-1:1:u.name.localeCompare(M.name)),h(k)}).catch(p=>{S||(C(p instanceof Error?p.message:"Failed to load"),h([]))}).finally(()=>{S||N(!1)}),()=>{S=!0}},[s,l]);function z(S){const p=l?`${l}/${S}`:S;return`${s}/${p}`}function m(S){d(p=>p?`${p}/${S}`:S),v(new Set)}function E(S){const p=z(S);if(!i){a(p);return}v(T=>{const k=new Set(T);return k.has(p)?k.delete(p):k.add(p),a(Array.from(k)),k})}function _(){d(S=>{const p=S.split("/").filter(Boolean);return p.pop(),p.join("/")}),v(new Set)}return e.jsxs("div",{className:"vfs-browser",role:"region","aria-label":"File browser",children:[l&&e.jsx("button",{onClick:_,"aria-label":"Go up one folder",style:{marginBottom:4},children:"↑ .."}),w&&e.jsx("p",{children:"Loading…"}),g&&e.jsxs("p",{role:"alert",children:["Error: ",g]}),!w&&!g&&x.length===0&&e.jsx("p",{children:"Empty folder"}),e.jsx("ul",{style:{listStyle:"none",padding:0,margin:0},children:x.map(S=>{const p=z(S.name),T=y.has(p);return e.jsx("li",{children:e.jsxs("button",{onClick:()=>S.type==="folder"?m(S.name):E(S.name),"aria-selected":i?T:void 0,style:{fontWeight:T?"bold":void 0},children:[S.type==="folder"?"📁 ":"📄 ",S.name]})},S.name)})})]})}function zs({sources:s,accept:a,multi:i=!1,onPick:l,onCancel:d}){const[x,h]=o.useState(s[0]??"disk"),[y,v]=o.useState([]),[w,N]=o.useState([]),[g,C]=o.useState(null),z=o.useRef(null);o.useEffect(()=>{s.includes("agent-workspace")&&fetch("/api/agents").then(p=>p.json()).then(p=>N(Array.isArray(p)?p:[])).catch(()=>{})},[s]),o.useEffect(()=>{const p=T=>{T.key==="Escape"&&(T.preventDefault(),d())};return document.addEventListener("keydown",p),()=>document.removeEventListener("keydown",p)},[d]);const m=p=>{if(!p)return;const T=[];for(const k of Array.from(p))T.push({source:"disk",file:k});v(k=>i?[...k,...T]:T)},E=p=>{const k=(Array.isArray(p)?p:[p]).map(u=>({source:"workspace",path:u}));v(u=>i?[...u.filter(M=>M.source!=="workspace"),...k]:k)},_=p=>{if(!g)return;const k=(Array.isArray(p)?p:[p]).map(u=>({source:"agent-workspace",slug:g,path:u}));v(u=>i?[...u.filter(M=>M.source!=="agent-workspace"),...k]:k)},S=()=>l(y);return e.jsx("div",{role:"dialog","aria-modal":"true","aria-label":"Pick a file",className:"fixed inset-0 z-50 flex items-center justify-center bg-black/60",children:e.jsxs("div",{className:"bg-shell-surface border border-white/10 rounded-xl w-[720px] h-[540px] flex flex-col overflow-hidden",children:[e.jsxs("div",{role:"tablist",className:"flex border-b border-white/10",children:[s.includes("disk")&&e.jsx("button",{role:"tab","aria-selected":x==="disk",className:`px-4 py-2 text-sm ${x==="disk"?"border-b-2 border-sky-400":"opacity-70"}`,onClick:()=>h("disk"),children:"Disk"}),s.includes("workspace")&&e.jsx("button",{role:"tab","aria-selected":x==="workspace",className:`px-4 py-2 text-sm ${x==="workspace"?"border-b-2 border-sky-400":"opacity-70"}`,onClick:()=>h("workspace"),children:"My workspace"}),s.includes("agent-workspace")&&e.jsx("button",{role:"tab","aria-selected":x==="agent-workspace",className:`px-4 py-2 text-sm ${x==="agent-workspace"?"border-b-2 border-sky-400":"opacity-70"}`,onClick:()=>h("agent-workspace"),children:"Agent workspaces"})]}),e.jsxs("div",{className:"flex-1 overflow-hidden",children:[x==="disk"&&e.jsxs("div",{className:"p-6 flex items-center justify-center",children:[e.jsx("input",{ref:z,type:"file",className:"hidden",multiple:i,accept:a,onChange:p=>m(p.target.files)}),e.jsx("button",{onClick:()=>{var p;return(p=z.current)==null?void 0:p.click()},className:"px-4 py-2 bg-sky-500/20 text-sky-200 rounded",children:"Choose files from disk"}),y.filter(p=>p.source==="disk").length>0&&e.jsxs("div",{className:"ml-6 text-xs text-shell-text-tertiary",children:[y.length," file(s) queued"]})]}),x==="workspace"&&e.jsx(bt,{root:"/workspaces/user",onSelect:E,multi:i}),x==="agent-workspace"&&e.jsxs("div",{className:"h-full flex flex-col",children:[e.jsx("div",{className:"p-2 border-b border-white/10",children:e.jsxs("select",{value:g??"",onChange:p=>C(p.target.value||null),className:"bg-white/5 border border-white/10 rounded px-2 py-1 text-sm",children:[e.jsx("option",{value:"",children:"Pick an agent…"}),w.map(p=>e.jsxs("option",{value:p.name,children:["@",p.name]},p.name))]})}),g&&e.jsx(bt,{root:`/workspaces/agent/${g}`,onSelect:_,multi:i})]})]}),e.jsxs("div",{className:"border-t border-white/10 p-2 flex items-center justify-end gap-2 text-sm",children:[e.jsxs("span",{className:"opacity-60 mr-auto",children:[y.length," selected"]}),e.jsx("button",{onClick:d,className:"px-3 py-1 opacity-70 hover:opacity-100",children:"Cancel"}),e.jsxs("button",{onClick:S,disabled:y.length===0,className:"px-3 py-1 bg-sky-500/30 text-sky-200 rounded disabled:opacity-40",children:["Select (",y.length,")"]})]})]})})}function Ps(s){return new Promise(a=>{const i=document.createElement("div");document.body.appendChild(i);const l=Zt.createRoot(i),d=()=>{l.unmount(),i.remove()};l.render(es.createElement(zs,{sources:s.sources,accept:s.accept,multi:s.multi,onPick:x=>{d(),a(x)},onCancel:()=>{d(),a([])}}))})}function Rs({isOwn:s,isHuman:a,isPinned:i=!1,onEdit:l,onDelete:d,onCopyLink:x,onPin:h,onMarkUnread:y,onClose:v}){const w=o.useRef(null);o.useEffect(()=>{var C;const g=(C=w.current)==null?void 0:C.querySelector('[role="menuitem"]');g==null||g.focus()},[]);const N=g=>{var E,_,S,p,T;const C=Array.from(((E=w.current)==null?void 0:E.querySelectorAll('[role="menuitem"]'))||[]),z=document.activeElement,m=z?C.indexOf(z):-1;g.key==="ArrowDown"?(g.preventDefault(),(_=C[Math.min(C.length-1,m+1)])==null||_.focus()):g.key==="ArrowUp"?(g.preventDefault(),(S=C[Math.max(0,m-1)])==null||S.focus()):g.key==="Home"?(g.preventDefault(),(p=C[0])==null||p.focus()):g.key==="End"?(g.preventDefault(),(T=C[C.length-1])==null||T.focus()):g.key==="Escape"&&(g.preventDefault(),v==null||v())};return e.jsxs("div",{ref:w,role:"menu","aria-label":"Message overflow menu",onKeyDown:N,className:"bg-shell-surface border border-white/10 rounded-md shadow-lg py-1 min-w-[160px] text-sm",children:[s&&e.jsx("button",{role:"menuitem",onClick:l,className:"block w-full text-left px-3 py-1.5 hover:bg-white/5 focus:bg-white/5 focus:outline-none",children:"Edit"}),s&&e.jsx("button",{role:"menuitem",onClick:d,className:"block w-full text-left px-3 py-1.5 hover:bg-white/5 focus:bg-white/5 focus:outline-none text-red-300",children:"Delete"}),e.jsx("button",{role:"menuitem",onClick:x,className:"block w-full text-left px-3 py-1.5 hover:bg-white/5 focus:bg-white/5 focus:outline-none",children:"Copy link"}),a&&e.jsx("button",{role:"menuitem",onClick:h,className:"block w-full text-left px-3 py-1.5 hover:bg-white/5 focus:bg-white/5 focus:outline-none",children:i?"Unpin":"Pin"}),e.jsx("button",{role:"menuitem",onClick:y,className:"block w-full text-left px-3 py-1.5 hover:bg-white/5 focus:bg-white/5 focus:outline-none",children:"Mark unread"})]})}const Ds=80;function Os({open:s,onClose:a,children:i,labelledBy:l,dragHandle:d=!0}){const x=o.useRef(null),[h,y]=o.useState(0);if(o.useEffect(()=>{if(!s)return;const w=N=>{N.key==="Escape"&&(N.preventDefault(),a())};return document.addEventListener("keydown",w),()=>document.removeEventListener("keydown",w)},[s,a]),o.useEffect(()=>{var N;if(!s)return;const w=(N=x.current)==null?void 0:N.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');w==null||w.focus()},[s]),!s)return null;const v=w=>{const N=w.clientY,g=w.currentTarget;g.setPointerCapture(w.pointerId);const C=E=>{const _=Math.max(0,E.clientY-N);y(_)},z=E=>{const _=Math.max(0,E.clientY-N);g.releasePointerCapture(w.pointerId),g.removeEventListener("pointermove",C),g.removeEventListener("pointerup",z),g.removeEventListener("pointercancel",m),_>Ds&&a(),y(0)},m=()=>{g.releasePointerCapture(w.pointerId),g.removeEventListener("pointermove",C),g.removeEventListener("pointerup",z),g.removeEventListener("pointercancel",m),y(0)};g.addEventListener("pointermove",C),g.addEventListener("pointerup",z),g.addEventListener("pointercancel",m)};return e.jsxs(e.Fragment,{children:[e.jsx("div",{"data-testid":"bottom-sheet-backdrop",className:"fixed inset-0 z-50 bg-black/60",onClick:a}),e.jsxs("div",{ref:x,role:"dialog","aria-modal":"true","aria-labelledby":l,className:"fixed bottom-0 inset-x-0 z-50 bg-shell-surface rounded-t-xl border-t border-white/10 shadow-2xl max-h-[85vh] overflow-y-auto",style:{paddingBottom:"env(safe-area-inset-bottom, 0px)",transform:`translateY(${h}px)`,transition:h===0?"transform 0.2s ease-out":"none"},children:[d&&e.jsx("div",{"data-testid":"bottom-sheet-handle",onPointerDown:v,className:"flex justify-center py-2 cursor-grab active:cursor-grabbing touch-none",children:e.jsx("div",{className:"w-10 h-1 bg-white/20 rounded-full"})}),i]})]})}function Ls({initial:s,onSave:a,onCancel:i}){const[l,d]=o.useState(s);return e.jsx("textarea",{autoFocus:!0,value:l,onChange:x=>d(x.target.value),onKeyDown:x=>{if(x.key==="Escape")x.preventDefault(),i();else if(x.key==="Enter"&&!x.shiftKey){x.preventDefault();const h=l.trim();h?a(h):i()}},"aria-label":"Edit message",rows:1,className:"w-full bg-white/5 border border-white/10 rounded px-2 py-1 text-sm"})}function Is(){return e.jsx("span",{className:"text-white/40 italic text-sm",children:"This message was deleted"})}function Us({count:s,onClick:a}){return s===0?null:e.jsxs("button",{onClick:a,className:"ml-1 px-1.5 py-0.5 text-xs bg-white/5 hover:bg-white/10 rounded opacity-70 hover:opacity-100","aria-label":`Pinned messages (${s})`,children:["📌 ",s]})}function Fs({pins:s,onJumpTo:a,onClose:i}){return e.jsxs("div",{role:"dialog","aria-label":"Pinned messages",className:"absolute top-full right-0 mt-1 w-[320px] max-h-[400px] overflow-y-auto bg-shell-surface border border-white/10 rounded-md shadow-lg z-40",children:[e.jsxs("header",{className:"flex items-center justify-between px-3 py-2 border-b border-white/10",children:[e.jsxs("span",{className:"text-xs font-semibold",children:["Pinned (",s.length,")"]}),e.jsx("button",{onClick:i,"aria-label":"Close",className:"text-sm opacity-70 hover:opacity-100",children:"×"})]}),s.length===0?e.jsx("div",{className:"p-4 text-sm text-white/50",children:"No pinned messages yet."}):e.jsx("ul",{className:"divide-y divide-white/5",children:s.map(l=>e.jsxs("li",{className:"p-2 text-sm",children:[e.jsxs("div",{className:"text-xs opacity-60 mb-0.5",children:["@",l.author_id]}),e.jsx("div",{className:"line-clamp-2",children:l.content}),e.jsx("button",{onClick:()=>a(l.id),className:"mt-1 text-xs text-sky-300 hover:text-sky-200",children:"Jump to →"})]},l.id))})]})}function Bs({authorId:s,onApprove:a}){return e.jsxs("div",{className:"mt-1 flex items-center gap-2 text-xs",children:[e.jsxs("span",{className:"text-white/60",children:["@",s," wants to pin this"]}),e.jsx("button",{onClick:a,className:"px-2 py-0.5 bg-sky-500/20 text-sky-200 rounded hover:bg-sky-500/30","aria-label":`Pin this message from ${s}`,children:"📌 Pin this"})]})}async function te(s){if(s.ok)return;let a=null;try{a=await s.json()}catch{}throw new Error((a==null?void 0:a.error)||`HTTP ${s.status}`)}async function yt(s){const a=await fetch(`/api/chat/messages/${s}/pin`,{method:"POST"});await te(a)}async function Hs(s){const a=await fetch(`/api/chat/messages/${s}/pin`,{method:"DELETE"});await te(a)}async function Me(s){const a=await fetch(`/api/chat/channels/${s}/pins`);return await te(a),(await a.json()).pins||[]}async function Js(s,a){const i=await fetch(`/api/chat/messages/${s}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({content:a})});return await te(i),i.json()}async function Ws(s){const a=await fetch(`/api/chat/messages/${s}`,{method:"DELETE"});await te(a)}async function Ks(s,a){const i=await fetch(`/api/chat/channels/${s}/read-cursor/rewind`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({before_message_id:a})});await te(i)}function qs(s,a,i,l){return a==="user"||i.some(d=>d.name===s)?"active":l.some(d=>{var x;return d.archived_slug===s||((x=d.original)==null?void 0:x.name)===s})?"archived":"removed"}function Vs(s){const a=Date.now()-new Date(s).getTime(),i=Math.floor(a/6e4);if(i<1)return"now";if(i<60)return`${i}m ago`;const l=Math.floor(i/60);if(l<24)return`${l}h ago`;const d=Math.floor(l/24);return d<7?`${d}d ago`:new Date(s).toLocaleDateString()}function Ys(s){const a=[],i=/(\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`)/g;let l=0,d,x=0;for(;(d=i.exec(s))!==null;)d.index>l&&a.push(s.slice(l,d.index)),d[2]?a.push(e.jsx("strong",{className:"font-semibold",children:d[2]},x++)):d[3]?a.push(e.jsx("em",{className:"italic",children:d[3]},x++)):d[4]&&a.push(e.jsx("code",{className:"bg-white/10 px-1.5 py-0.5 rounded text-[13px] font-mono",children:d[4]},x++)),l=d.index+d[0].length;return l<s.length&&a.push(s.slice(l)),a}const Gs=["👍","❤️","😂","🎉","🤔","👀","🚀","✅"];function rn({windowId:s,title:a}){var Ze,et,tt;const i=ls(),{keyboardInset:l}=ds(),[d,x]=o.useState([]),[h,y]=o.useState([]),[v,w]=o.useState(!1),[N,g]=o.useState([]),[C,z]=o.useState([]),[m,E]=o.useState(null),[_,S]=o.useState([]),[p,T]=o.useState({}),[k,u]=o.useState(""),[M,V]=o.useState("disconnected"),[Nt,Y]=o.useState(!1),[ze,Pe]=o.useState(null),[be,ye]=o.useState(null),[O,W]=o.useState({name:"",type:"topic",description:""}),[se,Re]=o.useState(null),[St,we]=o.useState(!1),[K,ne]=o.useState(null),[H,De]=o.useState(null),[Ct,Et]=o.useState({}),[_t,je]=o.useState([]),[$t,ae]=o.useState([]),[Oe,D]=o.useState(null),[Tt,Le]=o.useState(null),[G,L]=o.useState([]),[he,B]=o.useState(null),[At,ve]=o.useState(null),[Mt,ke]=o.useState(!1),[ue,re]=o.useState([]),[zt,Pt]=o.useState(null),{openThread:ie,openThreadFor:Rt,closeThread:Ie}=As(),R=o.useRef(null),Ue=o.useRef(null),Fe=o.useRef(null),I=o.useRef(null),Ne=o.useRef(null),Se=o.useRef(0),X=o.useRef(!0),Ce=o.useRef(1e3),q=o.useRef(null),Q=o.useCallback(async()=>{try{const[t,n]=await Promise.all([fetch("/api/chat/channels"),fetch("/api/chat/unread")]);if(t.ok){const r=await t.json();x(r.channels??[])}if(n.ok){const r=await n.json();T(r.unread??{})}}catch{}},[]),le=o.useCallback(async()=>{try{const t=await fetch("/api/chat/channels?archived=true");if(t.ok){const n=await t.json();y(n.channels??[])}}catch{}},[]),pe=o.useCallback(async()=>{try{const[t,n]=await Promise.all([fetch("/api/agents"),fetch("/api/agents/archived")]);if(t.ok&&(t.headers.get("content-type")??"").includes("application/json")){const c=await t.json();Array.isArray(c)&&g(c)}if(n.ok&&(n.headers.get("content-type")??"").includes("application/json")){const c=await n.json();Array.isArray(c)&&z(c)}}catch{}},[]),Be=o.useCallback(async t=>{try{const n=await fetch(`/api/chat/channels/${t}/messages?limit=50`);if(n.ok){const r=await n.json();S(r.messages??[]),X.current=!0}}catch{}},[]),He=o.useCallback(async t=>{try{await fetch(`/api/chat/channels/${t}/mark-read`,{method:"POST"}),T(n=>{const r={...n};return delete r[t],r})}catch{}},[]),Ee=o.useCallback(()=>{if(R.current&&R.current.readyState<=1)return;V("connecting");const t=window.location.protocol==="https:"?"wss:":"ws:",n=new WebSocket(`${t}//${window.location.host}/ws/chat`);n.onopen=()=>{V("connected"),Ce.current=1e3,q.current&&n.send(JSON.stringify({type:"join",channel_id:q.current}))},n.onmessage=r=>{try{const c=JSON.parse(r.data);if(c.type==="typing"&&c.kind==="human"){je(f=>f.includes(c.slug)?f:[...f,c.slug]),setTimeout(()=>je(f=>f.filter(b=>b!==c.slug)),3500);return}if(c.type==="thinking"){c.state==="start"?ae(f=>f.includes(c.slug)?f:[...f,c.slug]):ae(f=>f.filter(b=>b!==c.slug));return}switch(c.type){case"message":S(f=>f.some(b=>b.id===c.id)?f:[...f,c]),c.channel_id!==q.current&&T(f=>({...f,[c.channel_id]:(f[c.channel_id]??0)+1}));break;case"message_delta":S(f=>f.map(b=>b.id===c.message_id?{...b,content:b.content+(c.delta??""),state:"streaming"}:b));break;case"message_state":S(f=>f.map(b=>b.id===c.message_id?{...b,state:c.state}:b));break;case"typing":if((c.user_type??"user")!=="agent")break;ae(f=>f.includes(c.user_id)?f:[...f,c.user_id]),setTimeout(()=>{ae(f=>f.filter(b=>b!==c.user_id))},5e3);break;case"reaction_update":S(f=>f.map(b=>b.id===c.message_id?{...b,reactions:c.reactions}:b));break;case"message_edit":S(f=>f.map(b=>b.id===c.message_id?{...b,...c.content!==void 0&&{content:c.content},...c.edited_at!==void 0&&{edited_at:c.edited_at},...c.metadata!==void 0&&{metadata:c.metadata}}:b));break;case"message_delete":S(f=>f.map(b=>b.id===c.message_id?{...b,deleted_at:c.deleted_at??Date.now()/1e3}:b));break}}catch{}},n.onclose=()=>{V("disconnected"),R.current=null;const r=Ce.current;Ce.current=Math.min(r*2,3e4),setTimeout(Ee,r)},n.onerror=()=>{n.close()},R.current=n},[]);o.useEffect(()=>(Q(),le(),pe(),Ee(),()=>{R.current&&(R.current.onclose=null,R.current.close())}),[Q,le,pe,Ee]),o.useEffect(()=>{fetch("/api/auth/me").then(t=>t.ok?t.json():null).then(t=>{t!=null&&t.id&&Pt(t.id)}).catch(()=>{})},[]),o.useEffect(()=>{let t=!1;const n=async r=>{const c=r.detail;if(c!=null&&c.channelId&&!t&&(E(c.channelId),c.prefillPromptName))try{const f=await fetch(`/api/admin-prompts/${encodeURIComponent(c.prefillPromptName)}`,{headers:{Accept:"application/json"}});if(t)return;if(f.ok&&(f.headers.get("content-type")??"").includes("application/json")){const $=await f.json();if(t)return;u($.body??""),Re({promptName:c.prefillPromptName,agentName:c.prefillAgent}),setTimeout(()=>{var P;t||(P=I.current)==null||P.focus()},150)}}catch{}};return window.addEventListener("taos:open-messages",n),()=>{t=!0,window.removeEventListener("taos:open-messages",n)}},[]),o.useEffect(()=>{var t,n;m&&(q.current&&q.current!==m&&((t=R.current)==null?void 0:t.readyState)===1&&R.current.send(JSON.stringify({type:"leave",channel_id:q.current})),q.current=m,((n=R.current)==null?void 0:n.readyState)===1&&R.current.send(JSON.stringify({type:"join",channel_id:m})),Be(m),He(m),je([]),ae([]))},[m,Be,He]);const Je=o.useRef(null);o.useEffect(()=>{if(!m||_.length===0)return;const n=new URLSearchParams(window.location.search).get("msg");if(!n||!/^[a-zA-Z0-9_-]{1,64}$/.test(n))return;const r=`${m}:${n}`;if(Je.current===r)return;const c=document.querySelector(`[data-message-id="${n}"]`);c&&(Je.current=r,c.scrollIntoView({behavior:"smooth",block:"center"}),c.classList.add("data-highlight"),setTimeout(()=>c.classList.remove("data-highlight"),2e3))},[m,_.length]),o.useEffect(()=>{if(!m){re([]);return}Me(m).then(t=>re(t)).catch(()=>re([]))},[m]),o.useEffect(()=>{let t=!0;return fetch("/api/frameworks/slash-commands").then(n=>n.json()).then(n=>{t&&Et(n||{})}).catch(()=>{}),()=>{t=!1}},[m]),o.useEffect(()=>{var t;X.current&&((t=Ue.current)==null||t.scrollIntoView({behavior:"smooth"}))},[_]);const Dt=()=>{const t=Fe.current;if(!t)return;const n=t.scrollHeight-t.scrollTop-t.clientHeight<60;X.current=n},Ot=vs(m,"user"),We=k.startsWith("/"),Lt=We&&k.slice(1).split(/\s/,1)[0]||"",It=()=>{Ie(),we(!0)},Ke=(t,n)=>{we(!1),Rt(t,n)},qe=async()=>{const t=k.trim();if(!t&&G.length===0||!m)return;if(G.some(r=>r.uploading)){D("waiting for uploads to finish…");return}const n=G.filter(r=>r.record&&!r.error).map(r=>r.record);if(n.length>0)try{const r=await fetch("/api/chat/messages",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({channel_id:m,author_id:"user",author_type:"user",content:t,content_type:"text",attachments:n})});if(!r.ok){const c=await r.json().catch(()=>({}));D(c.error||"couldn't send message");return}u(""),L([]),I.current&&(I.current.style.height="auto"),X.current=!0;return}catch(r){D(r.message||"send failed");return}if(t){if(t.startsWith("/"))try{const r=await fetch("/api/chat/messages",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({channel_id:m,content:t})});if(r.status===400){const c=await r.json().catch(()=>({}));D(c.error||"couldn't send message");return}if(r.ok&&(await r.json().catch(()=>({}))).handled){D(null),u(""),X.current=!0,I.current&&(I.current.style.height="auto");return}}catch{}if(D(null),R.current&&R.current.readyState===1)R.current.send(JSON.stringify({type:"message",channel_id:m,content:t}));else try{const r=await fetch("/api/chat/messages",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({channel_id:m,author_id:"user",author_type:"user",content:t,content_type:"text"})});if(!r.ok){const c=await r.json().catch(()=>({}));D(c.error||"couldn't send message");return}}catch(r){D(r.message||"send failed");return}u(""),X.current=!0,I.current&&(I.current.style.height="auto")}},Ut=t=>{var r;u(t),I.current&&(I.current.style.height="auto",I.current.style.height=Math.min(I.current.scrollHeight,120)+"px");const n=Date.now();m&&((r=R.current)==null?void 0:r.readyState)===1&&n-Se.current>3e3&&(R.current.send(JSON.stringify({type:"typing",channel_id:m})),Se.current=n),Ne.current&&clearTimeout(Ne.current),Ne.current=setTimeout(()=>{Se.current=0},4e3),Ot()},Ft=t=>{t.key==="Enter"&&!t.shiftKey&&(t.preventDefault(),qe())},Bt=async()=>{const t=await Ps({sources:["disk","workspace","agent-workspace"],multi:!0});for(const n of t){const r=Math.random().toString(36).slice(2),c=n.source==="disk"?n.file.name:n.path.split("/").pop()||"",f=n.source==="disk"?n.file.size:0;L(b=>[...b,{id:r,filename:c,size:f,uploading:!0}]);try{const b=n.source==="disk"?await Ae(n.file,m??void 0):await Ts({path:n.path,source:n.source,slug:n.source==="agent-workspace"?n.slug:void 0});L($=>$.map(P=>P.id===r?{...P,record:b,uploading:!1}:P))}catch(b){L($=>$.map(P=>P.id===r?{...P,uploading:!1,error:b.message}:P))}}},Ve=(t,n)=>{var r;((r=R.current)==null?void 0:r.readyState)===1&&R.current.send(JSON.stringify({type:"reaction",message_id:t,emoji:n})),Pe(null)},Ye=async()=>{if(O.name.trim())try{const t=await fetch("/api/chat/channels",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:O.name.trim(),type:O.type,description:O.description.trim()||void 0})});if(t.ok){const n=await t.json();x(r=>[...r,n]),E(n.id),Y(!1),W({name:"",type:"topic",description:""})}}catch{}},Ge=o.useCallback(async(t,n)=>{var c,f,b,$;const r=(f=(c=h.find(P=>P.id===t))==null?void 0:c.settings)==null?void 0:f.archived_agent_id;if(r){const P=C.find(F=>F.id===r);if(P){if(!window.confirm(`Restore agent "${((b=P.original)==null?void 0:b.display_name)||(($=P.original)==null?void 0:$.name)||P.archived_slug}"?`))return;try{const F=await fetch(`/api/agents/archived/${r}/restore`,{method:"POST"});if(!F.ok){const me=await F.json().catch(()=>({}));window.alert(`Restore failed: ${me.error??F.status}`);return}await Q(),await le(),await pe()}catch(F){window.alert(`Network error: ${String(F)}`)}}else window.alert("Agent entry missing — delete only.")}else window.alert(`Cannot restore channel "${n}": no associated agent found.`)},[h,C,Q,le,pe]),Xe=o.useCallback(async t=>{if(window.confirm("Permanently delete this chat? All messages are erased. This cannot be undone."))try{const n=await fetch(`/api/chat/channels/${t}`,{method:"DELETE"});if(!n.ok){const r=await n.json().catch(()=>({}));window.alert(`Delete failed: ${r.error??n.status}`);return}y(r=>r.filter(c=>c.id!==t)),m===t&&E(null)}catch(n){window.alert(`Network error: ${String(n)}`)}},[m,le]),Ht=t=>{ve(t),B(null)},Jt=async(t,n)=>{try{await Js(t,n),ve(null)}catch(r){D(r.message)}},Wt=async t=>{if(B(null),!!window.confirm("Delete this message?"))try{await Ws(t)}catch(n){D(n.message)}},Kt=async t=>{if(B(null),!m)return;const n=`${window.location.origin}/chat/${m}?msg=${t}`;try{await navigator.clipboard.writeText(n)}catch{}},qt=async t=>{B(null);const n=ue.some(r=>r.id===t.id);try{if(n?await Hs(t.id):await yt(t.id),m){const r=await Me(m);re(r)}}catch(r){D(r.message)}},Vt=async t=>{if(B(null),!!m)try{await Ks(m,t)}catch(n){D(n.message)}},Yt=async t=>{try{if(await yt(t),m){const n=await Me(m);re(n)}}catch(n){D(n.message)}},_e={dm:d.filter(t=>t.type==="dm"),topic:d.filter(t=>t.type==="topic"),group:d.filter(t=>t.type==="group")},j=[...d,...h].find(t=>t.id===m),J=((Ze=j==null?void 0:j.settings)==null?void 0:Ze.archived)===!0,Qe=[{label:"Direct Messages",icon:e.jsx(ut,{size:13}),items:_e.dm},{label:"Topics",icon:e.jsx(ht,{size:13}),items:_e.topic},{label:"Groups",icon:e.jsx(Te,{size:13}),items:_e.group}],Gt=i?e.jsxs("div",{style:{padding:"8px 0 16px"},children:[e.jsx("div",{style:{padding:"0 20px 8px",fontSize:11,display:"flex",alignItems:"center",gap:6},children:M==="connected"?e.jsxs(e.Fragment,{children:[e.jsx(fe,{size:11,style:{color:"#34d399"}}),e.jsx("span",{style:{color:"rgba(52,211,153,0.8)"},children:"Connected"})]}):M==="connecting"?e.jsxs(e.Fragment,{children:[e.jsx(fe,{size:11,style:{color:"#fbbf24"}}),e.jsx("span",{style:{color:"rgba(251,191,36,0.8)"},children:"Connecting…"})]}):e.jsxs(e.Fragment,{children:[e.jsx(lt,{size:11,style:{color:"#f87171"}}),e.jsx("span",{style:{color:"rgba(248,113,113,0.8)"},children:"Offline"})]})}),Qe.map(t=>e.jsxs("div",{style:{marginBottom:20},children:[e.jsxs("div",{style:{fontSize:12,textTransform:"uppercase",letterSpacing:.5,color:"rgba(255,255,255,0.45)",padding:"0 20px 6px",fontWeight:600,display:"flex",alignItems:"center",gap:6},children:[t.icon," ",t.label]}),t.items.length===0?e.jsx("div",{style:{padding:"0 20px",fontSize:12,color:"rgba(255,255,255,0.2)",fontStyle:"italic"},children:"None yet"}):e.jsx("div",{style:{margin:"0 12px",borderRadius:16,background:"rgba(255,255,255,0.05)",border:"1px solid rgba(255,255,255,0.08)",overflow:"hidden"},children:t.items.map((n,r,c)=>e.jsxs("button",{type:"button",onClick:()=>E(n.id),"aria-label":`Channel ${n.name}`,style:{display:"flex",alignItems:"center",gap:10,width:"100%",padding:"14px 16px",background:m===n.id?"rgba(59,130,246,0.15)":"none",border:"none",borderBottom:r===c.length-1?"none":"1px solid rgba(255,255,255,0.06)",cursor:"pointer",color:"inherit",textAlign:"left"},children:[e.jsx("span",{style:{flex:1,fontSize:15,fontWeight:400,color:"rgba(255,255,255,0.9)",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:n.name}),(p[n.id]??0)>0&&e.jsx("span",{style:{background:"#3b82f6",color:"#fff",fontSize:10,fontWeight:700,borderRadius:9999,minWidth:18,height:18,display:"flex",alignItems:"center",justifyContent:"center",padding:"0 4px"},children:p[n.id]}),e.jsx($e,{size:16,style:{color:"rgba(255,255,255,0.25)",flexShrink:0}})]},n.id))})]},t.label)),h.length>0&&e.jsxs("div",{style:{marginBottom:20},children:[e.jsxs("button",{type:"button",onClick:()=>w(t=>!t),"aria-expanded":v,"aria-controls":"archived-channels-mobile",style:{fontSize:12,textTransform:"uppercase",letterSpacing:.5,color:"rgba(255,255,255,0.35)",padding:"0 20px 6px",fontWeight:600,display:"flex",alignItems:"center",gap:6,background:"none",border:"none",cursor:"pointer",width:"100%"},children:[e.jsx($e,{size:12,style:{transition:"transform 0.15s",transform:v?"rotate(90deg)":"none",color:"rgba(255,255,255,0.3)"},"aria-hidden":"true"}),e.jsx(oe,{size:12,"aria-hidden":"true"}),"Archived (",h.length,")"]}),e.jsx("div",{id:"archived-channels-mobile",style:{display:v?"block":"none"},children:e.jsx("div",{style:{margin:"0 12px",borderRadius:16,background:"rgba(255,255,255,0.03)",border:"1px solid rgba(255,255,255,0.06)",overflow:"hidden"},children:h.map((t,n,r)=>{var b;const c=(b=t.settings)==null?void 0:b.archived_agent_id,f=c?C.some($=>$.id===c):!1;return e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,borderBottom:n===r.length-1?"none":"1px solid rgba(255,255,255,0.04)",opacity:.6},children:[e.jsxs("button",{type:"button",onClick:()=>E(t.id),"aria-label":`Archived channel ${t.name}`,style:{flex:1,display:"flex",alignItems:"center",gap:8,padding:"12px 8px 12px 16px",background:m===t.id?"rgba(59,130,246,0.12)":"none",border:"none",cursor:"pointer",color:"inherit",textAlign:"left",minWidth:0},children:[e.jsx(oe,{size:11,"aria-hidden":"true",style:{color:"rgba(255,255,255,0.4)",flexShrink:0}}),e.jsx("span",{style:{flex:1,fontSize:14,color:"rgba(255,255,255,0.7)",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:t.name})]}),e.jsxs("div",{style:{display:"flex",gap:2,paddingRight:8},children:[e.jsx("button",{type:"button",onClick:()=>Ge(t.id,t.name),disabled:!f,"aria-label":`Restore archived channel ${t.name}`,title:f?"Restore agent":"Agent entry missing — delete only",style:{background:"none",border:"none",cursor:f?"pointer":"not-allowed",color:f?"rgba(52,211,153,0.7)":"rgba(255,255,255,0.2)",padding:"6px"},children:e.jsx(ot,{size:13,"aria-hidden":"true"})}),e.jsx("button",{type:"button",onClick:()=>Xe(t.id),"aria-label":`Permanently delete archived channel ${t.name}`,title:"Delete permanently",style:{background:"none",border:"none",cursor:"pointer",color:"rgba(248,113,113,0.7)",padding:"6px"},children:e.jsx(ct,{size:13,"aria-hidden":"true"})})]})]},t.id)})})})]})]}):e.jsxs("div",{className:"w-full flex flex-col h-full",children:[e.jsx("div",{className:"px-3 py-1.5 text-[11px] flex items-center gap-1.5",children:M==="connected"?e.jsxs(e.Fragment,{children:[e.jsx(fe,{size:11,className:"text-emerald-400"}),e.jsx("span",{className:"text-emerald-400/80",children:"Connected"})]}):M==="connecting"?e.jsxs(e.Fragment,{children:[e.jsx(fe,{size:11,className:"text-amber-400 animate-pulse"}),e.jsx("span",{className:"text-amber-400/80",children:"Connecting..."})]}):e.jsxs(e.Fragment,{children:[e.jsx(lt,{size:11,className:"text-red-400"}),e.jsx("span",{className:"text-red-400/80",children:"Offline"})]})}),e.jsxs("div",{className:"flex-1 overflow-y-auto py-1",children:[Qe.map(t=>e.jsxs("div",{children:[e.jsxs("div",{className:"px-3 pt-3 pb-1 text-[10px] font-semibold uppercase tracking-wider text-white/30 flex items-center gap-1.5",children:[t.icon," ",t.label]}),t.items.length===0&&e.jsx("div",{className:"px-3 py-1 text-[11px] text-white/20 italic",children:"None yet"}),t.items.map(n=>e.jsxs(U,{variant:m===n.id?"secondary":"ghost",onClick:()=>E(n.id),className:"w-full justify-start h-auto py-1.5 px-3 text-[13px] rounded-none font-normal","aria-label":`Channel ${n.name}`,children:[e.jsx("span",{className:"truncate flex-1 text-left",children:n.name}),(p[n.id]??0)>0&&e.jsx("span",{className:"shrink-0 bg-blue-500 text-white text-[10px] font-bold rounded-full min-w-[18px] h-[18px] flex items-center justify-center px-1",children:p[n.id]})]},n.id))]},t.label)),h.length>0&&e.jsxs("div",{className:"mt-2",children:[e.jsxs("button",{type:"button",onClick:()=>w(t=>!t),className:"flex items-center gap-1.5 px-3 pt-2 pb-1 text-[10px] font-semibold uppercase tracking-wider text-white/25 hover:text-white/40 transition-colors w-full text-left","aria-expanded":v,"aria-controls":"archived-channels-desktop",children:[e.jsx($e,{size:11,className:`transition-transform ${v?"rotate-90":""}`,"aria-hidden":"true"}),e.jsx(oe,{size:11,"aria-hidden":"true"}),"Archived (",h.length,")"]}),e.jsx("div",{id:"archived-channels-desktop",className:v?"":"hidden",children:h.map(t=>{var c;const n=(c=t.settings)==null?void 0:c.archived_agent_id,r=n?C.some(f=>f.id===n):!1;return e.jsxs("div",{className:"group relative flex items-center opacity-60 hover:opacity-80 transition-opacity",children:[e.jsxs(U,{variant:m===t.id?"secondary":"ghost",onClick:()=>E(t.id),className:"flex-1 justify-start h-auto py-1.5 pl-3 pr-1 text-[13px] rounded-none font-normal min-w-0","aria-label":`Archived channel ${t.name}`,children:[e.jsx(oe,{size:11,className:"shrink-0 mr-1.5 text-white/40","aria-hidden":"true"}),e.jsx("span",{className:"truncate flex-1 text-left",children:t.name})]}),e.jsxs("div",{className:"hidden group-hover:flex items-center shrink-0 pr-1",children:[e.jsx("button",{type:"button",onClick:()=>Ge(t.id,t.name),disabled:!r,"aria-label":`Restore archived channel ${t.name}`,title:r?"Restore agent":"Agent entry missing — delete only",className:`p-1 rounded transition-colors ${r?"text-white/30 hover:text-emerald-400 hover:bg-emerald-500/10 cursor-pointer":"text-white/15 cursor-not-allowed"}`,children:e.jsx(ot,{size:12,"aria-hidden":"true"})}),e.jsx("button",{type:"button",onClick:()=>Xe(t.id),"aria-label":`Permanently delete archived channel ${t.name}`,title:"Delete permanently",className:"p-1 rounded text-white/30 hover:text-red-400 hover:bg-red-500/10 transition-colors cursor-pointer",children:e.jsx(ct,{size:12,"aria-hidden":"true"})})]})]},t.id)})})]})]})]}),Xt=e.jsx("div",{className:"flex-1 flex flex-col min-w-0 h-full",children:m?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"px-4 py-2.5 border-b border-white/[0.06] flex items-center gap-3 shrink-0",children:[(j==null?void 0:j.type)==="topic"?e.jsx(ht,{size:16,className:"text-white/40"}):(j==null?void 0:j.type)==="group"?e.jsx(Te,{size:16,className:"text-white/40"}):e.jsx(ut,{size:16,className:"text-white/40"}),(()=>{if((j==null?void 0:j.type)!=="dm")return null;const t=(j.members??[]).find(r=>r!=="user");if(!t)return null;const n=N.find(r=>r.name===t);return n?e.jsx("span",{className:"text-base leading-none shrink-0","aria-hidden":"true",children:it(n.emoji,n.framework)}):null})(),e.jsxs("div",{className:"min-w-0 flex-1",children:[e.jsxs("div",{className:"text-sm font-medium truncate flex items-center gap-1",children:[(j==null?void 0:j.name)??"Unknown",j&&j.type!=="dm"&&e.jsx("button",{"aria-label":"Channel settings",onClick:It,className:"ml-1 opacity-60 hover:opacity-100",children:"ⓘ"}),e.jsx("a",{"aria-label":"Open chat guide",href:"https://github.com/jaylfc/tinyagentos/blob/master/docs/chat-guide.md",target:"_blank",rel:"noreferrer",className:"ml-1 opacity-60 hover:opacity-100 text-[12px]",children:"?"}),e.jsxs("div",{className:"relative",children:[e.jsx(Us,{count:ue.length,onClick:()=>ke(t=>!t)}),Mt&&e.jsx(Fs,{pins:ue,onJumpTo:t=>{ke(!1);const n=document.querySelector(`[data-message-id="${t}"]`);n&&(n.scrollIntoView({behavior:"smooth",block:"center"}),n.classList.add("data-highlight"),setTimeout(()=>n.classList.remove("data-highlight"),2e3))},onClose:()=>ke(!1)})]})]}),(j==null?void 0:j.description)&&e.jsx("div",{className:"text-[11px] text-white/35 truncate",children:j.description})]}),(j==null?void 0:j.members)&&e.jsxs("div",{className:"text-[11px] text-white/30 flex items-center gap-1",children:[e.jsx(Te,{size:12})," ",j.members.length]})]}),e.jsxs("div",{ref:Fe,onScroll:Dt,className:"flex-1 overflow-y-auto px-4 py-3 space-y-0.5",style:i&&l>0?{paddingBottom:`${l+60}px`}:void 0,onDragOver:t=>t.preventDefault(),onDrop:t=>{t.preventDefault();for(const n of Array.from(t.dataTransfer.files)){const r=Math.random().toString(36).slice(2);L(c=>[...c,{id:r,filename:n.name,size:n.size,uploading:!0}]),Ae(n,m??void 0).then(c=>L(f=>f.map(b=>b.id===r?{...b,record:c,uploading:!1}:b))).catch(c=>L(f=>f.map(b=>b.id===r?{...b,uploading:!1,error:c.message}:b)))}},children:[_.length===0&&e.jsx("div",{className:"flex items-center justify-center h-full text-white/20 text-sm",children:"No messages yet. Say something!"}),_.map((t,n)=>{var F,me,st,nt;const r=t.author_type==="agent",c=n>0?_[n-1]:void 0,f=!c||c.author_id!==t.author_id,b=qs(t.author_id,t.author_type,N,C),$=r&&b!=="active",P=b==="archived"?"Agent no longer active":b==="removed"?"Agent removed":void 0;return e.jsxs("div",{"data-message-id":t.id,className:`group relative px-3 py-1 rounded-md transition-colors hover:bg-white/[0.03] ${r&&!$?"bg-blue-500/[0.04]":""} ${f?"mt-3":""}`,onMouseEnter:()=>Le(t.id),onMouseLeave:()=>Le(A=>A===t.id?null:A),children:[f&&e.jsxs("div",{className:"flex items-center gap-2 mb-0.5",onContextMenu:A=>{t.author_type==="agent"&&(A.preventDefault(),ne({slug:t.author_id,x:A.clientX,y:A.clientY}))},children:[r&&!$&&(()=>{const A=N.find(Z=>Z.name===t.author_id);return A?e.jsx("span",{className:"text-[14px] leading-none","aria-hidden":"true",children:it(A.emoji,A.framework)}):null})(),e.jsx("span",{className:`text-[13px] font-semibold ${$?"line-through text-white/35":r?"text-blue-400":"text-white/90"}`,style:$?{opacity:.55}:void 0,title:P,children:t.author_id}),r&&!$&&e.jsxs("span",{className:"text-[10px] bg-blue-500/20 text-blue-300 px-1.5 py-0.5 rounded font-medium flex items-center gap-0.5",children:[e.jsx(pt,{size:10,"aria-hidden":"true"})," Agent"]}),$&&e.jsxs("span",{className:"text-[10px] bg-zinc-500/20 text-zinc-500 px-1.5 py-0.5 rounded font-medium flex items-center gap-0.5",children:[e.jsx(pt,{size:10,"aria-hidden":"true"}),b==="archived"?"inactive":"removed"]}),e.jsx("span",{className:`text-[11px] ${$?"text-white/15":"text-white/25"}`,children:Vs(t.created_at)}),t.edited_at&&e.jsx("span",{className:"text-[10px] text-white/20",children:"(edited)"})]}),t.deleted_at?e.jsx(Is,{}):At===t.id?e.jsx(Ls,{initial:t.content,onSave:A=>Jt(t.id,A),onCancel:()=>ve(null)}):e.jsxs("div",{className:`text-[13px] leading-relaxed whitespace-pre-wrap break-words ${$?"text-white/45":"text-white/80"}`,children:[Ys(t.content),t.state==="pending"&&e.jsx("span",{className:"ml-1 text-white/30",children:"..."}),t.state==="streaming"&&e.jsxs("span",{className:"ml-1 inline-flex gap-0.5",children:[e.jsx("span",{className:"w-1 h-1 bg-blue-400 rounded-full animate-bounce [animation-delay:0ms]"}),e.jsx("span",{className:"w-1 h-1 bg-blue-400 rounded-full animate-bounce [animation-delay:150ms]"}),e.jsx("span",{className:"w-1 h-1 bg-blue-400 rounded-full animate-bounce [animation-delay:300ms]"})]}),t.state==="error"&&e.jsx("span",{className:"ml-1 text-red-400 text-[11px]",children:"(error)"})]}),((F=t.metadata)==null?void 0:F.pin_requested)&&t.author_type==="agent"&&e.jsx(Bs,{authorId:t.author_id,onApprove:()=>Yt(t.id)}),t.content_type==="canvas"&&(((me=t.metadata)==null?void 0:me.canvas_url)||((st=t.metadata)==null?void 0:st.canvas_id))&&e.jsx("div",{className:"mt-2",children:e.jsxs(U,{size:"sm",variant:"outline",onClick:()=>{var Z,at,rt;const A=((Z=t.metadata)==null?void 0:Z.canvas_url)??`/canvas/${(at=t.metadata)==null?void 0:at.canvas_id}`;ye({url:A,title:(rt=t.metadata)==null?void 0:rt.canvas_title})},className:"h-7 px-2.5 text-[12px] gap-1.5 bg-white/[0.04] border-white/10 hover:bg-white/[0.08]","aria-label":"View canvas",children:[e.jsx(mt,{size:13}),"View Canvas",(nt=t.metadata)!=null&&nt.canvas_title?`: ${t.metadata.canvas_title}`:""]})}),t.reactions&&Object.keys(t.reactions).length>0&&e.jsx("div",{className:"flex flex-wrap gap-1 mt-1",children:Object.entries(t.reactions).map(([A,Z])=>e.jsxs("button",{onClick:()=>Ve(t.id,A),className:"text-[12px] bg-white/[0.06] hover:bg-white/10 border border-white/[0.06] rounded-full px-2 py-0.5 flex items-center gap-1 transition-colors",children:[e.jsx("span",{children:A}),e.jsx("span",{className:"text-white/40",children:Z.length})]},A))}),Tt===t.id&&e.jsx("div",{className:"absolute top-0 right-2 -translate-y-1/2 z-10",children:e.jsx(ks,{onReact:()=>Pe(ze===t.id?null:t.id),onReplyInThread:()=>Ke(t.channel_id??m??"",t.id),onOverflow:A=>{A.preventDefault(),B({messageId:t.id,x:A.clientX,y:A.clientY})}})}),e.jsx($s,{attachments:t.attachments||[]}),typeof t.reply_count=="number"&&t.reply_count>0&&e.jsx(Ns,{replyCount:t.reply_count,lastReplyAt:t.last_reply_at??null,onOpen:()=>Ke(t.channel_id??m??"",t.id)}),ze===t.id&&e.jsx("div",{className:"absolute right-2 top-5 bg-zinc-800 border border-white/10 rounded-lg shadow-xl p-2 flex gap-1 z-10",children:Gs.map(A=>e.jsx("button",{onClick:()=>Ve(t.id,A),className:"text-lg hover:bg-white/10 rounded p-0.5 transition-colors",children:A},A))})]},t.id)}),e.jsx("div",{ref:Ue})]}),e.jsx(ys,{humans:_t,agents:$t,selfId:"user"}),J&&e.jsxs("div",{className:"mx-4 mb-2 px-3 py-2 rounded-lg bg-amber-500/10 border border-amber-500/20 text-[12px] text-amber-400/80 flex items-center gap-2 shrink-0",role:"status",children:[e.jsx(oe,{size:13,"aria-hidden":"true"}),"This chat is archived. The agent is no longer active."]}),se&&e.jsxs("div",{className:"mx-4 mb-1 px-3 py-2 rounded-lg bg-blue-500/10 border border-blue-500/20 text-[12px] text-blue-300/90 flex items-center gap-2 shrink-0",role:"status","aria-label":`Composer prefilled from prompt: ${se.promptName}`,children:[e.jsxs("span",{className:"flex-1 truncate",children:["Prefilled from: ",se.promptName,se.agentName?` for ${se.agentName}`:""," — edit and send"]}),e.jsx("button",{onClick:()=>{Re(null),u("")},className:"shrink-0 p-0.5 rounded hover:bg-white/10 transition-colors","aria-label":"Dismiss prefill",children:e.jsx(ge,{size:12,"aria-hidden":"true"})})]}),Oe&&e.jsx("div",{role:"alert",className:"text-xs text-red-300 bg-red-500/10 border border-red-500/30 rounded px-3 py-1 mx-4",children:Oe}),e.jsx(Es,{items:G,onRemove:t=>L(n=>n.filter(r=>r.id!==t)),onRetry:t=>{L(n=>n.map(r=>r.id===t?{...r,uploading:!1,error:"retry not yet supported — remove and re-add"}:r))}}),e.jsx("div",{className:"px-4 py-3 border-t border-white/[0.06] shrink-0",style:i?{paddingBottom:`max(env(safe-area-inset-bottom), ${l}px)`}:void 0,children:e.jsxs("div",{className:"relative",children:[We&&e.jsx(fs,{commands:Ct,queryAfterSlash:Lt,members:(j==null?void 0:j.members)||[],onPick:(t,n)=>{u(`@${t} /${n} `)},onClose:()=>{}}),e.jsxs("div",{className:`flex items-end gap-2 rounded-xl border px-2 py-1.5 ${J?"bg-white/[0.02] border-white/[0.04] opacity-50":"bg-white/[0.06] border-white/[0.08]"}`,children:[e.jsx(U,{variant:"ghost",size:"icon",onClick:Bt,className:"h-8 w-8 shrink-0 mb-0.5","aria-label":"Upload file",disabled:J,children:e.jsx(os,{size:16})}),e.jsx(ts,{ref:I,value:k,onChange:t=>!J&&Ut(t.target.value),onKeyDown:t=>!J&&Ft(t),onPaste:t=>{if(!t.clipboardData)return;const n=Array.from(t.clipboardData.files).filter(r=>r.type.startsWith("image/"));if(n.length!==0){t.preventDefault();for(const r of n){const c=Math.random().toString(36).slice(2);L(f=>[...f,{id:c,filename:r.name||"pasted.png",size:r.size,uploading:!0}]),Ae(r,m??void 0).then(f=>L(b=>b.map($=>$.id===c?{...$,record:f,uploading:!1}:$))).catch(f=>L(b=>b.map($=>$.id===c?{...$,uploading:!1,error:f.message}:$)))}}},placeholder:J?"This chat is archived":`Message #${(j==null?void 0:j.name)??""}...`,rows:1,disabled:J,className:"flex-1 bg-transparent border-0 px-1 py-1.5 min-h-0 text-[13px] focus-visible:ring-0 focus-visible:border-0 max-h-[120px] disabled:cursor-not-allowed","aria-label":"Message input"}),e.jsx(U,{size:"icon",onClick:qe,disabled:!k.trim()&&G.length===0||J||G.some(t=>t.uploading),className:"h-8 w-8 shrink-0 mb-0.5","aria-label":"Send message",children:e.jsx(cs,{size:15})})]})]})})]}):e.jsx("div",{className:"flex-1 flex items-center justify-center text-white/20",children:e.jsxs("div",{className:"text-center",children:[e.jsx(dt,{size:48,className:"mx-auto mb-3 opacity-30"}),e.jsx("p",{className:"text-sm",children:"Select a channel to start chatting"})]})})}),Qt=!i||m===null;return e.jsxs("div",{className:"flex flex-col h-full bg-shell-base text-white overflow-hidden",children:[Qt&&e.jsx("div",{className:"relative flex items-center px-3 py-2.5 border-b border-white/[0.06] shrink-0",children:a?e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"absolute inset-0 flex items-center justify-center pointer-events-none",children:e.jsx("span",{className:"text-sm font-semibold text-white/90",children:a})}),e.jsx("div",{className:"ml-auto",children:e.jsx(U,{variant:"ghost",size:"icon",onClick:()=>Y(!0),className:"h-7 w-7","aria-label":"New channel",children:e.jsx(xt,{size:15})})})]}):e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"flex items-center gap-2 text-sm font-medium text-white/80",children:[e.jsx(dt,{size:15}),!i&&"Messages"]}),e.jsx(U,{variant:"ghost",size:"icon",onClick:()=>Y(!0),className:"h-7 w-7 ml-auto","aria-label":"New channel",children:e.jsx(xt,{size:15})})]})}),e.jsx("div",{className:"flex-1 min-h-0 overflow-hidden",children:e.jsx(is,{selectedId:m,onBack:()=>E(null),listTitle:"Messages",detailTitle:j==null?void 0:j.name,listWidth:240,list:Gt,detail:Xt})}),he&&(()=>{const t=_.find(r=>r.id===he.messageId);if(!t)return null;const n=e.jsx(Rs,{isOwn:t.author_id===zt,isHuman:!0,isPinned:ue.some(r=>r.id===t.id),onEdit:()=>Ht(t.id),onDelete:()=>Wt(t.id),onCopyLink:()=>Kt(t.id),onPin:()=>qt(t),onMarkUnread:()=>Vt(t.id),onClose:()=>B(null)});return i?e.jsx(Os,{open:!0,onClose:()=>B(null),children:n}):e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"fixed inset-0 z-40",onClick:()=>B(null)}),e.jsx("div",{className:"fixed z-50",style:{top:he.y,left:he.x},children:n})]})})(),St&&j&&e.jsx(ms,{channel:{id:j.id,name:j.name,type:j.type,topic:j.topic??"",members:j.members??[],settings:j.settings??{}},knownAgents:N.map(t=>({name:t.name})),onClose:()=>we(!1),onChanged:()=>{Q()}}),ie&&e.jsx(Cs,{channelId:ie.channelId,parentId:ie.parentId,onClose:Ie,isFullscreen:i,onSend:async(t,n)=>{const r=await fetch("/api/chat/messages",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({channel_id:ie.channelId,author_id:"user",author_type:"user",content:t,content_type:"text",thread_id:ie.parentId,attachments:n})});if(!r.ok){const c=await r.json().catch(()=>({}));throw new Error(c.error||`HTTP ${r.status}`)}}}),K&&e.jsx(xs,{slug:K.slug,channelId:m??void 0,channelType:j==null?void 0:j.type,isMuted:((tt=(et=j==null?void 0:j.settings)==null?void 0:et.muted)==null?void 0:tt.includes(K.slug))??!1,x:K.x,y:K.y,onClose:()=>ne(null),onDm:async t=>{const n=d.find(r=>r.type==="dm"&&(r.members||[]).length===2&&(r.members||[]).includes("user")&&(r.members||[]).includes(t));if(n)E(n.id);else{const r=await fetch("/api/chat/channels",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:t,type:"dm",members:["user",t],description:"",topic:""})});if(r.ok){const c=await r.json();await Q(),E(c.id)}}ne(null)},onViewInfo:t=>{const n=N.find(r=>r.name===t);n&&De({slug:t,framework:n.framework||"unknown",model:n.model||"unknown",status:n.status||"unknown",x:K.x,y:K.y}),ne(null)},onJumpToSettings:t=>{window.dispatchEvent(new CustomEvent("taos:open-agent",{detail:{slug:t}})),ne(null)}}),H&&e.jsxs("div",{role:"dialog","aria-label":`Agent info for @${H.slug}`,className:"fixed z-50 bg-shell-surface border border-white/10 rounded-lg shadow-xl p-3 text-xs min-w-[200px]",style:{top:H.y,left:H.x},onMouseLeave:()=>De(null),children:[e.jsxs("div",{className:"font-semibold text-sm mb-1",children:["@",H.slug]}),e.jsxs("div",{className:"opacity-70",children:["Framework: ",H.framework]}),e.jsxs("div",{className:"opacity-70",children:["Model: ",H.model]}),e.jsxs("div",{className:"opacity-70",children:["Status: ",H.status]})]}),be&&e.jsx("div",{className:"fixed inset-0 z-[10002] flex items-center justify-center bg-black/60 backdrop-blur-sm",onClick:()=>ye(null),role:"dialog","aria-modal":"true","aria-label":"Canvas viewer",children:e.jsxs("div",{className:"w-[90vw] h-[85vh] max-w-5xl rounded-xl border border-white/10 overflow-hidden bg-zinc-900 flex flex-col",onClick:t=>t.stopPropagation(),children:[e.jsxs("div",{className:"flex items-center justify-between px-4 py-2 border-b border-white/10 shrink-0",children:[e.jsxs("div",{className:"flex items-center gap-2 text-sm text-white/80",children:[e.jsx(mt,{size:14}),e.jsx("span",{children:be.title??"Canvas"})]}),e.jsx(U,{variant:"ghost",size:"icon",onClick:()=>ye(null),className:"h-7 w-7","aria-label":"Close canvas viewer",children:e.jsx(ge,{size:14})})]}),e.jsx("iframe",{src:be.url,className:"flex-1 w-full border-none bg-white",title:"Canvas"})]})}),Nt&&(i?e.jsx("div",{className:"fixed inset-0 z-50",onClick:()=>Y(!1),role:"dialog","aria-modal":"true","aria-label":"New channel",children:e.jsxs("div",{className:"absolute bottom-0 left-0 right-0 bg-zinc-900 border-t border-white/[0.08] rounded-t-2xl p-4 space-y-3",onClick:t=>t.stopPropagation(),children:[e.jsxs("div",{className:"flex items-center justify-between mb-1",children:[e.jsx("span",{className:"text-sm font-semibold",children:"New Channel"}),e.jsx(U,{variant:"ghost",size:"icon",onClick:()=>Y(!1),className:"h-7 w-7","aria-label":"Close",children:e.jsx(ge,{size:15})})]}),e.jsxs("div",{className:"space-y-1",children:[e.jsx(ee,{htmlFor:"new-channel-name-mobile",className:"block uppercase tracking-wider",children:"Name"}),e.jsx(xe,{id:"new-channel-name-mobile",value:O.name,onChange:t=>W(n=>({...n,name:t.target.value})),placeholder:"general","aria-label":"Channel name"})]}),e.jsxs("div",{className:"space-y-1",children:[e.jsx(ee,{htmlFor:"new-channel-type-mobile",className:"block uppercase tracking-wider",children:"Type"}),e.jsxs("select",{id:"new-channel-type-mobile",value:O.type,onChange:t=>W(n=>({...n,type:t.target.value})),className:"w-full bg-white/[0.06] border border-white/10 rounded-lg px-3 py-2 text-sm text-white outline-none focus:border-blue-500/50","aria-label":"Channel type",children:[e.jsx("option",{value:"topic",children:"Topic"}),e.jsx("option",{value:"group",children:"Group"})]})]}),e.jsxs("div",{className:"space-y-1",children:[e.jsx(ee,{htmlFor:"new-channel-description-mobile",className:"block uppercase tracking-wider",children:"Description"}),e.jsx(xe,{id:"new-channel-description-mobile",value:O.description,onChange:t=>W(n=>({...n,description:t.target.value})),placeholder:"What's this channel about?","aria-label":"Channel description"})]}),e.jsx(U,{onClick:Ye,disabled:!O.name.trim(),className:"w-full",children:"Create Channel"})]})}):e.jsx("div",{className:"absolute inset-0 bg-black/60 flex items-center justify-center z-50 p-4",children:e.jsxs(ss,{className:"w-full max-w-[380px] max-h-full flex flex-col shadow-2xl bg-zinc-900",children:[e.jsxs(ns,{className:"flex flex-row items-center justify-between gap-2 p-0 px-4 py-3 border-b border-white/[0.06]",children:[e.jsx(as,{className:"text-sm font-medium",children:"New Channel"}),e.jsx(U,{variant:"ghost",size:"icon",onClick:()=>Y(!1),className:"h-7 w-7","aria-label":"Close",children:e.jsx(ge,{size:15})})]}),e.jsxs(rs,{className:"p-4 pt-4 space-y-3",children:[e.jsxs("div",{className:"space-y-1",children:[e.jsx(ee,{htmlFor:"new-channel-name",className:"block uppercase tracking-wider",children:"Name"}),e.jsx(xe,{id:"new-channel-name",value:O.name,onChange:t=>W(n=>({...n,name:t.target.value})),placeholder:"general","aria-label":"Channel name"})]}),e.jsxs("div",{className:"space-y-1",children:[e.jsx(ee,{htmlFor:"new-channel-type",className:"block uppercase tracking-wider",children:"Type"}),e.jsxs("select",{id:"new-channel-type",value:O.type,onChange:t=>W(n=>({...n,type:t.target.value})),className:"w-full bg-white/[0.06] border border-white/10 rounded-lg px-3 py-2 text-sm text-white outline-none focus:border-blue-500/50","aria-label":"Channel type",children:[e.jsx("option",{value:"topic",children:"Topic"}),e.jsx("option",{value:"group",children:"Group"})]})]}),e.jsxs("div",{className:"space-y-1",children:[e.jsx(ee,{htmlFor:"new-channel-description",className:"block uppercase tracking-wider",children:"Description"}),e.jsx(xe,{id:"new-channel-description",value:O.description,onChange:t=>W(n=>({...n,description:t.target.value})),placeholder:"What's this channel about?","aria-label":"Channel description"})]}),e.jsx(U,{onClick:Ye,disabled:!O.name.trim(),className:"w-full",children:"Create Channel"})]})]})}))]})}export{rn as MessagesApp,qs as resolveAuthorDisplayState};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Mobile reply/overflow actions still depend on hover-only state.

The compiled message-row logic still shows MessageHoverActions from onMouseEnter/onMouseLeave state. There’s no explicit touch path here, so the new mobile thread takeover and bottom-sheet flows depend on hover synthesis instead of a reliable phone interaction. Please expose these actions via a mobile-visible affordance or a touch/long-press toggle.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@static/desktop/assets/MessagesApp-BlnZE9ab.js` at line 1, The message-row
actions rely only on hover-driven state (Tt with Le via
onMouseEnter/onMouseLeave) so mobile/touch has no reliable way to reveal
MessageHoverActions (ks) or reaction picker (ze/Pe). Update the message
rendering block where onMouseEnter/onMouseLeave call Le to also support touch:
add onTouchStart/onTouchEnd (or onContextMenu for long-press) handlers that
set/toggle Tt via Le (and set Pe/ze for reactions) so touch users can reveal the
same UI, and also add a small persistent mobile-visible affordance button inside
the message row that opens the overflow menu by calling the same handler that
B/Le use (so ks can be opened on tap). Ensure you modify the message container
logic and the render path that checks Tt===t.id and ze===t.id to reuse the same
handlers so both hover and touch produce identical behavior.

Comment on lines +31 to +35
mobile_page.goto(f"{URL}/chat-pwa")
mobile_page.get_by_text("roundtable").first.click()
first = mobile_page.locator("[data-message-id]").first
first.tap()
mobile_page.get_by_role("button", name=re.compile("Reply in thread", re.I)).click()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

These E2Es are tied to seeded content.

Hard-coding "roundtable" and then using the first [data-message-id] makes the suite depend on a very specific dataset, but the file is only gated by TAOS_E2E_URL. Point this at a fresh or shared environment and these tests become nondeterministic immediately. Seed the channel/message in setup, or create/select test data through the API before asserting.

Also applies to: 46-50

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/test_messages_pwa.py` around lines 31 - 35, The test uses
hard-coded UI selections (mobile_page.get_by_text("roundtable"),
mobile_page.locator("[data-message-id]").first, mobile_page.get_by_role(...))
which ties E2Es to seeded content; instead create or select deterministic test
data in setup via the API (create a channel and message) and use those
resources' unique IDs or test-only markers to drive the UI interactions
(navigate to the channel created, find the specific message by its test id
instead of .first, then tap() and click the reply button). Apply the same change
for the similar block around lines 46-50 so tests no longer depend on external
seeded data.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
desktop/src/apps/MessagesApp.tsx (2)

1698-1716: Good platform-specific rendering refactor.

Extracting the menu element and conditionally wrapping it in BottomSheet for mobile is a clean approach. The open={true} is safe here since this block only executes when overflowMenu is truthy.

Consider adding labelledBy to the BottomSheet for improved accessibility if MessageOverflowMenu contains a heading element:

♻️ Optional a11y enhancement
 <BottomSheet
   open={true}
   onClose={() => setOverflowMenu(null)}
+  labelledBy="message-overflow-menu-title"
 >
   {menu}
 </BottomSheet>

This would require adding a corresponding id="message-overflow-menu-title" to a heading element inside MessageOverflowMenu.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/MessagesApp.tsx` around lines 1698 - 1716, Add an
accessibility label to the BottomSheet by passing a labelledBy prop referencing
the overflow menu heading id (e.g., labelledBy="message-overflow-menu-title")
and ensure the MessageOverflowMenu renders a heading element with
id="message-overflow-menu-title" so BottomSheet and screen readers can associate
the sheet with the menu title; update the BottomSheet usage in MessagesApp (the
BottomSheet wrapper around MessageOverflowMenu) and add the matching id to the
heading inside MessageOverflowMenu.

1301-1301: Consider extracting the magic number 60 to a named constant.

The hardcoded 60px added to keyboardInset appears to approximate the composer height. If the composer's dimensions change, this value could become out of sync. Consider extracting to a constant with a descriptive name for maintainability.

♻️ Suggested refactor
+const COMPOSER_HEIGHT_ESTIMATE = 60;
+
 // In the component...
-style={isMobile && keyboardInset > 0 ? { paddingBottom: `${keyboardInset + 60}px` } : undefined}
+style={isMobile && keyboardInset > 0 ? { paddingBottom: `${keyboardInset + COMPOSER_HEIGHT_ESTIMATE}px` } : undefined}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/MessagesApp.tsx` at line 1301, The inline magic number `60`
in the style expression should be extracted to a clearly named constant (e.g.,
COMPOSER_HEIGHT_PX or DEFAULT_COMPOSER_BOTTOM_PADDING) so the padding
calculation uses `keyboardInset + COMPOSER_HEIGHT_PX` instead of a literal;
update the expression in MessagesApp (where `isMobile` and `keyboardInset` are
referenced) to use that constant, place the constant near related UI size
constants or the top of MessagesApp.tsx, and add a short comment noting it
represents the composer height so future changes stay in sync.
static/desktop/assets/chat-BrDx525_.js (1)

2-2: Consider adding a null-check for #root element to improve error clarity.

The code currently calls createRoot(document.getElementById("root")) without validation. While the #root element is present in all entry point HTML files, adding a defensive check would provide a clearer error message if the HTML structure is ever misconfigured, rather than relying on React's error.

+ const rootEl = document.getElementById("root");
+ if (!rootEl) {
+   throw new Error('Missing mount element: `#root`');
+ }
- createRoot(document.getElementById("root")).render(e.jsx(t.StrictMode,{children:e.jsx(w,{})}));
+ createRoot(rootEl).render(e.jsx(t.StrictMode,{children:e.jsx(w,{})}));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@static/desktop/assets/chat-BrDx525_.js` at line 2, Add a defensive null-check
before calling u.createRoot(document.getElementById("root")): fetch the element
via document.getElementById("root"), verify it's non-null, and if null throw or
console.error a clear message (e.g., "Root element '#root' not found — ensure
HTML includes <div id=\"root\">") instead of passing null into u.createRoot;
then call u.createRoot(root).render(e.jsx(t.StrictMode,{children:e.jsx(w,{})})).
Update references in this file around u.createRoot and the render call so the
check is applied before render.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@static/desktop/assets/chat-BrDx525_.js`:
- Line 2: The component function y and its handlers (p and m) access
localStorage (key i) directly during render/interaction which can throw in
restricted environments; wrap all localStorage.getItem/setItem calls in a safe
wrapper that first checks for typeof window !== "undefined" and try/catch around
the access (or use a small safeStorage helper) and update y, p, and m to use
that helper so failures are swallowed/fallbacked (e.g., treat as absent) instead
of throwing and breaking render; ensure the prompt/decision logic still respects
the stored timestamp when available.

In `@static/desktop/assets/MCPApp-Cq-bUfV5.js`:
- Around line 1-2: The copy-all logs action in Se is joining lines with a
leading-space separator which inserts extra spaces; update the
navigator.clipboard.writeText call inside Se's j() handler to join the logs with
a plain newline (use n.join("\n")) so pasted logs preserve exact original lines
(locate the navigator.clipboard.writeText call in function Se where n.join is
used).

---

Nitpick comments:
In `@desktop/src/apps/MessagesApp.tsx`:
- Around line 1698-1716: Add an accessibility label to the BottomSheet by
passing a labelledBy prop referencing the overflow menu heading id (e.g.,
labelledBy="message-overflow-menu-title") and ensure the MessageOverflowMenu
renders a heading element with id="message-overflow-menu-title" so BottomSheet
and screen readers can associate the sheet with the menu title; update the
BottomSheet usage in MessagesApp (the BottomSheet wrapper around
MessageOverflowMenu) and add the matching id to the heading inside
MessageOverflowMenu.
- Line 1301: The inline magic number `60` in the style expression should be
extracted to a clearly named constant (e.g., COMPOSER_HEIGHT_PX or
DEFAULT_COMPOSER_BOTTOM_PADDING) so the padding calculation uses `keyboardInset
+ COMPOSER_HEIGHT_PX` instead of a literal; update the expression in MessagesApp
(where `isMobile` and `keyboardInset` are referenced) to use that constant,
place the constant near related UI size constants or the top of MessagesApp.tsx,
and add a short comment noting it represents the composer height so future
changes stay in sync.

In `@static/desktop/assets/chat-BrDx525_.js`:
- Line 2: Add a defensive null-check before calling
u.createRoot(document.getElementById("root")): fetch the element via
document.getElementById("root"), verify it's non-null, and if null throw or
console.error a clear message (e.g., "Root element '#root' not found — ensure
HTML includes <div id=\"root\">") instead of passing null into u.createRoot;
then call u.createRoot(root).render(e.jsx(t.StrictMode,{children:e.jsx(w,{})})).
Update references in this file around u.createRoot and the render call so the
check is applied before render.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6bcb746b-7570-4415-8b18-fdb3b389951a

📥 Commits

Reviewing files that changed from the base of the PR and between f0c71f0 and 9800c77.

📒 Files selected for processing (10)
  • desktop/src/apps/MessagesApp.tsx
  • desktop/src/shell/BottomSheet.tsx
  • static/desktop/assets/MCPApp-Cq-bUfV5.js
  • static/desktop/assets/MessagesApp-BG8aIEeG.js
  • static/desktop/assets/ProvidersApp-BY6yJVKl.js
  • static/desktop/assets/SettingsApp-F200Wg__.js
  • static/desktop/assets/chat-BrDx525_.js
  • static/desktop/assets/main-BzBv0_mH.js
  • static/desktop/chat.html
  • static/desktop/index.html
✅ Files skipped from review due to trivial changes (4)
  • static/desktop/assets/ProvidersApp-BY6yJVKl.js
  • static/desktop/index.html
  • static/desktop/assets/MessagesApp-BG8aIEeG.js
  • static/desktop/assets/SettingsApp-F200Wg__.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • static/desktop/chat.html
  • desktop/src/shell/BottomSheet.tsx

@@ -0,0 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/MessagesApp-BG8aIEeG.js","assets/vendor-react-l6srOxy7.js","assets/toolbar-UW6q5pkx.js","assets/vendor-radix-BhM7AEEG.js","assets/vendor-layout-B-pp9n1f.js","assets/vendor-layout-BfitWg9R.css","assets/MobileSplitView-CtNEF6zb.js","assets/vendor-icons-wm645Jsx.js","assets/use-is-mobile-v5lglusa.js"])))=>i.map(i=>d[i]);
import"./tokens-C63es8oZ.js";import{r as t,j as e,p as u}from"./vendor-react-l6srOxy7.js";import{_ as f}from"./vendor-codemirror-CL2HhW7v.js";import{u as x}from"./use-is-mobile-v5lglusa.js";const h=720*60*60*1e3,i="taos-install-dismissed";function y(){const s=x(),[n,a]=t.useState(null),[c,d]=t.useState(!1);if(t.useEffect(()=>{const o=l=>{l.preventDefault(),a(l)};return window.addEventListener("beforeinstallprompt",o),()=>window.removeEventListener("beforeinstallprompt",o)},[]),!s||!n||c||typeof window<"u"&&window.matchMedia("(display-mode: standalone)").matches)return null;const r=localStorage.getItem(i);if(r&&Date.now()-Number(r)<h)return null;const p=async()=>{try{await n.prompt(),await n.userChoice}catch{}a(null)},m=()=>{localStorage.setItem(i,String(Date.now())),d(!0)};return e.jsxs("div",{role:"region","aria-label":"Install prompt",className:"flex items-center gap-3 px-4 py-2 bg-sky-500/20 border-b border-sky-500/30 text-sm",children:[e.jsx("span",{className:"flex-1",children:"Install taOS talk for quick access"}),e.jsx("button",{onClick:p,className:"px-3 py-1 bg-sky-500/40 text-sky-100 rounded hover:bg-sky-500/60",children:"Install"}),e.jsx("button",{onClick:m,className:"px-2 py-1 opacity-70 hover:opacity-100",children:"Not now"})]})}const b=t.lazy(()=>f(()=>import("./MessagesApp-BG8aIEeG.js"),__vite__mapDeps([0,1,2,3,4,5,6,7,8])).then(s=>({default:s.MessagesApp})));function w(){return e.jsxs("div",{className:"h-screen w-screen flex flex-col overflow-hidden",style:{backgroundColor:"#1a1b2e",paddingTop:"env(safe-area-inset-top, 0px)"},children:[e.jsx(y,{}),e.jsx(t.Suspense,{fallback:e.jsx("div",{className:"flex items-center justify-center h-full",style:{color:"rgba(255,255,255,0.4)"},children:"Loading…"}),children:e.jsx(b,{windowId:"standalone-chat",title:"taOS talk"})})]})}u.createRoot(document.getElementById("root")).render(e.jsx(t.StrictMode,{children:e.jsx(w,{})}));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard storage access to avoid render-time crashes.

Line 2 reads/writes localStorage directly in the render path/handlers. In restricted storage environments, this can throw and break the chat shell startup.

Proposed fix (apply in source component, then rebuild bundle)
--- a/desktop/src/shell/InstallPromptBanner.tsx
+++ b/desktop/src/shell/InstallPromptBanner.tsx
@@
-  const prev = localStorage.getItem(KEY);
+  let prev: string | null = null;
+  try {
+    prev = localStorage.getItem(KEY);
+  } catch {
+    prev = null;
+  }
   if (prev && Date.now() - Number(prev) < DISMISS_MS) return null;
@@
-  const notNow = () => {
-    localStorage.setItem(KEY, String(Date.now()));
-    setDismissed(true);
-  };
+  const notNow = () => {
+    try {
+      localStorage.setItem(KEY, String(Date.now()));
+    } catch {
+      // ignore storage write failures
+    }
+    setDismissed(true);
+  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@static/desktop/assets/chat-BrDx525_.js` at line 2, The component function y
and its handlers (p and m) access localStorage (key i) directly during
render/interaction which can throw in restricted environments; wrap all
localStorage.getItem/setItem calls in a safe wrapper that first checks for
typeof window !== "undefined" and try/catch around the access (or use a small
safeStorage helper) and update y, p, and m to use that helper so failures are
swallowed/fallbacked (e.g., treat as absent) instead of throwing and breaking
render; ensure the prompt/decision logic still respects the stored timestamp
when available.

Comment on lines +1 to 2
import{r as l,j as e}from"./vendor-react-l6srOxy7.js";import{B as k,C as T,I as E,T as Q,L as R,S as ee}from"./toolbar-UW6q5pkx.js";import{M as te}from"./MobileSplitView-CtNEF6zb.js";import{a as se,b as ae,g as le}from"./main-BzBv0_mH.js";import{ao as I,V as $,al as ne,a5 as ie,aN as re,R as ce,y as M,g as O,aG as oe,X as U,f as D,aw as de,r as xe}from"./vendor-icons-wm645Jsx.js";import"./vendor-radix-BhM7AEEG.js";import"./vendor-layout-B-pp9n1f.js";import"./tokens-C63es8oZ.js";import"./vendor-codemirror-CL2HhW7v.js";const he={running:"bg-emerald-500",stopped:"bg-zinc-500",failed:"bg-red-500",installing:"bg-amber-500"},F={running:"bg-emerald-500/20 text-emerald-400",stopped:"bg-zinc-500/20 text-zinc-400",failed:"bg-red-500/20 text-red-400",installing:"bg-amber-500/20 text-amber-400"},P={running:"Running",stopped:"Stopped",failed:"Failed",installing:"Installing"},me={stdio:"bg-blue-500/20 text-blue-300",sse:"bg-violet-500/20 text-violet-300",ws:"bg-teal-500/20 text-teal-300"},pe=["running","installing","failed","stopped"];function ue(t){const n={running:[],stopped:[],failed:[],installing:[]};for(const o of t)n[o.status].push(o);return n}function G(t){return new Date(t*1e3).toLocaleTimeString(void 0,{hour:"2-digit",minute:"2-digit"})}function fe({server:t,attachments:n,onConfirm:o,onClose:i,loading:p}){const[m,r]=l.useState(""),b=n.length>=3,u=!b||m===t.id,g=l.useRef(null);l.useEffect(()=>{var h;(h=g.current)==null||h.focus()},[]);const f=n.map(h=>h.scope_kind==="all"?"all agents":h.scope_kind==="agent"?`agent: ${h.scope_id}`:`group: ${h.scope_id}`);return e.jsx("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",role:"dialog","aria-modal":"true","aria-label":`Uninstall ${t.name}`,children:e.jsxs("div",{className:"bg-[#1a1a2e] border border-white/10 rounded-2xl p-6 w-full max-w-md shadow-2xl",children:[e.jsxs("div",{className:"flex items-start gap-3 mb-4",children:[e.jsx("div",{className:"p-2 rounded-lg bg-red-500/15 mt-0.5",children:e.jsx(xe,{size:20,className:"text-red-400","aria-hidden":!0})}),e.jsxs("div",{className:"flex-1 min-w-0",children:[e.jsxs("h2",{className:"text-base font-semibold text-shell-text",children:["Uninstall ",t.name,"?"]}),e.jsxs("p",{className:"text-xs text-shell-text-secondary mt-0.5",children:["v",t.version]})]}),e.jsx("button",{onClick:i,className:"text-shell-text-secondary hover:text-shell-text transition-colors","aria-label":"Close",children:e.jsx(U,{size:16})})]}),e.jsxs("div",{className:"space-y-2 mb-4",children:[n.length>0&&e.jsxs("div",{className:"text-sm text-shell-text-secondary bg-white/[0.03] rounded-lg px-3 py-2.5 border border-white/[0.06]",children:[e.jsxs("span",{className:"font-medium text-red-400",children:[n.length," attachment",n.length!==1?"s":""]})," will be revoked:"," ",e.jsx("span",{className:"text-shell-text",children:f.join(", ")})]}),e.jsx("p",{className:"text-xs text-shell-text-secondary",children:"This will stop the server process, remove all attachments, delete env secrets, and remove files from disk. This cannot be undone."})]}),b&&e.jsxs("div",{className:"mb-4",children:[e.jsxs(R,{htmlFor:"uninstall-confirm-input",className:"text-xs mb-1.5 block text-shell-text-secondary",children:["Type ",e.jsx("span",{className:"font-mono font-semibold text-shell-text",children:t.id})," to confirm"]}),e.jsx(E,{ref:g,id:"uninstall-confirm-input",value:m,onChange:h=>r(h.target.value),placeholder:t.id,className:"font-mono","aria-label":`Type ${t.id} to confirm uninstall`})]}),e.jsxs("div",{className:"flex gap-2 justify-end",children:[e.jsx(k,{variant:"outline",size:"sm",onClick:i,disabled:p,children:"Cancel"}),e.jsxs(k,{variant:"destructive",size:"sm",onClick:o,disabled:!u||p,"aria-label":`Confirm uninstall ${t.name}`,children:[p?e.jsx($,{size:14,className:"animate-spin mr-1"}):e.jsx(M,{size:14,className:"mr-1"}),"Uninstall"]})]})]})})}function je({serverId:t,agents:n,groups:o,capabilities:i,onSaved:p,onClose:m}){const[r,b]=l.useState("all"),[u,g]=l.useState(""),[f,h]=l.useState(""),[N,j]=l.useState(!0),[d,y]=l.useState(new Set),[S,s]=l.useState([]),[x,v]=l.useState(!1),[c,A]=l.useState(null),z=i.filter(a=>a.type==="tool"),_=n.filter(a=>(a.display_name||a.name).toLowerCase().includes(u.toLowerCase())),B=o.filter(a=>a.name.toLowerCase().includes(u.toLowerCase()));function V(a){y(w=>{const C=new Set(w);return C.has(a)?C.delete(a):C.add(a),C})}function H(){s(a=>[...a,""])}function K(a,w){s(C=>C.map((L,Z)=>Z===a?w:L))}function W(a){s(w=>w.filter((C,L)=>L!==a))}async function q(){if(r!=="all"&&!f){A("Select a specific agent or group.");return}v(!0),A(null);try{const a={scope_kind:r,scope_id:r==="all"?void 0:f,allowed_tools:N?[]:Array.from(d),allowed_resources:S.filter(C=>C.trim())},w=await fetch(`/api/mcp/servers/${encodeURIComponent(t)}/permissions`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)});if(!w.ok){const C=await w.json().catch(()=>({detail:"Failed to attach"}));A(C.detail??"Failed to attach"),v(!1);return}p()}catch{A("Network error"),v(!1)}}const X=r==="all"?"all agents":r==="agent"?f?`${f}`:"the selected agent":f?`group ${f}`:"the selected group",Y=N?"all tools":d.size===0?"no tools (unrestricted within this attachment)":`${d.size} tool${d.size!==1?"s":""}`,J=N?[]:z.filter(a=>!d.has(a.name));return e.jsx("div",{className:"fixed inset-0 z-50 flex items-end sm:items-center justify-center bg-black/60 backdrop-blur-sm",role:"dialog","aria-modal":"true","aria-label":"Attach permission",children:e.jsxs("div",{className:"bg-[#1a1a2e] border border-white/10 rounded-t-2xl sm:rounded-2xl p-5 w-full max-w-lg shadow-2xl max-h-[90vh] flex flex-col overflow-hidden",children:[e.jsxs("div",{className:"flex items-center justify-between mb-4 shrink-0",children:[e.jsx("h2",{className:"text-base font-semibold text-shell-text",children:"Attach Permission"}),e.jsx("button",{onClick:m,className:"text-shell-text-secondary hover:text-shell-text transition-colors","aria-label":"Close",children:e.jsx(U,{size:16})})]}),e.jsxs("div",{className:"overflow-y-auto flex-1 min-h-0 space-y-5 pr-1",children:[e.jsxs("div",{children:[e.jsx(R,{className:"text-xs mb-2 block text-shell-text-secondary",children:"Scope"}),e.jsx("div",{className:"flex gap-1 p-1 bg-white/[0.04] rounded-lg",children:["all","agent","group"].map(a=>e.jsx("button",{onClick:()=>{b(a),h(""),g("")},className:`flex-1 py-1.5 rounded-md text-xs font-medium transition-colors ${r===a?"bg-white/[0.1] text-shell-text shadow-sm":"text-shell-text-secondary hover:text-shell-text"}`,"aria-pressed":r===a,children:a==="all"?"All agents":a==="agent"?"Specific agent":"Specific group"},a))})]}),(r==="agent"||r==="group")&&e.jsxs("div",{children:[e.jsx(R,{className:"text-xs mb-2 block text-shell-text-secondary",children:r==="agent"?"Select agent":"Select group"}),e.jsx(E,{placeholder:`Search ${r}s...`,value:u,onChange:a=>g(a.target.value),className:"mb-2","aria-label":`Search ${r}s`}),e.jsxs("div",{className:"max-h-32 overflow-y-auto space-y-1",children:[(r==="agent"?_:B).map(a=>{const w="name"in a?a.name:a.id,C="display_name"in a&&a.display_name?a.display_name:("name"in a,a.name);return e.jsx("button",{onClick:()=>h(w),className:`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors ${f===w?"bg-accent/20 text-accent-foreground border border-accent/30":"hover:bg-white/[0.06] text-shell-text-secondary"}`,"aria-pressed":f===w,children:C},w)}),(r==="agent"?_:B).length===0&&e.jsx("p",{className:"text-xs text-shell-text-secondary text-center py-2",children:"No results"})]})]}),e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center justify-between mb-2",children:[e.jsx(R,{className:"text-xs text-shell-text-secondary",children:"Tools"}),e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("span",{className:"text-xs text-shell-text-secondary",children:"Unrestricted"}),e.jsx(ee,{checked:N,onCheckedChange:j,"aria-label":"Allow all tools (unrestricted)"})]})]}),!N&&e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"flex gap-2 mb-2",children:[e.jsx("button",{className:"text-xs text-accent hover:underline",onClick:()=>y(new Set(z.map(a=>a.name))),"aria-label":"Select all tools",children:"Select all"}),e.jsx("span",{className:"text-shell-text-secondary text-xs",children:"/"}),e.jsx("button",{className:"text-xs text-accent hover:underline",onClick:()=>y(new Set),"aria-label":"Select no tools",children:"None"})]}),e.jsxs("div",{className:"space-y-1 max-h-40 overflow-y-auto",children:[z.length===0&&e.jsx("p",{className:"text-xs text-shell-text-secondary py-2 text-center",children:"No tools discovered yet. Attach will be unrestricted within scope."}),z.map(a=>e.jsxs("label",{className:"flex items-start gap-2.5 p-2 rounded-lg hover:bg-white/[0.04] cursor-pointer",children:[e.jsx("input",{type:"checkbox",checked:d.has(a.name),onChange:()=>V(a.name),className:"mt-0.5 accent-blue-500","aria-label":`Allow tool ${a.name}`}),e.jsxs("div",{className:"min-w-0",children:[e.jsx("span",{className:"text-xs font-medium font-mono text-shell-text",children:a.name}),a.description&&e.jsx("p",{className:"text-[11px] text-shell-text-secondary truncate",children:a.description})]})]},a.name))]})]}),N&&e.jsx("p",{className:"text-xs text-shell-text-secondary",children:"All tools are allowed within this scope."})]}),e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center justify-between mb-2",children:[e.jsx(R,{className:"text-xs text-shell-text-secondary",children:"Resource patterns"}),e.jsxs("button",{onClick:H,className:"text-xs text-accent hover:underline flex items-center gap-1","aria-label":"Add resource pattern",children:[e.jsx(O,{size:12}),"Add pattern"]})]}),S.length===0&&e.jsx("p",{className:"text-xs text-shell-text-secondary",children:"No patterns — all resources unrestricted."}),e.jsx("div",{className:"space-y-1.5",children:S.map((a,w)=>e.jsxs("div",{className:"flex gap-1.5",children:[e.jsx(E,{value:a,onChange:C=>K(w,C.target.value),placeholder:"/workspace/* or https://api.github.com/*",className:"font-mono text-xs","aria-label":`Resource pattern ${w+1}`}),e.jsx("button",{onClick:()=>W(w),className:"text-shell-text-secondary hover:text-red-400 transition-colors shrink-0","aria-label":`Remove pattern ${w+1}`,children:e.jsx(U,{size:14})})]},w))})]}),e.jsx("div",{className:"bg-blue-500/[0.07] border border-blue-500/20 rounded-lg p-3",children:e.jsxs("p",{className:"text-xs text-blue-200 leading-relaxed",children:[e.jsx("span",{className:"font-semibold",children:X})," will be able to call:"," ",e.jsx("span",{className:"font-medium",children:Y}),".",J.length>0&&e.jsxs(e.Fragment,{children:[" ","It will NOT be able to call:"," ",e.jsx("span",{className:"font-medium",children:J.map(a=>a.name).join(", ")}),"."]}),S.filter(a=>a.trim()).length>0&&e.jsxs(e.Fragment,{children:[" ","Resource access restricted to ",S.filter(a=>a.trim()).length," pattern",S.filter(a=>a.trim()).length!==1?"s":"","."]})]})})]}),c&&e.jsx("p",{className:"text-xs text-red-400 mt-2 shrink-0",children:c}),e.jsxs("div",{className:"flex gap-2 justify-end mt-4 shrink-0",children:[e.jsx(k,{variant:"outline",size:"sm",onClick:m,disabled:x,children:"Cancel"}),e.jsxs(k,{size:"sm",onClick:q,disabled:x,"aria-label":"Save attachment",children:[x?e.jsx($,{size:14,className:"animate-spin mr-1"}):null,"Attach"]})]})]})})}function ge({server:t,selected:n,onSelect:o}){return e.jsxs("button",{onClick:o,className:`w-full text-left flex items-center gap-3 px-4 py-3 transition-colors hover:bg-white/[0.05] ${n?"bg-white/[0.07]":""}`,"aria-pressed":n,"aria-label":`${t.name}, ${P[t.status]}`,children:[e.jsxs("div",{className:"relative shrink-0",children:[e.jsx("div",{className:"w-8 h-8 rounded-lg bg-white/[0.06] flex items-center justify-center",children:e.jsx(I,{size:15,className:"text-shell-text-secondary","aria-hidden":!0})}),e.jsx("span",{className:`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-[#0f0f1e] ${he[t.status]}`,"aria-label":`Status: ${P[t.status]}`})]}),e.jsxs("div",{className:"flex-1 min-w-0",children:[e.jsxs("div",{className:"flex items-center gap-1.5 min-w-0",children:[e.jsx("span",{className:"text-sm font-medium text-shell-text truncate",children:t.name}),e.jsx("span",{className:`shrink-0 text-[10px] px-1.5 py-0.5 rounded font-medium ${me[t.transport]??"bg-zinc-500/20 text-zinc-300"}`,children:t.transport})]}),e.jsxs("div",{className:"flex items-center gap-2 mt-0.5 text-[11px] text-shell-text-secondary",children:[t.last_started_at&&e.jsxs("span",{children:["Started ",G(t.last_started_at)]}),t.pid&&e.jsxs("span",{children:["PID ",t.pid]})]})]})]})}function be({servers:t,loading:n,selectedId:o,onSelect:i,onOpenStore:p}){const m=ue(t);return n?e.jsx("div",{className:"flex items-center justify-center h-32",children:e.jsx($,{size:20,className:"animate-spin text-shell-text-secondary"})}):t.length===0?e.jsxs("div",{className:"flex flex-col items-center justify-center gap-4 h-40 px-6 text-center",children:[e.jsx(I,{size:32,className:"text-shell-text-tertiary opacity-40","aria-hidden":!0}),e.jsx("p",{className:"text-sm text-shell-text-secondary",children:"No MCP servers installed"}),e.jsxs(k,{size:"sm",variant:"outline",onClick:p,"aria-label":"Browse MCP servers in Store",children:[e.jsx(ne,{size:14,className:"mr-1.5"}),"Browse MCP servers in Store"]})]}):e.jsx("div",{children:pe.map(r=>{const b=m[r];return b.length===0?null:e.jsxs("div",{children:[e.jsx("div",{className:"px-4 py-1.5 text-[10px] font-semibold uppercase tracking-wider text-shell-text-tertiary border-b border-white/[0.04]",children:P[r]}),b.map(u=>e.jsx(ge,{server:u,selected:o===u.id,onSelect:()=>i(u.id)},u.id))]},r)})})}function ve({server:t,capabilities:n,attachments:o,onAction:i,onUninstall:p}){const m=n.filter(r=>r.type==="tool").length;return e.jsxs("div",{className:"p-4 space-y-5 overflow-y-auto h-full",children:[e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsx("span",{className:`text-xs px-2 py-1 rounded-full font-medium ${F[t.status]}`,children:P[t.status]}),t.pid&&e.jsxs("span",{className:"text-xs text-shell-text-secondary",children:["PID ",t.pid]}),e.jsx("div",{className:"flex-1"}),t.status!=="running"&&e.jsxs(k,{size:"sm",variant:"outline",onClick:()=>i("start"),"aria-label":"Start server",children:[e.jsx(ie,{size:13,className:"mr-1"}),"Start"]}),t.status==="running"&&e.jsxs(k,{size:"sm",variant:"outline",onClick:()=>i("stop"),"aria-label":"Stop server",children:[e.jsx(re,{size:13,className:"mr-1"}),"Stop"]}),e.jsxs(k,{size:"sm",variant:"outline",onClick:()=>i("restart"),"aria-label":"Restart server",children:[e.jsx(ce,{size:13,className:"mr-1"}),"Restart"]})]}),e.jsxs("div",{className:"space-y-2",children:[t.description&&e.jsx("p",{className:"text-sm text-shell-text-secondary",children:t.description}),e.jsxs("div",{className:"grid grid-cols-2 gap-2",children:[e.jsxs(T,{className:"px-3 py-2.5",children:[e.jsx("div",{className:"text-[10px] text-shell-text-tertiary uppercase tracking-wide",children:"Version"}),e.jsx("div",{className:"text-sm font-mono font-medium",children:t.version})]}),e.jsxs(T,{className:"px-3 py-2.5",children:[e.jsx("div",{className:"text-[10px] text-shell-text-tertiary uppercase tracking-wide",children:"Transport"}),e.jsx("div",{className:"text-sm font-medium",children:t.transport})]}),e.jsxs(T,{className:"px-3 py-2.5",children:[e.jsx("div",{className:"text-[10px] text-shell-text-tertiary uppercase tracking-wide",children:"Tools"}),e.jsx("div",{className:"text-sm font-medium",children:m})]}),e.jsxs(T,{className:"px-3 py-2.5",children:[e.jsx("div",{className:"text-[10px] text-shell-text-tertiary uppercase tracking-wide",children:"Attachments"}),e.jsx("div",{className:"text-sm font-medium",children:o.length})]})]}),t.last_error&&e.jsxs("div",{className:"bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2",children:[e.jsx("p",{className:"text-xs font-medium text-red-400 mb-0.5",children:"Last error"}),e.jsx("pre",{className:"text-[11px] text-red-300 whitespace-pre-wrap font-mono",children:t.last_error})]})]}),e.jsx("div",{className:"pt-2 border-t border-white/[0.06]",children:e.jsxs(k,{variant:"destructive",size:"sm",onClick:p,"aria-label":`Uninstall ${t.name}`,children:[e.jsx(M,{size:13,className:"mr-1.5"}),"Uninstall"]})})]})}function Ne({serverId:t,attachments:n,onRefresh:o}){const[i,p]=l.useState(!1),[m,r]=l.useState([]),[b,u]=l.useState([]),[g,f]=l.useState([]),[h,N]=l.useState(null);l.useEffect(()=>{fetch("/api/agents",{headers:{Accept:"application/json"}}).then(s=>s.json()).then(s=>r(Array.isArray(s)?s:s.agents??[])).catch(()=>{}),fetch("/api/relationships/groups",{headers:{Accept:"application/json"}}).then(s=>s.json()).then(s=>u(Array.isArray(s)?s:[])).catch(()=>{}),fetch(`/api/mcp/servers/${encodeURIComponent(t)}/capabilities`,{headers:{Accept:"application/json"}}).then(s=>s.json()).then(s=>f(Array.isArray(s)?s:s.capabilities??[])).catch(()=>{})},[t]);async function j(s){await fetch(`/api/mcp/servers/${encodeURIComponent(t)}/permissions/${s}`,{method:"DELETE"}),o()}function d(s){return s.scope_kind==="all"?"All agents":s.scope_kind==="agent"?`Agent: ${s.scope_id}`:`Group: ${s.scope_id}`}function y(s){return s.allowed_tools.length===0?"all tools":`${s.allowed_tools.length} tool${s.allowed_tools.length!==1?"s":""}`}function S(s){return s.allowed_resources.length===0?"no restriction":`${s.allowed_resources.length} pattern${s.allowed_resources.length!==1?"s":""}`}return e.jsxs("div",{className:"p-4 flex flex-col gap-4 overflow-y-auto h-full",children:[e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsx("p",{className:"text-xs text-shell-text-secondary",children:n.length===0?"No attachments. Server is unreachable to all agents.":`${n.length} attachment${n.length!==1?"s":""}`}),e.jsxs(k,{size:"sm",variant:"outline",onClick:()=>p(!0),"aria-label":"Add attachment",children:[e.jsx(O,{size:13,className:"mr-1"}),"Attach"]})]}),n.length===0&&e.jsxs("div",{className:"flex flex-col items-center justify-center py-10 gap-2 text-center",children:[e.jsx(I,{size:28,className:"text-shell-text-tertiary opacity-40","aria-hidden":!0}),e.jsx("p",{className:"text-sm text-shell-text-secondary",children:"Zero-access by default"}),e.jsx("p",{className:"text-xs text-shell-text-secondary max-w-xs",children:"Attach this server to an agent or group to grant access. Tool and resource restrictions are optional."})]}),e.jsx("div",{className:"space-y-2",children:n.map(s=>e.jsxs(T,{className:"overflow-hidden",children:[e.jsxs("div",{className:"flex items-center gap-3 px-3 py-2.5",children:[e.jsx("div",{className:"flex-1 min-w-0 space-y-1",children:e.jsxs("div",{className:"flex items-center gap-1.5 flex-wrap",children:[e.jsx("span",{className:"text-xs font-medium text-shell-text",children:d(s)}),e.jsx("span",{className:"text-[10px] px-1.5 py-0.5 rounded bg-white/[0.06] text-shell-text-secondary",children:y(s)}),e.jsx("span",{className:"text-[10px] px-1.5 py-0.5 rounded bg-white/[0.06] text-shell-text-secondary",children:S(s)})]})}),(s.allowed_tools.length>0||s.allowed_resources.length>0)&&e.jsx("button",{onClick:()=>N(h===s.id?null:s.id),className:"text-shell-text-secondary hover:text-shell-text transition-colors","aria-label":h===s.id?"Collapse details":"Expand details","aria-expanded":h===s.id,children:e.jsx(oe,{size:14,className:`transition-transform ${h===s.id?"rotate-180":""}`})}),e.jsx("button",{onClick:()=>j(s.id),className:"text-shell-text-secondary hover:text-red-400 transition-colors","aria-label":`Remove attachment for ${d(s)}`,children:e.jsx(U,{size:14})})]}),h===s.id&&e.jsxs("div",{className:"px-3 pb-2.5 space-y-2 border-t border-white/[0.06] pt-2",children:[s.allowed_tools.length>0&&e.jsxs("div",{children:[e.jsx("p",{className:"text-[10px] text-shell-text-tertiary uppercase tracking-wide mb-1",children:"Allowed tools"}),e.jsx("div",{className:"flex flex-wrap gap-1",children:s.allowed_tools.map(x=>e.jsx("span",{className:"text-[10px] px-1.5 py-0.5 rounded bg-blue-500/15 text-blue-300 font-mono",children:x},x))})]}),s.allowed_resources.length>0&&e.jsxs("div",{children:[e.jsx("p",{className:"text-[10px] text-shell-text-tertiary uppercase tracking-wide mb-1",children:"Resource patterns"}),e.jsx("div",{className:"flex flex-wrap gap-1",children:s.allowed_resources.map((x,v)=>e.jsx("span",{className:"text-[10px] px-1.5 py-0.5 rounded bg-violet-500/15 text-violet-300 font-mono",children:x},v))})]})]})]},s.id))}),i&&e.jsx(je,{serverId:t,agents:m,groups:b,capabilities:g,onSaved:()=>{p(!1),o()},onClose:()=>p(!1)})]})}function ye({serverId:t}){const[n,o]=l.useState([]),[i,p]=l.useState(!0),[m,r]=l.useState(!1),[b,u]=l.useState(null),[g,f]=l.useState(!1);l.useEffect(()=>{p(!0),fetch(`/api/mcp/servers/${encodeURIComponent(t)}/env`,{headers:{Accept:"application/json"}}).then(s=>s.json()).then(s=>{o(Object.entries(s??{}).map(([v,c])=>({key:v,value:c,revealed:!1})))}).catch(()=>o([])).finally(()=>p(!1))},[t]);function h(){o(s=>[...s,{key:"",value:"",revealed:!0}])}function N(s,x){o(v=>v.map((c,A)=>A===s?{...c,key:x}:c))}function j(s,x){o(v=>v.map((c,A)=>A===s?{...c,value:x}:c))}function d(s){o(x=>x.filter((v,c)=>c!==s))}function y(s){o(x=>x.map((v,c)=>c===s?{...v,revealed:!v.revealed}:v))}async function S(){r(!0),u(null),f(!1);const s={};for(const x of n)x.key.trim()&&(s[x.key.trim()]=x.value);try{const x=await fetch(`/api/mcp/servers/${encodeURIComponent(t)}/env`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(x.ok)f(!0),setTimeout(()=>f(!1),2e3);else{const v=await x.json().catch(()=>({detail:"Save failed"}));u(v.detail??"Save failed")}}catch{u("Network error")}r(!1)}return i?e.jsx("div",{className:"flex items-center justify-center h-24",children:e.jsx($,{size:18,className:"animate-spin text-shell-text-secondary"})}):e.jsxs("div",{className:"p-4 flex flex-col gap-4 overflow-y-auto h-full",children:[e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsx("p",{className:"text-xs text-shell-text-secondary",children:"Environment variables are stored as secrets."}),e.jsxs("button",{onClick:h,className:"text-xs text-accent hover:underline flex items-center gap-1","aria-label":"Add environment variable",children:[e.jsx(O,{size:12}),"Add"]})]}),e.jsx("div",{className:"space-y-2",children:n.map((s,x)=>e.jsxs("div",{className:"flex gap-2 items-center",children:[e.jsx(E,{value:s.key,onChange:v=>N(x,v.target.value),placeholder:"KEY",className:"font-mono text-xs w-36 shrink-0","aria-label":`Environment variable name ${x+1}`}),e.jsxs("div",{className:"flex-1 relative",children:[e.jsx(E,{type:s.revealed?"text":"password",value:s.value,onChange:v=>j(x,v.target.value),placeholder:"value",className:"font-mono text-xs pr-8","aria-label":`Environment variable value ${x+1}`}),e.jsx("button",{onClick:()=>y(x),className:"absolute right-2 top-1/2 -translate-y-1/2 text-shell-text-tertiary hover:text-shell-text transition-colors","aria-label":s.revealed?"Hide value":"Reveal value",children:s.revealed?e.jsx("span",{className:"text-[10px]",children:"hide"}):e.jsx("span",{className:"text-[10px]",children:"show"})})]}),e.jsx("button",{onClick:()=>d(x),className:"text-shell-text-secondary hover:text-red-400 transition-colors shrink-0","aria-label":`Remove variable ${s.key||x+1}`,children:e.jsx(U,{size:14})})]},x))}),b&&e.jsx("p",{className:"text-xs text-red-400",children:b}),e.jsxs(k,{size:"sm",onClick:S,disabled:m,className:"self-start","aria-label":"Save environment variables",children:[g?e.jsx(D,{size:13,className:"mr-1 text-emerald-400"}):m?e.jsx($,{size:13,className:"animate-spin mr-1"}):null,g?"Saved":"Save"]})]})}function we({serverId:t}){const[n,o]=l.useState(""),[i,p]=l.useState(!0),[m,r]=l.useState(!1),[b,u]=l.useState(null),[g,f]=l.useState(!1);l.useEffect(()=>{p(!0),fetch(`/api/mcp/servers/${encodeURIComponent(t)}/config`,{headers:{Accept:"application/json"}}).then(j=>j.json()).then(j=>o(JSON.stringify(j,null,2))).catch(()=>o("{}")).finally(()=>p(!1))},[t]);let h=!0;try{JSON.parse(n)}catch{h=!1}async function N(){if(h){r(!0),u(null),f(!1);try{const j=JSON.parse(n),d=await fetch(`/api/mcp/servers/${encodeURIComponent(t)}/config`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(j)});if(d.ok)f(!0),setTimeout(()=>f(!1),2e3);else{const y=await d.json().catch(()=>({detail:"Save failed"}));u(y.detail??"Save failed")}}catch{u("Network error")}r(!1)}}return i?e.jsx("div",{className:"flex items-center justify-center h-24",children:e.jsx($,{size:18,className:"animate-spin text-shell-text-secondary"})}):e.jsxs("div",{className:"p-4 flex flex-col gap-3 h-full overflow-hidden",children:[e.jsx("p",{className:"text-xs text-shell-text-secondary shrink-0",children:"JSON configuration overrides for this server."}),e.jsx(Q,{value:n,onChange:j=>o(j.target.value),className:`flex-1 font-mono text-xs resize-none ${h?"":"border-red-500/50"}`,"aria-label":"Server configuration JSON","aria-invalid":!h,spellCheck:!1}),!h&&e.jsx("p",{className:"text-xs text-red-400 shrink-0",children:"Invalid JSON"}),b&&e.jsx("p",{className:"text-xs text-red-400 shrink-0",children:b}),e.jsxs(k,{size:"sm",onClick:N,disabled:!h||m,className:"self-start shrink-0","aria-label":"Save configuration",children:[g?e.jsx(D,{size:13,className:"mr-1 text-emerald-400"}):m?e.jsx($,{size:13,className:"animate-spin mr-1"}):null,g?"Saved":"Save"]})]})}function Se({serverId:t}){const[n,o]=l.useState([]),[i,p]=l.useState(!1),[m,r]=l.useState(!1),[b,u]=l.useState(!1),g=l.useRef(null),f=l.useRef(!1),h=l.useRef(null);f.current=m,l.useEffect(()=>{const d=new EventSource(`/api/mcp/servers/${encodeURIComponent(t)}/logs/stream`);return h.current=d,d.onopen=()=>p(!0),d.onerror=()=>p(!1),d.onmessage=y=>{f.current||o(S=>[...S.slice(-500),y.data])},()=>{d.close(),h.current=null}},[t]),l.useEffect(()=>{!m&&g.current&&(g.current.scrollTop=g.current.scrollHeight)},[n,m]);function N(){const d=g.current;if(!d)return;const y=d.scrollHeight-d.scrollTop-d.clientHeight<40;!y&&!f.current&&r(!0),y&&f.current&&r(!1)}async function j(){await navigator.clipboard.writeText(n.join(`
`)),u(!0),setTimeout(()=>u(!1),1500)}return e.jsxs("div",{className:"flex flex-col h-full overflow-hidden",children:[e.jsxs("div",{className:"flex items-center gap-3 px-4 py-2 border-b border-white/[0.06] shrink-0",children:[e.jsx("span",{className:`w-2 h-2 rounded-full ${i?"bg-emerald-500":"bg-zinc-500"}`,"aria-label":i?"Connected":"Disconnected"}),e.jsx("span",{className:"text-xs text-shell-text-secondary",children:i?"Live":"Disconnected"}),m&&e.jsx("span",{className:"text-xs text-amber-400",children:"Paused — scroll to bottom to resume"}),e.jsx("div",{className:"flex-1"}),e.jsx(k,{size:"sm",variant:"ghost",onClick:j,"aria-label":"Copy all logs",children:b?e.jsx(D,{size:13}):e.jsx(de,{size:13})})]}),e.jsxs("div",{ref:g,onScroll:N,className:"flex-1 overflow-y-auto p-4 font-mono text-[11px] leading-relaxed text-shell-text-secondary whitespace-pre-wrap",role:"log","aria-label":"Server logs","aria-live":"polite",children:[n.length===0&&e.jsx("span",{className:"text-shell-text-tertiary",children:"Waiting for log lines..."}),n.map((d,y)=>{const S=/error|exception|traceback/i.test(d);return e.jsx("div",{className:S?"text-red-400":"",children:d},y)})]})]})}function Ce({serverId:t}){const[n,o]=l.useState([]);return l.useEffect(()=>{function i(){fetch(`/api/mcp/servers/${encodeURIComponent(t)}/used-by`,{headers:{Accept:"application/json"}}).then(m=>m.json()).then(m=>o(Array.isArray(m)?m:[])).catch(()=>{})}i();const p=setInterval(i,3e3);return()=>clearInterval(p)},[t]),e.jsx("div",{className:"p-4 overflow-y-auto h-full",children:n.length===0?e.jsxs("div",{className:"flex flex-col items-center justify-center gap-2 py-12 text-center",children:[e.jsx("p",{className:"text-sm text-shell-text-secondary",children:"No agents currently calling this server"}),e.jsx("p",{className:"text-xs text-shell-text-tertiary",children:"Updates every 3 seconds"})]}):e.jsx("div",{className:"space-y-2",children:n.map((i,p)=>e.jsxs(T,{className:"px-3 py-2.5 flex items-center gap-3",children:[e.jsx("span",{className:"w-2 h-2 rounded-full bg-emerald-500 shrink-0","aria-label":"Active"}),e.jsxs("div",{className:"flex-1 min-w-0",children:[e.jsx("p",{className:"text-sm font-medium text-shell-text",children:i.agent_name}),i.tool&&e.jsx("p",{className:"text-xs text-shell-text-secondary font-mono",children:i.tool})]}),i.started_at&&e.jsx("span",{className:"text-xs text-shell-text-secondary",children:G(i.started_at)})]},p))})})}function ke({server:t,onRefreshList:n,onDeselect:o}){const[i,p]=l.useState("overview"),[m,r]=l.useState([]),[b,u]=l.useState([]),[g,f]=l.useState(null),[h,N]=l.useState(!1),[j,d]=l.useState(!1),y=ae(c=>c.addNotification);function S(){fetch(`/api/mcp/servers/${encodeURIComponent(t.id)}/permissions`,{headers:{Accept:"application/json"}}).then(c=>c.json()).then(c=>u(Array.isArray(c)?c:[])).catch(()=>{})}l.useEffect(()=>{S(),fetch(`/api/mcp/servers/${encodeURIComponent(t.id)}/capabilities`,{headers:{Accept:"application/json"}}).then(c=>c.json()).then(c=>r(Array.isArray(c)?c:c.capabilities??[])).catch(()=>{})},[t.id]);async function s(c){f(c);try{await fetch(`/api/mcp/servers/${encodeURIComponent(t.id)}/${c}`,{method:"POST"}),n()}catch{y({source:"mcp",title:"Action failed",body:`Failed to ${c} ${t.name}`,level:"error"})}f(null)}async function x(){d(!0);try{const A=await(await fetch(`/api/mcp/servers/${encodeURIComponent(t.id)}`,{method:"DELETE"})).json().catch(()=>({})),z=A.agents_affected??b.length,_=A.secrets_dropped??0;y({source:"mcp",title:`Removed ${t.name}`,body:`${z} agent${z!==1?"s":""} lost access, ${_} secret${_!==1?"s":""} dropped.`,level:"info"}),N(!1),o(),n()}catch{y({source:"mcp",title:"Uninstall failed",body:`Could not uninstall ${t.name}`,level:"error"})}d(!1)}const v=[{id:"overview",label:"Overview"},{id:"permissions",label:"Permissions"},{id:"env",label:"Env"},{id:"config",label:"Config"},{id:"logs",label:"Logs"},{id:"used-by",label:"Used by"}];return e.jsxs("div",{className:"flex flex-col h-full overflow-hidden",children:[e.jsxs("div",{className:"shrink-0 px-4 py-3 border-b border-white/[0.06] flex items-center gap-3",children:[e.jsx("div",{className:"w-8 h-8 rounded-lg bg-white/[0.06] flex items-center justify-center shrink-0",children:e.jsx(I,{size:15,className:"text-shell-text-secondary","aria-hidden":!0})}),e.jsxs("div",{className:"flex-1 min-w-0",children:[e.jsx("h2",{className:"text-sm font-semibold text-shell-text truncate",children:t.name}),e.jsxs("p",{className:"text-[11px] text-shell-text-secondary",children:["v",t.version]})]}),e.jsx("span",{className:`text-[10px] px-2 py-0.5 rounded-full font-medium ${F[t.status]}`,children:P[t.status]}),g&&e.jsx($,{size:14,className:"animate-spin text-shell-text-secondary shrink-0","aria-label":"Loading"})]}),e.jsx("div",{className:"shrink-0 border-b border-white/[0.06] overflow-x-auto",children:e.jsx("div",{className:"flex min-w-max px-2",role:"tablist","aria-label":"Server detail tabs",children:v.map(c=>e.jsx("button",{role:"tab","aria-selected":i===c.id,onClick:()=>p(c.id),className:`px-3 py-2.5 text-xs font-medium whitespace-nowrap transition-colors border-b-2 ${i===c.id?"border-accent text-shell-text":"border-transparent text-shell-text-secondary hover:text-shell-text"}`,children:c.label},c.id))})}),e.jsxs("div",{className:"flex-1 min-h-0 overflow-hidden",children:[i==="overview"&&e.jsx(ve,{server:t,capabilities:m,attachments:b,onAction:s,onUninstall:()=>N(!0)}),i==="permissions"&&e.jsx(Ne,{serverId:t.id,attachments:b,onRefresh:S}),i==="env"&&e.jsx(ye,{serverId:t.id}),i==="config"&&e.jsx(we,{serverId:t.id}),i==="logs"&&e.jsx(Se,{serverId:t.id}),i==="used-by"&&e.jsx(Ce,{serverId:t.id})]}),h&&e.jsx(fe,{server:t,attachments:b,loading:j,onConfirm:x,onClose:()=>N(!1)})]})}function Ie({windowId:t}){const[n,o]=l.useState([]),[i,p]=l.useState(!0),[m,r]=l.useState(null),b=se(j=>j.openWindow),u=l.useCallback(async()=>{try{const j=await fetch("/api/mcp/servers",{headers:{Accept:"application/json"}});if(j.ok){const d=await j.json();o(Array.isArray(d)?d:d.servers??[])}}catch{}finally{p(!1)}},[]);l.useEffect(()=>{u();const j=setInterval(u,1e4);return()=>clearInterval(j)},[u]);const g=n.find(j=>j.id===m)??null;function f(){const j=le("store");j&&b("store",j.defaultSize)}const h=e.jsx(be,{servers:n,loading:i,selectedId:m,onSelect:r,onOpenStore:f}),N=g?e.jsx(ke,{server:g,onRefreshList:u,onDeselect:()=>r(null)}):null;return e.jsx(te,{list:h,detail:N,selectedId:m,onBack:()=>r(null),listTitle:"MCP",detailTitle:(g==null?void 0:g.name)??""})}export{Ie as MCPApp};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Copy-all logs now mutates content with extra leading spaces.

In Line 1 / Line 2, using n.join("\n ") alters pasted logs (every subsequent line is prefixed with a space). This can break exact log reproduction and downstream parsing.

Proposed fix
-async function j(){await navigator.clipboard.writeText(n.join(`
- `)),u(!0),setTimeout(()=>u(!1),1500)}
+async function j(){await navigator.clipboard.writeText(n.join(`
+`)),u(!0),setTimeout(()=>u(!1),1500)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@static/desktop/assets/MCPApp-Cq-bUfV5.js` around lines 1 - 2, The copy-all
logs action in Se is joining lines with a leading-space separator which inserts
extra spaces; update the navigator.clipboard.writeText call inside Se's j()
handler to join the logs with a plain newline (use n.join("\n")) so pasted logs
preserve exact original lines (locate the navigator.clipboard.writeText call in
function Se where n.join is used).

@jaylfc jaylfc merged commit 7b90e81 into master Apr 20, 2026
8 checks passed
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.

1 participant