Messages PWA Phase 1 — mobile UX + install prompt#238
Conversation
…et, install banner
📝 WalkthroughWalkthroughThis 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Code Review SummaryStatus: No Issues Found | Recommendation: Merge ✅ Incremental Review: All new changes reviewed. No issues found. Files Reviewed (15 files total | 2 changed)Changed Files:
Previously Reviewed:
Reviewed by seed-2-0-pro-260328 · 195,292 tokens |
There was a problem hiding this comment.
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 foroffsetTopin keyboard inset calculation.The hook's
read()function usesvv.offsetTopin the calculation:Math.max(0, window.innerHeight - vv.height - vv.offsetTop). The current test keepsoffsetTop: 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
+ 60offset 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 * 1000directly. IfDISMISS_MSin the component changes, this test could silently become inaccurate. Consider importing and using the constant, or adding+ 1to 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 auseMemoto 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
📒 Files selected for processing (28)
desktop/chat.htmldesktop/src/ChatStandalone.tsxdesktop/src/apps/MessagesApp.tsxdesktop/src/apps/chat/ThreadPanel.tsxdesktop/src/hooks/__tests__/use-visual-viewport.test.tsdesktop/src/hooks/use-visual-viewport.tsdesktop/src/shell/BottomSheet.tsxdesktop/src/shell/InstallPromptBanner.tsxdesktop/src/shell/__tests__/BottomSheet.test.tsxdesktop/src/shell/__tests__/InstallPromptBanner.test.tsxdesktop/tsconfig.tsbuildinfodesktop/vitest.setup.tsdocs/superpowers/plans/2026-04-20-messages-pwa-phase-1.mddocs/superpowers/specs/2026-04-20-messages-pwa-phase-1-design.mdstatic/desktop/assets/MCPApp-Cmbj2-Z_.jsstatic/desktop/assets/MessagesApp-BkrxHSyC.jsstatic/desktop/assets/MessagesApp-BlnZE9ab.jsstatic/desktop/assets/ProvidersApp-Bag-W8Oo.jsstatic/desktop/assets/SettingsApp-BdbbrvJ2.jsstatic/desktop/assets/chat-BFUaevuL.jsstatic/desktop/assets/chat-D-JXvZwU.jsstatic/desktop/assets/main-CMjABSro.jsstatic/desktop/assets/tokens-BrwHLUZg.cssstatic/desktop/assets/tokens-C-PTlQoZ.cssstatic/desktop/assets/tokens-C63es8oZ.jsstatic/desktop/chat.htmlstatic/desktop/index.htmltests/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
| 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 }); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's look at the actual file to confirm the current state
cat -n desktop/vitest.setup.tsRepository: 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.
| 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.
| **`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. |
There was a problem hiding this comment.
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}; | |||
There was a problem hiding this comment.
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.
| 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() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
BottomSheetfor mobile is a clean approach. Theopen={true}is safe here since this block only executes whenoverflowMenuis truthy.Consider adding
labelledByto the BottomSheet for improved accessibility ifMessageOverflowMenucontains 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 insideMessageOverflowMenu.🤖 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 number60to a named constant.The hardcoded
60pxadded tokeyboardInsetappears 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#rootelement to improve error clarity.The code currently calls
createRoot(document.getElementById("root"))without validation. While the#rootelement 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
📒 Files selected for processing (10)
desktop/src/apps/MessagesApp.tsxdesktop/src/shell/BottomSheet.tsxstatic/desktop/assets/MCPApp-Cq-bUfV5.jsstatic/desktop/assets/MessagesApp-BG8aIEeG.jsstatic/desktop/assets/ProvidersApp-BY6yJVKl.jsstatic/desktop/assets/SettingsApp-F200Wg__.jsstatic/desktop/assets/chat-BrDx525_.jsstatic/desktop/assets/main-BzBv0_mH.jsstatic/desktop/chat.htmlstatic/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,{})})); | |||
There was a problem hiding this comment.
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.
| 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}; |
There was a problem hiding this comment.
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).
Summary
Phase 1 of the Messages PWA — mobile-native UX for taOS talk. Service worker / offline caching is deferred to Phase 2.
Mobile UX
visualViewport.heightso the input floats above the soft keyboard; message list adjusts in kind.PWA install
InstallPromptBannerlistens forbeforeinstallpromptand shows a dismissible top banner on mobile. 30-day suppression after "Not now". Hidden when already installed (display-mode: standalone).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)ThreadPanelgainsisFullscreenprop.MessagesAppconditionally swaps overflow menu to bottom sheet and adjusts composer padding.ChatStandalonemounts<InstallPromptBanner>at the root.Test plan
Summary by CodeRabbit
New Features
Tests