Skip to content

Chat Phase 2b-2a — per-message affordances + pinning#237

Open
jaylfc wants to merge 40 commits intomasterfrom
feat/chat-phase-2b-2a-per-msg
Open

Chat Phase 2b-2a — per-message affordances + pinning#237
jaylfc wants to merge 40 commits intomasterfrom
feat/chat-phase-2b-2a-per-msg

Conversation

@jaylfc
Copy link
Copy Markdown
Owner

@jaylfc jaylfc commented Apr 19, 2026

Summary

Phase 2b-2a: edit-own, delete-own (soft), copy-link (deep-link URL), mark-unread, pinning (human-only) with agent pin-request via 📌 self-reaction.

Per-message affordances

  • overflow on hover toolbar opens dropdown: Edit, Delete, Copy link, Pin/Unpin, Mark unread.
  • Edit: text-only, always editable, (edited) marker in author row.
  • Delete: soft via `deleted_at` column — UI renders tombstone "This message was deleted". Thread replies stay anchored.
  • Copy link: `https:///chat/?msg=` → opening scrolls + briefly outlines target.
  • Mark unread: rewinds `chat_read_positions.last_read_at` for (user, channel).

Pinning

  • Humans pin/unpin via `⋯` → Pin. 50/channel cap. Channel header shows `📌 N` badge with popover (Jump to →).
  • Agents request pins by adding 📌 reaction to their own messages → humans see inline `@agent wants to pin this` pill with one-click approval.

New backend endpoints

  • `GET /api/chat/channels/{id}/pins`
  • `POST /api/chat/messages/{id}/pin` (human-only; 409 when cap hit)
  • `DELETE /api/chat/messages/{id}/pin`
  • `PATCH /api/chat/messages/{id}` (author-only text edit, rejects extra fields)
  • `DELETE /api/chat/messages/{id}` (author-only soft delete, idempotent)
  • `POST /api/chat/channels/{id}/read-cursor/rewind` (mark-unread)

Based on #236 (Phase 2b-1)

Branched off `feat/chat-phase-2b-1-threads-attachments`. Should merge after #236.

Test plan

  • Backend: 49 tests across 5 new/updated files pass.
  • Desktop: 22 new vitest tests pass; full suite 264/267 (3 pre-existing snap-zones failures unrelated).
  • Desktop bundle rebuilt.
  • Playwright E2E stubs written; gated on `TAOS_E2E_URL`.
  • Manual smoke: Edit own message → see `(edited)` + updated text. Delete own → tombstone. Pin a message → badge + popover. Agent adds 📌 to own message → pin-request pill. Copy link and reopen → scroll+highlight.

Summary by CodeRabbit

  • New Features
    • Message threading with dedicated thread panels for replies
    • File attachment upload and image gallery viewing
    • Message editing and deletion capabilities
    • Message pinning with pin management UI
    • Enhanced message action menus with quick reactions
    • File picker supporting disk and workspace sources
    • Direct message deep-linking via URL parameters
    • Mark message as unread functionality

jaylfc added 30 commits April 19, 2026 21:03
… + shared file picker

19 tasks covering attachments column migration, from-path endpoint,
thread messages query + GET endpoint, thread recipient resolver, router
integration, /help command + intercept, bridge event payload + attachment
footer, VfsBrowser refactor, SharedFilePickerDialog shell primitive,
chat-attachments-api client, AttachmentsBar + Gallery + Lightbox, hover
actions + thread indicator + panel, chat-guide.md, MessagesApp integration,
bundle rebuild, Playwright E2E.
…dPanel + use-thread-panel + GET message-by-id
…ete = soft

- Add deleted_at REAL column to MESSAGES_SCHEMA
- Add guarded ALTER TABLE migration in ChatMessageStore.init() for existing DBs
- Add soft_delete_message() method; delete_message() now delegates to it
- Filter deleted_at IS NULL in get_messages() so soft-deleted rows are hidden
- Update test_delete_message to assert row preserved with deleted_at set
- Add test_soft_delete_sets_deleted_at and test_soft_delete_nonexistent_returns_false
- Update restore test cleanup to use delete_channel_messages (hard delete) so restore reimport assertions remain valid
@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot bot commented Apr 19, 2026

Code Review Summary

Status: 10 Issues Found | Recommendation: Address before merge

Fix these issues in Kilo Cloud

Overview

Severity Count
CRITICAL 2
WARNING 6
SUGGESTION 2
Issue Details (click to expand)

CRITICAL

File Line Issue
desktop/src/apps/MessagesApp.tsx 468 Unhandled Promise Rejection. uploadDiskFile() is called without a .catch() handler on drag & drop uploads. Failed uploads will trigger an unhandled rejection event which may crash the page.
desktop/src/apps/MessagesApp.tsx 543 Unhandled Promise Rejection. uploadDiskFile() is called without a .catch() handler on paste uploads. Failed uploads will trigger an unhandled rejection event which may crash the page.
desktop/src/apps/chat/AttachmentLightbox.tsx 13 Memory Leak: onClose is missing from useEffect dependency array. This creates a stale closure. The event listener will not be properly removed on unmount, and old callback references will remain in memory.

WARNING

File Line Issue
desktop/src/apps/chat/AttachmentLightbox.tsx 26 Null access risk: images[idx]! uses non-null assertion without boundary checking. External state updates could place idx out of array bounds causing a runtime exception.
desktop/src/apps/chat/ThreadPanel.tsx 30 Missing error handling: Parent message fetch has no error handling. Failed requests leave permanent loading/empty UI state with no user feedback.
desktop/src/apps/chat/ThreadPanel.tsx 39 Missing error handling: Thread messages fetch has no error handling. Failed requests leave permanent loading/empty UI state with no user feedback.
desktop/src/apps/MessagesApp.tsx 491 Race Condition: Rapid pin/unpin clicks cause concurrent fetch operations with no cancellation. Stale responses may overwrite correct pin state.
desktop/src/apps/chat/ThreadIndicator.tsx 22 Off-by-one error: Math.max(0, now - ts) returns 0 for future timestamps, incorrectly showing "just now" for future dates. Use Math.abs() for clock drift tolerance.
desktop/src/apps/MessagesApp.tsx 493 Missing type safety: listPins() returns unknown[] cast directly to PinnedMessage[] without runtime validation. Malformed API responses will cause render crashes.
desktop/src/apps/MessagesApp.tsx 439 Silent error swallowing: /api/auth/me fetch has empty catch clause. All network errors are completely ignored with no logging or user feedback.

SUGGESTION

File Issue
All new fetch calls Missing abort controllers: No fetch calls in new useEffect hooks implement AbortController. Unmounting mid-request causes React state update warnings and minor memory leaks.
desktop/src/apps/MessagesApp.tsx Unhandled async event handlers: All async button event handlers (delete, copy link, pin, mark unread, pin request) are async. React does not await returned promises - failures will produce unhandled rejections unless caught internally.
Files Reviewed (16 files)
  • desktop/src/apps/MessagesApp.tsx - 5 issues
  • desktop/src/apps/chat/AttachmentGallery.tsx
  • desktop/src/apps/chat/AttachmentLightbox.tsx - 2 issues
  • desktop/src/apps/chat/AttachmentsBar.tsx
  • desktop/src/apps/chat/MessageEditor.tsx
  • desktop/src/apps/chat/MessageHoverActions.tsx
  • desktop/src/apps/chat/MessageOverflowMenu.tsx
  • desktop/src/apps/chat/MessageTombstone.tsx
  • desktop/src/apps/chat/PinBadge.tsx
  • desktop/src/apps/chat/PinRequestAffordance.tsx
  • desktop/src/apps/chat/PinnedMessagesPopover.tsx
  • desktop/src/apps/chat/ThreadIndicator.tsx - 1 issue
  • desktop/src/apps/chat/ThreadPanel.tsx - 2 issues
  • desktop/src/apps/chat/__tests__/AttachmentGallery.test.tsx
  • desktop/src/apps/chat/__tests__/AttachmentsBar.test.tsx
  • desktop/src/apps/chat/__tests__/MessageEditor.test.tsx
  • desktop/src/apps/chat/__tests__/MessageHoverActions.test.tsx
  • desktop/src/apps/chat/__tests__/MessageOverflowMenu.test.tsx

Reviewed by seed-2-0-pro-260328 · 171,325 tokens

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 19, 2026

📝 Walkthrough

Walkthrough

Implement comprehensive chat messaging features including threaded message replies, file attachment uploads with multi-source picker, per-message affordances (edit/delete/pin/mark-unread), pinned message management with UI, and message deep-linking via query parameters.

Changes

Cohort / File(s) Summary
Main Chat App Update
desktop/src/apps/MessagesApp.tsx
Replace hover reaction UI with new MessageHoverActions component; extend Message model with metadata fields (pin_requested, deleted_at, attachments, reply_count, last_reply_at); add auth lookup for currentUserId; implement deep-link scrolling via ?msg=<id> parameter; wire pinned-message support (listPins, PinBadge, PinnedMessagesPopover); centralize message actions in MessageOverflowMenu; add message editing/deletion UI (MessageEditor, MessageTombstone); integrate thread panel and useThreadPanel hook; refactor attachment workflow with pendingAttachments state, multi-file openFilePicker, and HTTP POST sending when attachments present; update composer for paste and attachment state.
Chat UI Components
desktop/src/apps/chat/AttachmentGallery.tsx, AttachmentLightbox.tsx, AttachmentsBar.tsx, MessageEditor.tsx, MessageHoverActions.tsx, MessageOverflowMenu.tsx, MessageTombstone.tsx, PinBadge.tsx, PinRequestAffordance.tsx, PinnedMessagesPopover.tsx, ThreadIndicator.tsx, ThreadPanel.tsx
Add 12 new chat UI components for attachment gallery/lightbox, pending attachments bar, message editing/actions, pin affordances, pinned message popover, thread indicators, and thread reply panel.
Chat API Utilities
desktop/src/lib/chat-attachments-api.ts, chat-messages-api.ts, use-thread-panel.ts
Add REST API wrappers for file uploads (uploadDiskFile, attachmentFromPath), message operations (pinMessage, unpinMessage, listPins, editMessage, deleteMessage, markUnread), and thread panel state management hook.
File Picker & VFS
desktop/src/shell/FilePicker.tsx, VfsBrowser.tsx, file-picker-api.ts
Introduce multi-source file selection components: FilePicker modal supporting disk, workspace, and agent-workspace sources with tab-based UI; VfsBrowser for navigating workspace file systems with single/multi-select; file-picker-api for mounting/unmounting picker dialog reactively.
Component Tests
desktop/src/apps/chat/__tests__/AttachmentGallery.test.tsx, AttachmentsBar.test.tsx, MessageEditor.test.tsx, MessageHoverActions.test.tsx, MessageOverflowMenu.test.tsx, MessageTombstone.test.tsx, PinBadge.test.tsx, PinRequestAffordance.test.tsx, PinnedMessagesPopover.test.tsx, ThreadIndicator.test.tsx, ...__tests__/FilePicker.test.tsx, VfsBrowser.test.tsx
Add 12 Vitest test suites covering rendering, interactions, and callbacks for new chat and file-picker components.
API Tests
desktop/src/lib/__tests__/chat-attachments-api.test.ts, chat-messages-api.test.ts
Add Vitest test suites validating HTTP request structure and error handling for attachment upload and message operation APIs.
Theme & Build
desktop/src/theme/tokens.css, desktop/tsconfig.tsbuildinfo
Add .data-highlight CSS class with yellow outline and animation for message highlighting; update TypeScript build metadata with new module entries.
Documentation
docs/chat-guide.md, docs/superpowers/plans/2026-04-19-chat-phase-2b-1-threads-attachments.md, docs/superpowers/plans/2026-04-19-chat-phase-2b-2a-per-msg.md, docs/superpowers/specs/2026-04-19-chat-phase-2b-2a-per-msg-design.md
Add comprehensive chat user guide, implementation plans for threaded messages and per-message affordances, and design specification for pin/edit/delete/mark-unread UX and backend schema.
Bundled Assets
static/desktop/assets/ActivityApp-*.js, AgentBrowsersApp-*.js, BrowserApp-*.js, CalendarApp-*.js, ChannelsApp-*.js, ClusterApp-*.js, ContactsApp-*.js, FilesApp-*.js, GitHubApp-*.js, ImageViewerApp-*.js, ImagesApp-*.js, ImportApp-*.js, LibraryApp-*.js, MCPApp-*.js, MemoryApp-*.js, MessagesApp-*.js, MobileSplitView-*.js, ModelsApp-*.js
Update/rebuild compiled desktop app bundles: swap icon/UI module imports, update MessagesApp with new chat features (pins, threads, attachments, message actions, file picker), add/remove app bundles, refresh MobileSplitView with responsive pane logic.

Sequence Diagrams

sequenceDiagram
    participant User
    participant MessageHoverActions
    participant MessageOverflowMenu
    participant API as REST API
    participant State

    User->>MessageHoverActions: Hover over message
    MessageHoverActions->>MessageHoverActions: Show toolbar (React/OverflowMore)
    User->>MessageOverflowMenu: Click "More" button
    MessageOverflowMenu->>MessageOverflowMenu: Display menu (Edit/Delete/Pin/Unread)
    
    alt Edit
        User->>State: Click Edit
        State->>MessageEditor: Show textarea
        User->>MessageEditor: Enter new content + Enter
        MessageEditor->>API: PATCH /api/chat/messages/{id}
        API-->>State: Updated message
        State->>State: Re-render content
    else Delete
        User->>State: Click Delete
        State->>State: window.confirm()
        User-->>State: Confirm deletion
        State->>API: DELETE /api/chat/messages/{id}
        API-->>State: Success
        State->>MessageTombstone: Render "deleted" state
    else Pin
        User->>State: Click Pin
        State->>API: POST /api/chat/messages/{id}/pin
        API-->>State: Pinned response
        State->>PinBadge: Update pin count
        PinBadge-->>User: Show updated count
    else Mark Unread
        User->>State: Click Mark Unread
        State->>API: POST /api/chat/channels/{id}/read-cursor/rewind
        API-->>State: Read cursor updated
    end
Loading
sequenceDiagram
    participant User
    participant Composer
    participant FilePicker as File Picker
    participant API as Upload API
    participant AttachmentsBar
    participant State

    User->>Composer: Paste image or click attach icon
    Composer->>FilePicker: openFilePicker({ sources: [...] })
    FilePicker->>FilePicker: Render modal (Disk/Workspace tabs)
    User->>FilePicker: Select file(s)
    FilePicker-->>Composer: Return FileSelection[]
    
    Composer->>State: Add to pendingAttachments
    State->>AttachmentsBar: Render bar with items
    
    loop Per pending attachment
        State->>API: POST /api/chat/upload (FormData)
        API-->>State: AttachmentRecord { url, filename, ... }
        State->>State: Update pendingAttachments[].record
        AttachmentsBar->>AttachmentsBar: Hide "uploading" spinner
    end
    
    User->>Composer: Type message + press Enter
    Composer->>API: POST /api/chat/messages { content, attachments }
    API-->>State: Message created with attachments
    State->>AttachmentsBar: Clear bar
Loading
sequenceDiagram
    participant User
    participant Message
    participant ThreadIndicator
    participant ThreadPanel
    participant API as Thread API
    participant State

    User->>Message: Hover message with reply_count > 0
    Message->>ThreadIndicator: Render button + count
    User->>ThreadIndicator: Click "Open thread"
    ThreadIndicator->>State: openThreadFor(channelId, messageId)
    State->>ThreadPanel: Mount with parentId
    
    ThreadPanel->>API: GET /api/chat/messages/{id} (parent)
    API-->>ThreadPanel: Parent message content
    
    ThreadPanel->>API: GET /api/chat/channels/{id}/threads/{parentId}/messages
    API-->>ThreadPanel: Thread messages list
    ThreadPanel->>ThreadPanel: Render parent + all replies
    
    User->>ThreadPanel: Type reply + press Enter
    ThreadPanel->>API: POST /api/chat/channels/{id}/threads/{parentId}/messages
    API-->>State: Message added to thread
    ThreadPanel->>ThreadPanel: Append to msgs list
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

Poem

🐰 Hop, hop, the chat grows stronger now!
With threads to weave and pins to pin,
Files fly from disk and workspace boughs,
Edit, delete—affordances win!
Deep links guide us where we've been. 🧵📌

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Chat Phase 2b-2a — per-message affordances + pinning' clearly summarizes the main feature additions: per-message UI actions and message pinning functionality.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/chat-phase-2b-2a-per-msg

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (9)
docs/superpowers/plans/2026-04-19-chat-phase-2b-1-threads-attachments.md-2545-2547 (1)

2545-2547: ⚠️ Potential issue | 🟡 Minor

Use a repo-relative rebuild command.

This snippet only works on one machine. The plan should avoid hard-coded local paths so anyone following it can rebuild and commit from their own checkout.

Suggested fix
-cd /Volumes/NVMe/Users/jay/Development/tinyagentos
+cd "$(git rev-parse --show-toplevel)"
 git add -A static/desktop desktop/tsconfig.tsbuildinfo
 git commit -m "build: rebuild desktop bundle for chat Phase 2b-1"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-19-chat-phase-2b-1-threads-attachments.md`
around lines 2545 - 2547, Replace the hard-coded absolute cd command and make
the rebuild steps repo-relative: remove "cd
/Volumes/NVMe/Users/jay/Development/tinyagentos" and either instruct users to
run the following from the repository root or show how to derive it (e.g., use
"cd $(git rev-parse --show-toplevel)" before running the provided commands);
keep the git add and git commit lines ("git add -A static/desktop
desktop/tsconfig.tsbuildinfo" and "git commit -m \"build: rebuild desktop bundle
for chat Phase 2b-1\"") but ensure the instructions state they must be executed
from the repo root (or include the repo-relative cd example) so the snippet
works on any checkout.
desktop/src/apps/chat/ThreadIndicator.tsx-15-19 (1)

15-19: ⚠️ Potential issue | 🟡 Minor

Don't hide the reply metadata from assistive tech.

aria-label="Open thread" replaces the button’s accessible name with a generic action, so screen readers won’t announce the reply count or “last reply …” context shown on screen.

Suggested fix
   return (
     <button
+      type="button"
       onClick={onOpen}
       className="mt-1 px-2 py-0.5 text-xs text-sky-200 hover:bg-white/5 rounded"
-      aria-label="Open thread"
-    >{label}</button>
+    >
+      {label}
+    </button>
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/chat/ThreadIndicator.tsx` around lines 15 - 19, The button
in ThreadIndicator.tsx currently sets aria-label="Open thread", which replaces
the visible reply metadata (the label variable) in the accessible name; remove
or replace that static aria-label so screen readers get the same information as
sighted users. Edit the <button> in ThreadIndicator (the element with
onClick={onOpen}) to either remove the aria-label entirely so the label variable
becomes the accessible name, or set aria-label={label} (or use aria-labelledby
pointing to a visually-hidden span containing label) so the reply count and
“last reply …” context are preserved for assistive tech.
docs/superpowers/plans/2026-04-19-chat-phase-2b-1-threads-attachments.md-2603-2610 (1)

2603-2610: ⚠️ Potential issue | 🟡 Minor

Update documentation to match the actual test implementation.

The example in the documentation uses JavaScript regex literal syntax (/pattern/i), but the actual test file at tests/e2e/test_chat_phase2b1.py line 52 correctly uses Python's re.compile(). The documentation must be updated to reflect the correct Python syntax.

Suggested fix
+import re
@@
-    expect(page.get_by_text(/narrow routing|threads/i)).to_be_visible()
+    expect(page.get_by_text(re.compile(r"narrow routing|threads", re.I))).to_be_visible()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-19-chat-phase-2b-1-threads-attachments.md`
around lines 2603 - 2610, The documentation snippet for
test_help_posts_system_message uses JavaScript regex literal syntax; replace the
JS literal /narrow routing|threads/i with Python's re.compile(r"narrow
routing|threads", re.I) in the expect call (e.g.,
expect(page.get_by_text(re.compile(r"narrow routing|threads",
re.I))).to_be_visible()) and, if the snippet shows imports, include "import re"
so the example matches the actual test implementation that uses re.compile.
desktop/src/apps/chat/PinBadge.tsx-4-8 (1)

4-8: ⚠️ Potential issue | 🟡 Minor

Set explicit type="button" to avoid accidental form submits.

Line 4 currently relies on default button behavior; in a form context this can submit unexpectedly.

Proposed fix
     <button
+      type="button"
       onClick={onClick}
       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 (${count})`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/chat/PinBadge.tsx` around lines 4 - 8, The PinBadge button
currently lacks an explicit type which can cause accidental form submits; update
the button element inside the PinBadge component (the button with props onClick
and aria-label={`Pinned messages (${count})`}) to include type="button" so it
does not act as a submit button in forms.
desktop/src/apps/chat/PinRequestAffordance.tsx-10-14 (1)

10-14: ⚠️ Potential issue | 🟡 Minor

Set an explicit button type to avoid unintended form submits.

At Line 10, this <button> defaults to type="submit". If this affordance is ever rendered inside a form, clicking “📌 Pin this” can trigger an accidental submit.

Proposed fix
       <button
+        type="button"
         onClick={onApprove}
         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 ${authorId}`}
       >📌 Pin this</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/chat/PinRequestAffordance.tsx` around lines 10 - 14, The
button in PinRequestAffordance.tsx is missing an explicit type so it defaults to
type="submit" and may accidentally submit a parent form; update the <button>
rendered in the PinRequestAffordance component (the element that uses onApprove
and aria-label with authorId) to include type="button" to prevent unintended
form submissions while preserving the existing onClick and aria-label behavior.
desktop/src/apps/chat/MessageHoverActions.tsx-16-18 (1)

16-18: ⚠️ Potential issue | 🟡 Minor

Explicitly set type="button" on toolbar actions.

At Lines 16-18, these buttons currently default to submit behavior, which can cause unintended form submissions in nested form contexts.

Proposed fix
-      <button aria-label="Add reaction" onClick={onReact} className="p-1 hover:bg-white/5">😀</button>
-      <button aria-label="Reply in thread" onClick={onReplyInThread} className="p-1 hover:bg-white/5">💬</button>
-      <button aria-label="More" onClick={onOverflow} className="p-1 hover:bg-white/5">⋯</button>
+      <button type="button" aria-label="Add reaction" onClick={onReact} className="p-1 hover:bg-white/5">😀</button>
+      <button type="button" aria-label="Reply in thread" onClick={onReplyInThread} className="p-1 hover:bg-white/5">💬</button>
+      <button type="button" aria-label="More" onClick={onOverflow} className="p-1 hover:bg-white/5">⋯</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/chat/MessageHoverActions.tsx` around lines 16 - 18, The
three toolbar buttons in MessageHoverActions (the elements using onReact,
onReplyInThread, and onOverflow) lack an explicit type and will act as submit
buttons in forms; update each button element to include type="button" to prevent
accidental form submissions (i.e., add type="button" to the buttons that
currently render the emoji, reply, and overflow actions).
desktop/src/shell/__tests__/VfsBrowser.test.tsx-43-46 (1)

43-46: ⚠️ Potential issue | 🟡 Minor

Order claim is not actually asserted.

The test says “folders first, then files” but only checks existence, so ordering regressions won’t be caught.

Proposed assertion improvement
   expect(await screen.findByText(/notes/)).toBeInTheDocument();
-  expect(screen.getByText(/report\.md/)).toBeInTheDocument();
+  const notes = screen.getByText(/notes/);
+  const report = screen.getByText(/report\.md/);
+  expect(notes.compareDocumentPosition(report) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/shell/__tests__/VfsBrowser.test.tsx` around lines 43 - 46, The
test only checks presence of "notes" and "report.md" but doesn't assert their
order; update the VfsBrowser.test.tsx assertions to verify "notes" appears
before "report.md" in the rendered DOM (for example, use
screen.findAllByText(/notes|report\.md/) or screen.findAllByRole('listitem') to
get the ordered NodeList/array and then assert that the index of the element
matching /notes/ is less than the index of the element matching /report\.md/);
ensure you update the expectations that currently call
screen.findByText(/notes/) and screen.getByText(/report\.md/) to perform this
relative-order check.
desktop/src/shell/VfsBrowser.tsx-61-95 (1)

61-95: ⚠️ Potential issue | 🟡 Minor

Reset navigation state when root changes.

currentPath and selected survive a root change, so if I browse into foo/bar for one agent and then switch to another agent, the next fetch starts from that stale subpath under the new root. That can land the user in the wrong folder or an immediate error state.

A small useEffect that clears currentPath and selected on root changes would keep each workspace switch anchored at its own root.

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

In `@desktop/src/shell/VfsBrowser.tsx` around lines 61 - 95, When the root prop
changes, currentPath and selected are not reset so the UI can remain pointed at
a stale subpath; add a new useEffect that listens for changes to root and calls
setCurrentPath("") (or the intended root-empty value) and setSelected(null) (or
appropriate initial selected state) to clear navigation state; reference the
existing state setters setCurrentPath and setSelected and place this effect
alongside the existing useEffect that fetches entries so each workspace switch
starts at its own root.
docs/superpowers/specs/2026-04-19-chat-phase-2b-2a-per-msg-design.md-33-43 (1)

33-43: ⚠️ Potential issue | 🟡 Minor

Align the schema section with the chat_pins design.

This spec says pin state lives on chat_messages via pinned_at / pinned_by, but the plan and backend changes use the separate chat_pins table instead. Leaving both models in the docs will send future migrations and tests toward columns that do not exist.

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

In `@docs/superpowers/specs/2026-04-19-chat-phase-2b-2a-per-msg-design.md` around
lines 33 - 43, The schema section currently adds pinned_at/pinned_by to
chat_messages but the implementation uses the separate chat_pins table; update
the docs to match the backend by removing the pinned_at and pinned_by columns
from the chat_messages list and instead describe the chat_pins model
(referencing chat_pins and chat_messages, and the pin-related fields like
pinned_by/pinned_at as belonging to chat_pins), and note that migration is
additive for chat_pins rather than adding columns to chat_messages.
🧹 Nitpick comments (7)
desktop/src/apps/chat/__tests__/ThreadIndicator.test.tsx (1)

5-20: Add a click-path test for onOpen.

These cases cover rendering, but a broken click handler would still pass. One interaction assertion would lock down the actual affordance behavior.

Suggested test
-import { render, screen } from "@testing-library/react";
+import { render, screen, fireEvent } from "@testing-library/react";
@@
   it("uses plural 'replies' for two+ and includes last reply relative time", () => {
     const past = Math.floor(Date.now() / 1000) - 120; // 2 min ago
     render(<ThreadIndicator replyCount={3} lastReplyAt={past} onOpen={vi.fn()} />);
     const btn = screen.getByRole("button", { name: /Open thread/i });
     expect(btn.textContent).toMatch(/3 replies/);
     expect(btn.textContent).toMatch(/2m ago/);
   });
+
+  it("calls onOpen when clicked", () => {
+    const onOpen = vi.fn();
+    render(<ThreadIndicator replyCount={1} onOpen={onOpen} />);
+    fireEvent.click(screen.getByRole("button"));
+    expect(onOpen).toHaveBeenCalledTimes(1);
+  });
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/chat/__tests__/ThreadIndicator.test.tsx` around lines 5 -
20, Add an interaction test to ThreadIndicator.test.tsx that verifies the onOpen
handler is invoked when the button is clicked: render <ThreadIndicator
replyCount={1} ... /> (or replyCount>0) with a vi.fn() mock for onOpen, find the
button via screen.getByRole("button", { name: /Open thread/i }), simulate a
click using userEvent.click (or fireEvent.click) and assert the mock was called;
reference the ThreadIndicator component and its onOpen prop so the test fails if
the click path is broken.
desktop/src/apps/chat/__tests__/PinRequestAffordance.test.tsx (1)

6-11: Tighten the click assertion to a single call.

Line 10 only checks that it was called at least once; asserting exactly once catches accidental double-invocation wiring.

Suggested test tightening
-    expect(onApprove).toHaveBeenCalled();
+    expect(onApprove).toHaveBeenCalledTimes(1);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/chat/__tests__/PinRequestAffordance.test.tsx` around lines 6
- 11, The test currently asserts that the onApprove mock was called at least
once; update the assertion in the PinRequestAffordance test to assert exactly
one call by replacing expect(onApprove).toHaveBeenCalled() with
expect(onApprove).toHaveBeenCalledTimes(1) so the test fails on accidental
double-invocation of the PinRequestAffordance's onApprove handler.
desktop/src/apps/chat/__tests__/PinnedMessagesPopover.test.tsx (1)

5-27: Consider adding a test for the onClose action path.

onClose is part of the public contract but currently unasserted in this suite.

Suggested additional test
+  it("fires onClose when close control is clicked", () => {
+    const onClose = vi.fn();
+    const pins = [{ id: "m1", author_id: "tom", content: "x", created_at: 123, pinned_by: "u", pinned_at: 200 }];
+    render(<PinnedMessagesPopover pins={pins} onJumpTo={vi.fn()} onClose={onClose} />);
+    fireEvent.click(screen.getByRole("button", { name: /close/i }));
+    expect(onClose).toHaveBeenCalledTimes(1);
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/chat/__tests__/PinnedMessagesPopover.test.tsx` around lines
5 - 27, Add a test that asserts the PinnedMessagesPopover calls the onClose prop
when the user triggers the close action: render PinnedMessagesPopover with a
vi.fn() for onClose, simulate the close interaction (e.g., fireEvent.click on
the close button found by role/name like /close/i or the popover’s close
control), and expect the onClose mock toHaveBeenCalled(); locate
PinnedMessagesPopover in the existing PinnedMessagesPopover.test.tsx and mirror
the pattern used for the onJumpTo test (use vi.fn(), render(...),
fireEvent.click(...), expect(onClose).toHaveBeenCalled()).
desktop/src/apps/chat/__tests__/PinBadge.test.tsx (1)

16-21: Use exact click-call count for stronger regression protection.

Line 20 should assert one invocation to detect duplicate event bindings.

Suggested test tightening
-    expect(onClick).toHaveBeenCalled();
+    expect(onClick).toHaveBeenCalledTimes(1);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/chat/__tests__/PinBadge.test.tsx` around lines 16 - 21, The
test for the PinBadge onClick handler currently uses a loose assertion; update
the assertion in the "fires onClick" test that references the onClick mock
(created as onClick = vi.fn() and passed to <PinBadge count={1}
onClick={onClick} />) to assert the exact call count using
expect(onClick).toHaveBeenCalledTimes(1) instead of
expect(onClick).toHaveBeenCalled() to catch duplicate bindings.
desktop/src/apps/chat/__tests__/MessageEditor.test.tsx (1)

11-18: Add coverage for Enter on empty/whitespace content.

This suite misses the trimmed === "" branch, which should call onCancel.

Suggested additional test
+  it("Enter on whitespace triggers cancel", () => {
+    const onCancel = vi.fn();
+    const onSave = vi.fn();
+    render(<MessageEditor initial="hi" onSave={onSave} onCancel={onCancel} />);
+    const input = screen.getByRole("textbox");
+    fireEvent.change(input, { target: { value: "   " } });
+    fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
+    expect(onSave).not.toHaveBeenCalled();
+    expect(onCancel).toHaveBeenCalledTimes(1);
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/chat/__tests__/MessageEditor.test.tsx` around lines 11 - 18,
Add a test to cover the trimmed === "" branch by rendering MessageEditor (same
as existing test), changing the textbox value to a whitespace-only string (e.g.,
"   ") and simulating Enter (keyDown with key "Enter" and shiftKey false), then
assert that onCancel was called (and onSave was not called); reference the
MessageEditor component and the onSave/onCancel mocks to locate where to add
this test.
desktop/src/lib/__tests__/chat-messages-api.test.ts (1)

41-69: Add JSON header assertions for write endpoints.

These tests already validate body shape; asserting Content-Type: application/json will better lock the request contract.

Proposed assertion upgrade
   expect(fetch).toHaveBeenCalledWith(
     "/api/chat/messages/m1",
     expect.objectContaining({
       method: "PATCH",
+      headers: expect.objectContaining({ "Content-Type": "application/json" }),
       body: JSON.stringify({ content: "new text" }),
     }),
   );
@@
   expect(fetch).toHaveBeenCalledWith(
     "/api/chat/channels/c1/read-cursor/rewind",
     expect.objectContaining({
       method: "POST",
+      headers: expect.objectContaining({ "Content-Type": "application/json" }),
       body: JSON.stringify({ before_message_id: "m2" }),
     }),
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/lib/__tests__/chat-messages-api.test.ts` around lines 41 - 69,
Update the tests for the write endpoints to assert JSON content-type: add header
checks to the editMessage and markUnread tests so their
expect(fetch).toHaveBeenCalledWith includes expect.objectContaining({ headers:
expect.objectContaining({ "Content-Type": "application/json" }), ... })
alongside the existing method/body assertions; reference the editMessage and
markUnread test cases (and leave deleteMessage unchanged since it's a DELETE) so
the contract explicitly requires the JSON header.
desktop/src/lib/__tests__/chat-attachments-api.test.ts (1)

17-47: Tighten request-shape assertions to catch payload regressions.

Current assertions mostly validate URL/method; adding body/header checks would better protect API contract changes.

Proposed assertion upgrades
   expect(fetch).toHaveBeenCalledWith(
     "/api/chat/upload",
-    expect.objectContaining({ method: "POST" }),
+    expect.objectContaining({
+      method: "POST",
+      body: expect.any(FormData),
+    }),
   );
@@
   expect(fetch).toHaveBeenCalledWith(
     "/api/chat/attachments/from-path",
     expect.objectContaining({
       method: "POST",
+      headers: expect.objectContaining({ "Content-Type": "application/json" }),
       body: JSON.stringify({ path: "/workspaces/user/r.md", source: "workspace" }),
     }),
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/lib/__tests__/chat-attachments-api.test.ts` around lines 17 - 47,
Both tests currently only assert URL/method; tighten them to verify request
payload shape and headers to prevent regressions: in the uploadDiskFile test
(function uploadDiskFile) assert the fetch init.body is a FormData (or has
expected entries like the file) and that headers/body imply multipart upload
(e.g., no forced JSON content-type), and in the attachmentFromPath test
(function attachmentFromPath) assert the fetch init.body equals JSON.stringify({
path: "...", source: "workspace" }) and that headers include the appropriate
content-type (e.g., "application/json") so the POST payload and headers are
explicitly checked.
🤖 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/apps/chat/MessageOverflowMenu.tsx`:
- Around line 18-39: The component MessageOverflowMenu is using role="menu" and
role="menuitem" without implementing menu keyboard behavior; remove the ARIA
menu semantics and use plain buttons instead: delete role="menu" from the
container and remove role="menuitem" from each button (keep the existing onClick
handlers like onEdit, onDelete, onCopyLink, onPin, onMarkUnread), ensure the
container still has an accessible label (use aria-label or visually-hidden
heading if needed) and preserve focus/keyboard activation via the native button
elements.

In `@desktop/src/apps/chat/ThreadPanel.tsx`:
- Around line 38-40: The fetch in ThreadPanel.tsx is treating the response as {
messages: [...] } but the endpoint returns an array, so setMsgs is always given
[] — update the response handling for
fetch(`/api/chat/channels/${channelId}/threads/${parentId}/messages`) to expect
an array (e.g., if Array.isArray(d) then setMsgs(d) else setMsgs([])) and use
the existing setMsgs state updater (and keep the alive guard) so thread replies
populate correctly; locate the fetch call and adjust the .then((d) => ...)
handler accordingly, referencing setMsgs, msgs, channelId and parentId.
- Around line 44-49: submit currently clears the draft (calls setInput(""))
before awaiting onSend, so failures drop the user's text; change submit to
capture const content = input.trim(), return if empty, then await
onSend(content, []), and only call setInput("") after onSend succeeds (or in a
try/catch restore the original input on failure) — update the submit function to
call setInput("") on success and preserve or restore input on error, referencing
the submit, input, setInput and onSend symbols.

In `@desktop/src/apps/MessagesApp.tsx`:
- Around line 1637-1640: The MessageOverflowMenu's isHuman prop is being set
from msg.author_type (msg.author_type === "user"), which hides pin actions based
on who sent the message; instead it must be driven by the current
viewer/principal. Change the prop assignment so isHuman reflects the current
user/principal (for example use currentUser?.type === "user" or a boolean like
currentUserIsHuman) rather than msg.author_type; update the call site where
MessageOverflowMenu is rendered (the line setting isHuman) and ensure any
necessary currentUser/currentUserType variable is passed in or read from context
so pin/unpin visibility is based on the viewer, not msg.author_type.
- Around line 818-835: The edit and delete handlers (handleSaveEdit and
handleDelete) currently only call apiEditMessage/apiDeleteMessage and close UI
state, so local messages state isn't updated; either update messages directly
here after the API call (use setMessages to replace the edited message content
and metadata for msgId in messages, and for deletes convert the message to a
tombstone entry compatible with MessageTombstone) or adjust the websocket
reducer to handle the backend's new "message_update" event (and treat delete
payloads as soft-deletes) so realtime reconciliation occurs; pick one approach
and implement the corresponding fix referencing handleSaveEdit, handleDelete,
setMessages, messages, apiEditMessage, apiDeleteMessage, the websocket reducer
event handlers, and MessageTombstone.
- Around line 542-554: The deep-link effect in MessagesApp.tsx (the useEffect
that reads URLSearchParams and queries `[data-message-id="${msgId}"]`) keeps
retriggering because it is keyed on messages.length and does not clear the ?msg
param; after a successful scroll+highlight you should prevent re-running by
either removing the msg param from the URL (use URLSearchParams to delete "msg"
and call window.history.replaceState with the new search) or by recording the
handled id in a ref (e.g., handledMsgRef.current = msgId) and returning early if
the same id is seen; implement one of these in the effect right after the
successful el.scrollIntoView / class toggle so subsequent message updates won’t
reapply the jump/highlight.

In `@desktop/src/shell/FilePicker.tsx`:
- Around line 56-69: The handlers onWorkspacePick and onAgentWorkspacePick
currently append incoming selections to the queued state, causing duplicates
because VfsBrowser supplies the full current selection each time; change the
setQueued updater to replace queued entries for the same source (and include
slug equality for "agent-workspace") instead of concatenating: inside
setQueued(prev => { filter out entries whose source is "workspace" (for
onWorkspacePick) or whose source is "agent-workspace" and slug === selectedAgent
(for onAgentWorkspacePick) to get remaining entries, then return multi ?
[...remaining, ...selections] : selections }). This ensures the queued list
reflects the latest snapshot from onWorkspacePick/onAgentWorkspacePick and
removes deselected items.

In `@static/desktop/assets/AgentBrowsersApp-CFn8GY-5.js`:
- Line 1: The Connect button in the AgentBrowsersApp detail view (inside Ue)
currently uses a no-op onClick: ()=>{} making it look functional; replace that
by either wiring it to the real taOS/noVNC entrypoint (call the actual connect
function you have for opening a noVNC session) or disable/hide it until the
backend flow exists—specifically update the o button with aria-label "Connect to
browser via noVNC — opens browser in a taOS window" (and the onClick prop
currently set to ()=>{}) to either call the proper connect handler (e.g.,
openNoVNC/openTaOSSession) passing the profile id, or set disabled={true} (and
adjust title/aria-disabled) so it isn’t presented as actionable.
- Line 1: The handlers J, pe, and be are replacing profile objects in state m
(and selected s) with action-envelope responses (which only include
status/id/etc.), causing loss of fields like profile_name/node; instead, after
receiving the mutation response, fetch the full updated profile or merge only
the changed fields into the existing profile object before updating state.
Concretely: in J, pe, and be, avoid directly using the returned envelope to
replace items in h (m) and n (s); either call the profile refresh N() (or an API
to GET the single profile) and use that full profile to update h and n, or merge
the envelope onto the existing profile (e.g., map h and for the matching id
spread the old profile and the returned fields) so profile_name, node,
timestamps remain intact.
- Line 1: The screenshot helper ze() currently calls the JSON-only helper y() so
image/png responses are ignored; update ze() to perform a raw fetch to
`/api/agent-browsers/profiles/${encodeURIComponent(a)}/screenshot` that reads
the response as a Blob (or arrayBuffer) and converts it to a data URL (or
createObjectURL) before returning it; ensure the b callback (function b / const
b = r.useCallback(...)) continues to call ze(t) and stores the returned data URL
into state P via ce(l=>({...l,[t]:i})), so P[s.id] is populated and the preview
image displays.

In `@static/desktop/assets/GitHubApp-IYMAlDty.js`:
- Line 1: The footer and banner "Connect" buttons in the nt component are no-ops
and the UI reads non-existent R.method causing misleading text; implement real
connect handlers and map auth response fields correctly: hook the
unauthenticated footer button (in nt render) and the banner "Connect" button
(ne) to launch the GitHub auth flow (e.g. open the OAuth start endpoint or
navigate to the auth route) so users can complete sign-in, and update the
display logic that currently uses R.method to instead use the auth response
field returned by Je (R.source) and ensure the displayed username reads the
actual user field provided by the auth response (e.g. R.username or
R.user.login) so the footer shows correct status.
- Line 1: The controls for "watched", content type (S) and status filter (pe)
are only updating button state and not changing fetched data — watched (X) is
never populated and the list/filter logic in the memoized g only switches
between J (starred) and Y (notifications). Fix by wiring the UI handlers to real
fetches and filters: populate X when u("watched") is selected (call a fetch like
We or a new fetchWatched function inside the I handler or a dedicated
fetchWatched), update the selection callbacks V (content type) and the status
onClick (ue) to trigger data reloads or apply client-side filters, and
incorporate S and pe into the g useMemo filter logic so items are filtered by
content type (repos/issues/prs/releases) and state (open/closed/merged);
alternatively hide the watched/content/status controls until the corresponding
data-fetch functions (I, fetchWatched, Fe/Ke/Ve) and filters are implemented.
- Line 1: The component assumes flattened app shapes but the API returns raw
GitHub payloads; normalize responses before they reach render by transforming
data in the fetch helpers We(...) and Pe(...) (and the callers I() and _() that
set state via be(...) and me(...)). In We, map each repo from GitHub shape
(owner.login, stargazers_count, forks_count, description, language, updated_at,
name, etc.) into the app shape (owner, stars, forks, description, language,
updated_at, name, etc.) before returning; in Pe, map notification items
(repository.full_name -> repo, subject.title -> title, subject.url/number ->
number if present, updated_at/created_at -> created_at) into the app shape the
UI expects; keep the same return shape (repos:[],notifications:[]) and default
values when fields are missing so downstream code (I(), _(), g memo, rendering
functions ve/Ne/etc.) never sees raw GitHub keys.

In `@static/desktop/assets/ImportApp-DBAV17Xb.js`:
- Line 1: The upload loop in function T currently ignores response.ok and
swallows errors, then always reports "Uploaded N files..."; update T to check
each fetch("/api/import/upload", ...) response.ok and treat non-ok or thrown
errors as failures (do not increment the success count or progress for failed
uploads), collect failed file names/IDs into a list, and after the loop set
o(...) to a success/failure message that reflects how many succeeded vs failed
(e.g., "Uploaded X of Y files; failed: [...]") and set j(false) appropriately;
ensure the catch block for each file records the failure and does not advance
the success counter or show a misleading completed state.

In `@static/desktop/assets/LibraryApp-Cdo_EHou.js`:
- Line 1: The detail view reads Object.keys(a.metadata) without guarding for
a.metadata being undefined; change the code in the LibraryApp component (inside
function ut, where a is the selected item and metadata is referenced) to default
a.metadata to an empty object (e.g., const metadata = a?.metadata ?? {}) and use
that safe metadata variable for checks and rendering (replace
Object.keys(a.metadata) and any direct a.metadata access with
Object.keys(metadata) and metadata) so the Metadata tab won't crash when
metadata is absent.

In `@static/desktop/assets/MCPApp-JMsboybS.js`:
- Line 1: Summary: The PR currently includes a minified production bundle
(MCPApp-JMsboybS.js) which is not reviewable; instead the original TSX source
files under desktop/src/apps/chat/ should be reviewed. Fix: remove the minified
bundle from the review (or exclude it from the commit/PR) and update the PR to
include or point reviewers to the corresponding source files (e.g.,
MessageEditor.tsx, MessageHoverActions.tsx, MessageOverflowMenu.tsx,
PinBadge.tsx, PinRequestAffordance.tsx, PinnedMessagesPopover.tsx) so reviewers
can inspect functions/components (e.g., fe, je, ge, be, ve) in their original
TypeScript/TSX form; alternatively add a note in the PR description clarifying
which source files to review and why the bundle can be ignored.

---

Minor comments:
In `@desktop/src/apps/chat/MessageHoverActions.tsx`:
- Around line 16-18: The three toolbar buttons in MessageHoverActions (the
elements using onReact, onReplyInThread, and onOverflow) lack an explicit type
and will act as submit buttons in forms; update each button element to include
type="button" to prevent accidental form submissions (i.e., add type="button" to
the buttons that currently render the emoji, reply, and overflow actions).

In `@desktop/src/apps/chat/PinBadge.tsx`:
- Around line 4-8: The PinBadge button currently lacks an explicit type which
can cause accidental form submits; update the button element inside the PinBadge
component (the button with props onClick and aria-label={`Pinned messages
(${count})`}) to include type="button" so it does not act as a submit button in
forms.

In `@desktop/src/apps/chat/PinRequestAffordance.tsx`:
- Around line 10-14: The button in PinRequestAffordance.tsx is missing an
explicit type so it defaults to type="submit" and may accidentally submit a
parent form; update the <button> rendered in the PinRequestAffordance component
(the element that uses onApprove and aria-label with authorId) to include
type="button" to prevent unintended form submissions while preserving the
existing onClick and aria-label behavior.

In `@desktop/src/apps/chat/ThreadIndicator.tsx`:
- Around line 15-19: The button in ThreadIndicator.tsx currently sets
aria-label="Open thread", which replaces the visible reply metadata (the label
variable) in the accessible name; remove or replace that static aria-label so
screen readers get the same information as sighted users. Edit the <button> in
ThreadIndicator (the element with onClick={onOpen}) to either remove the
aria-label entirely so the label variable becomes the accessible name, or set
aria-label={label} (or use aria-labelledby pointing to a visually-hidden span
containing label) so the reply count and “last reply …” context are preserved
for assistive tech.

In `@desktop/src/shell/__tests__/VfsBrowser.test.tsx`:
- Around line 43-46: The test only checks presence of "notes" and "report.md"
but doesn't assert their order; update the VfsBrowser.test.tsx assertions to
verify "notes" appears before "report.md" in the rendered DOM (for example, use
screen.findAllByText(/notes|report\.md/) or screen.findAllByRole('listitem') to
get the ordered NodeList/array and then assert that the index of the element
matching /notes/ is less than the index of the element matching /report\.md/);
ensure you update the expectations that currently call
screen.findByText(/notes/) and screen.getByText(/report\.md/) to perform this
relative-order check.

In `@desktop/src/shell/VfsBrowser.tsx`:
- Around line 61-95: When the root prop changes, currentPath and selected are
not reset so the UI can remain pointed at a stale subpath; add a new useEffect
that listens for changes to root and calls setCurrentPath("") (or the intended
root-empty value) and setSelected(null) (or appropriate initial selected state)
to clear navigation state; reference the existing state setters setCurrentPath
and setSelected and place this effect alongside the existing useEffect that
fetches entries so each workspace switch starts at its own root.

In `@docs/superpowers/plans/2026-04-19-chat-phase-2b-1-threads-attachments.md`:
- Around line 2545-2547: Replace the hard-coded absolute cd command and make the
rebuild steps repo-relative: remove "cd
/Volumes/NVMe/Users/jay/Development/tinyagentos" and either instruct users to
run the following from the repository root or show how to derive it (e.g., use
"cd $(git rev-parse --show-toplevel)" before running the provided commands);
keep the git add and git commit lines ("git add -A static/desktop
desktop/tsconfig.tsbuildinfo" and "git commit -m \"build: rebuild desktop bundle
for chat Phase 2b-1\"") but ensure the instructions state they must be executed
from the repo root (or include the repo-relative cd example) so the snippet
works on any checkout.
- Around line 2603-2610: The documentation snippet for
test_help_posts_system_message uses JavaScript regex literal syntax; replace the
JS literal /narrow routing|threads/i with Python's re.compile(r"narrow
routing|threads", re.I) in the expect call (e.g.,
expect(page.get_by_text(re.compile(r"narrow routing|threads",
re.I))).to_be_visible()) and, if the snippet shows imports, include "import re"
so the example matches the actual test implementation that uses re.compile.

In `@docs/superpowers/specs/2026-04-19-chat-phase-2b-2a-per-msg-design.md`:
- Around line 33-43: The schema section currently adds pinned_at/pinned_by to
chat_messages but the implementation uses the separate chat_pins table; update
the docs to match the backend by removing the pinned_at and pinned_by columns
from the chat_messages list and instead describe the chat_pins model
(referencing chat_pins and chat_messages, and the pin-related fields like
pinned_by/pinned_at as belonging to chat_pins), and note that migration is
additive for chat_pins rather than adding columns to chat_messages.

---

Nitpick comments:
In `@desktop/src/apps/chat/__tests__/MessageEditor.test.tsx`:
- Around line 11-18: Add a test to cover the trimmed === "" branch by rendering
MessageEditor (same as existing test), changing the textbox value to a
whitespace-only string (e.g., "   ") and simulating Enter (keyDown with key
"Enter" and shiftKey false), then assert that onCancel was called (and onSave
was not called); reference the MessageEditor component and the onSave/onCancel
mocks to locate where to add this test.

In `@desktop/src/apps/chat/__tests__/PinBadge.test.tsx`:
- Around line 16-21: The test for the PinBadge onClick handler currently uses a
loose assertion; update the assertion in the "fires onClick" test that
references the onClick mock (created as onClick = vi.fn() and passed to
<PinBadge count={1} onClick={onClick} />) to assert the exact call count using
expect(onClick).toHaveBeenCalledTimes(1) instead of
expect(onClick).toHaveBeenCalled() to catch duplicate bindings.

In `@desktop/src/apps/chat/__tests__/PinnedMessagesPopover.test.tsx`:
- Around line 5-27: Add a test that asserts the PinnedMessagesPopover calls the
onClose prop when the user triggers the close action: render
PinnedMessagesPopover with a vi.fn() for onClose, simulate the close interaction
(e.g., fireEvent.click on the close button found by role/name like /close/i or
the popover’s close control), and expect the onClose mock toHaveBeenCalled();
locate PinnedMessagesPopover in the existing PinnedMessagesPopover.test.tsx and
mirror the pattern used for the onJumpTo test (use vi.fn(), render(...),
fireEvent.click(...), expect(onClose).toHaveBeenCalled()).

In `@desktop/src/apps/chat/__tests__/PinRequestAffordance.test.tsx`:
- Around line 6-11: The test currently asserts that the onApprove mock was
called at least once; update the assertion in the PinRequestAffordance test to
assert exactly one call by replacing expect(onApprove).toHaveBeenCalled() with
expect(onApprove).toHaveBeenCalledTimes(1) so the test fails on accidental
double-invocation of the PinRequestAffordance's onApprove handler.

In `@desktop/src/apps/chat/__tests__/ThreadIndicator.test.tsx`:
- Around line 5-20: Add an interaction test to ThreadIndicator.test.tsx that
verifies the onOpen handler is invoked when the button is clicked: render
<ThreadIndicator replyCount={1} ... /> (or replyCount>0) with a vi.fn() mock for
onOpen, find the button via screen.getByRole("button", { name: /Open thread/i
}), simulate a click using userEvent.click (or fireEvent.click) and assert the
mock was called; reference the ThreadIndicator component and its onOpen prop so
the test fails if the click path is broken.

In `@desktop/src/lib/__tests__/chat-attachments-api.test.ts`:
- Around line 17-47: Both tests currently only assert URL/method; tighten them
to verify request payload shape and headers to prevent regressions: in the
uploadDiskFile test (function uploadDiskFile) assert the fetch init.body is a
FormData (or has expected entries like the file) and that headers/body imply
multipart upload (e.g., no forced JSON content-type), and in the
attachmentFromPath test (function attachmentFromPath) assert the fetch init.body
equals JSON.stringify({ path: "...", source: "workspace" }) and that headers
include the appropriate content-type (e.g., "application/json") so the POST
payload and headers are explicitly checked.

In `@desktop/src/lib/__tests__/chat-messages-api.test.ts`:
- Around line 41-69: Update the tests for the write endpoints to assert JSON
content-type: add header checks to the editMessage and markUnread tests so their
expect(fetch).toHaveBeenCalledWith includes expect.objectContaining({ headers:
expect.objectContaining({ "Content-Type": "application/json" }), ... })
alongside the existing method/body assertions; reference the editMessage and
markUnread test cases (and leave deleteMessage unchanged since it's a DELETE) so
the contract explicitly requires the JSON header.
🪄 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: 39a76860-2f56-49a2-ad13-7b08e1202425

📥 Commits

Reviewing files that changed from the base of the PR and between 6d606ac and 9323090.

📒 Files selected for processing (128)
  • desktop/src/apps/MessagesApp.tsx
  • desktop/src/apps/chat/AttachmentGallery.tsx
  • desktop/src/apps/chat/AttachmentLightbox.tsx
  • desktop/src/apps/chat/AttachmentsBar.tsx
  • desktop/src/apps/chat/MessageEditor.tsx
  • desktop/src/apps/chat/MessageHoverActions.tsx
  • desktop/src/apps/chat/MessageOverflowMenu.tsx
  • desktop/src/apps/chat/MessageTombstone.tsx
  • desktop/src/apps/chat/PinBadge.tsx
  • desktop/src/apps/chat/PinRequestAffordance.tsx
  • desktop/src/apps/chat/PinnedMessagesPopover.tsx
  • desktop/src/apps/chat/ThreadIndicator.tsx
  • desktop/src/apps/chat/ThreadPanel.tsx
  • desktop/src/apps/chat/__tests__/AttachmentGallery.test.tsx
  • desktop/src/apps/chat/__tests__/AttachmentsBar.test.tsx
  • desktop/src/apps/chat/__tests__/MessageEditor.test.tsx
  • desktop/src/apps/chat/__tests__/MessageHoverActions.test.tsx
  • desktop/src/apps/chat/__tests__/MessageOverflowMenu.test.tsx
  • desktop/src/apps/chat/__tests__/MessageTombstone.test.tsx
  • desktop/src/apps/chat/__tests__/PinBadge.test.tsx
  • desktop/src/apps/chat/__tests__/PinRequestAffordance.test.tsx
  • desktop/src/apps/chat/__tests__/PinnedMessagesPopover.test.tsx
  • desktop/src/apps/chat/__tests__/ThreadIndicator.test.tsx
  • desktop/src/lib/__tests__/chat-attachments-api.test.ts
  • desktop/src/lib/__tests__/chat-messages-api.test.ts
  • desktop/src/lib/chat-attachments-api.ts
  • desktop/src/lib/chat-messages-api.ts
  • desktop/src/lib/use-thread-panel.ts
  • desktop/src/shell/FilePicker.tsx
  • desktop/src/shell/VfsBrowser.tsx
  • desktop/src/shell/__tests__/FilePicker.test.tsx
  • desktop/src/shell/__tests__/VfsBrowser.test.tsx
  • desktop/src/shell/file-picker-api.ts
  • desktop/src/theme/tokens.css
  • desktop/tsconfig.tsbuildinfo
  • docs/chat-guide.md
  • docs/superpowers/plans/2026-04-19-chat-phase-2b-1-threads-attachments.md
  • docs/superpowers/plans/2026-04-19-chat-phase-2b-2a-per-msg.md
  • docs/superpowers/specs/2026-04-19-chat-phase-2b-2a-per-msg-design.md
  • static/desktop/assets/ActivityApp-CG-PW6E_.js
  • static/desktop/assets/AgentBrowsersApp-CFn8GY-5.js
  • static/desktop/assets/AgentBrowsersApp-wWjBRYht.js
  • static/desktop/assets/AgentsApp-1_BLyIy2.js
  • static/desktop/assets/BrowserApp-FjtUA0FW.js
  • static/desktop/assets/CalendarApp-BJnvuKGY.js
  • static/desktop/assets/CalendarApp-DSaV9uPb.js
  • static/desktop/assets/ChannelsApp-BMXzpUI6.js
  • static/desktop/assets/ClusterApp-DzgzEDRn.js
  • static/desktop/assets/ContactsApp-CmwPWf7s.js
  • static/desktop/assets/FilesApp-Bm-rxwrE.js
  • static/desktop/assets/GitHubApp-CJvVZ0RH.js
  • static/desktop/assets/GitHubApp-IYMAlDty.js
  • static/desktop/assets/ImageViewerApp-D7vhXACc.js
  • static/desktop/assets/ImagesApp-DfCeUrhn.js
  • static/desktop/assets/ImportApp-AV3jmR5U.js
  • static/desktop/assets/ImportApp-DBAV17Xb.js
  • static/desktop/assets/LibraryApp-Cdo_EHou.js
  • static/desktop/assets/LibraryApp-NzJAyw3P.js
  • static/desktop/assets/MCPApp-JMsboybS.js
  • static/desktop/assets/MemoryApp-eDECkdBk.js
  • static/desktop/assets/MessagesApp-BsK4ioIn.js
  • static/desktop/assets/MessagesApp-DJJbqaHc.js
  • static/desktop/assets/MobileSplitView-CtNEF6zb.js
  • static/desktop/assets/MobileSplitView-qc4KfHBU.js
  • static/desktop/assets/ModelsApp-COpOwo4V.js
  • static/desktop/assets/ProvidersApp-C0T_x8YG.js
  • static/desktop/assets/RedditApp-BOuG46mh.js
  • static/desktop/assets/RedditApp-CkwARPpU.js
  • static/desktop/assets/SecretsApp-C1umTVfg.js
  • static/desktop/assets/SettingsApp-BmLIU-FB.js
  • static/desktop/assets/StoreApp-CNUGjBHW.js
  • static/desktop/assets/TasksApp-BLKBbvXY.js
  • static/desktop/assets/TextEditorApp-US6Eef1_.js
  • static/desktop/assets/XApp-E7cm6999.js
  • static/desktop/assets/YouTubeApp-Bv-vMHrm.js
  • static/desktop/assets/YouTubeApp-DPW-GRB6.js
  • static/desktop/assets/chat-DWaQ_wPY.js
  • static/desktop/assets/index-0OnUwbQt.js
  • static/desktop/assets/index-5RjMGAa1.js
  • static/desktop/assets/index-BEgWFDZf.js
  • static/desktop/assets/index-B_XPm7mm.js
  • static/desktop/assets/index-C7isKigO.js
  • static/desktop/assets/index-CH8xqmNE.js
  • static/desktop/assets/index-CTe7-jHC.js
  • static/desktop/assets/index-C_KJzFJ_.js
  • static/desktop/assets/index-C_qAIZSt.js
  • static/desktop/assets/index-CoNKmJJQ.js
  • static/desktop/assets/index-CoQ45O6-.js
  • static/desktop/assets/index-D-E10IgF.js
  • static/desktop/assets/index-DTh72AYJ.js
  • static/desktop/assets/index-DdCLyul1.js
  • static/desktop/assets/index-Dw2m-Rvd.js
  • static/desktop/assets/index-DwzRNNkz.js
  • static/desktop/assets/index-Dza7_6d-.js
  • static/desktop/assets/main-Bs5bQgxi.js
  • static/desktop/assets/tokens-B9Rl30P8.js
  • static/desktop/assets/tokens-gmaKUSZd.css
  • static/desktop/assets/tokens-ib1qRNqW.css
  • static/desktop/assets/vendor-codemirror-CL2HhW7v.js
  • static/desktop/assets/vendor-icons-wm645Jsx.js
  • static/desktop/chat.html
  • static/desktop/index.html
  • tests/e2e/test_chat_phase2b1.py
  • tests/e2e/test_chat_phase2b2a.py
  • tests/test_agent_chat_router.py
  • tests/test_bridge_session_phase1.py
  • tests/test_chat_attachments.py
  • tests/test_chat_edit_delete.py
  • tests/test_chat_help.py
  • tests/test_chat_mark_unread.py
  • tests/test_chat_messages.py
  • tests/test_chat_pin_request.py
  • tests/test_chat_pins.py
  • tests/test_chat_threads.py
  • tests/test_routes_agents.py
  • tinyagentos/agent_chat_router.py
  • tinyagentos/chat/channel_store.py
  • tinyagentos/chat/help.py
  • tinyagentos/chat/message_store.py
  • tinyagentos/chat/reactions.py
  • tinyagentos/chat/threads.py
  • tinyagentos/routes/chat.py
  • tinyagentos/scripts/install_hermes.sh
  • tinyagentos/scripts/install_langroid.sh
  • tinyagentos/scripts/install_openai-agents-sdk.sh
  • tinyagentos/scripts/install_openai_agents_sdk.sh
  • tinyagentos/scripts/install_pocketflow.sh
  • tinyagentos/scripts/install_smolagents.sh
💤 Files with no reviewable changes (7)
  • static/desktop/assets/ImportApp-AV3jmR5U.js
  • static/desktop/assets/LibraryApp-NzJAyw3P.js
  • static/desktop/assets/MobileSplitView-qc4KfHBU.js
  • static/desktop/assets/CalendarApp-BJnvuKGY.js
  • static/desktop/assets/AgentBrowsersApp-wWjBRYht.js
  • static/desktop/assets/GitHubApp-CJvVZ0RH.js
  • static/desktop/assets/MessagesApp-DJJbqaHc.js

Comment on lines +18 to +39
role="menu"
aria-label="Message overflow menu"
className="bg-shell-surface border border-white/10 rounded-md shadow-lg py-1 min-w-[160px] text-sm"
>
{isOwn && (
<button role="menuitem" onClick={onEdit}
className="block w-full text-left px-3 py-1.5 hover:bg-white/5">Edit</button>
)}
{isOwn && (
<button role="menuitem" onClick={onDelete}
className="block w-full text-left px-3 py-1.5 hover:bg-white/5 text-red-300">Delete</button>
)}
<button role="menuitem" onClick={onCopyLink}
className="block w-full text-left px-3 py-1.5 hover:bg-white/5">Copy link</button>
{isHuman && (
<button role="menuitem" onClick={onPin}
className="block w-full text-left px-3 py-1.5 hover:bg-white/5">
{isPinned ? "Unpin" : "Pin"}
</button>
)}
<button role="menuitem" onClick={onMarkUnread}
className="block w-full text-left px-3 py-1.5 hover:bg-white/5">Mark unread</button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use proper menu semantics or drop role="menu"/role="menuitem".

Right now this is announced as an ARIA menu, but it does not implement menu keyboard behavior. That creates an accessibility mismatch. Either implement full menu keyboard support or use plain buttons without menu roles.

Suggested fix (plain action list semantics)
-    <div
-      role="menu"
-      aria-label="Message overflow menu"
+    <div
+      aria-label="Message overflow menu"
       className="bg-shell-surface border border-white/10 rounded-md shadow-lg py-1 min-w-[160px] text-sm"
     >
       {isOwn && (
-        <button role="menuitem" onClick={onEdit}
+        <button type="button" onClick={onEdit}
           className="block w-full text-left px-3 py-1.5 hover:bg-white/5">Edit</button>
       )}
       {isOwn && (
-        <button role="menuitem" onClick={onDelete}
+        <button type="button" onClick={onDelete}
           className="block w-full text-left px-3 py-1.5 hover:bg-white/5 text-red-300">Delete</button>
       )}
-      <button role="menuitem" onClick={onCopyLink}
+      <button type="button" onClick={onCopyLink}
         className="block w-full text-left px-3 py-1.5 hover:bg-white/5">Copy link</button>
       {isHuman && (
-        <button role="menuitem" onClick={onPin}
+        <button type="button" onClick={onPin}
           className="block w-full text-left px-3 py-1.5 hover:bg-white/5">
           {isPinned ? "Unpin" : "Pin"}
         </button>
       )}
-      <button role="menuitem" onClick={onMarkUnread}
+      <button type="button" onClick={onMarkUnread}
         className="block w-full text-left px-3 py-1.5 hover:bg-white/5">Mark unread</button>
📝 Committable suggestion

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

Suggested change
role="menu"
aria-label="Message overflow menu"
className="bg-shell-surface border border-white/10 rounded-md shadow-lg py-1 min-w-[160px] text-sm"
>
{isOwn && (
<button role="menuitem" onClick={onEdit}
className="block w-full text-left px-3 py-1.5 hover:bg-white/5">Edit</button>
)}
{isOwn && (
<button role="menuitem" onClick={onDelete}
className="block w-full text-left px-3 py-1.5 hover:bg-white/5 text-red-300">Delete</button>
)}
<button role="menuitem" onClick={onCopyLink}
className="block w-full text-left px-3 py-1.5 hover:bg-white/5">Copy link</button>
{isHuman && (
<button role="menuitem" onClick={onPin}
className="block w-full text-left px-3 py-1.5 hover:bg-white/5">
{isPinned ? "Unpin" : "Pin"}
</button>
)}
<button role="menuitem" onClick={onMarkUnread}
className="block w-full text-left px-3 py-1.5 hover:bg-white/5">Mark unread</button>
aria-label="Message overflow menu"
className="bg-shell-surface border border-white/10 rounded-md shadow-lg py-1 min-w-[160px] text-sm"
>
{isOwn && (
<button type="button" onClick={onEdit}
className="block w-full text-left px-3 py-1.5 hover:bg-white/5">Edit</button>
)}
{isOwn && (
<button type="button" onClick={onDelete}
className="block w-full text-left px-3 py-1.5 hover:bg-white/5 text-red-300">Delete</button>
)}
<button type="button" onClick={onCopyLink}
className="block w-full text-left px-3 py-1.5 hover:bg-white/5">Copy link</button>
{isHuman && (
<button type="button" onClick={onPin}
className="block w-full text-left px-3 py-1.5 hover:bg-white/5">
{isPinned ? "Unpin" : "Pin"}
</button>
)}
<button type="button" onClick={onMarkUnread}
className="block w-full text-left px-3 py-1.5 hover:bg-white/5">Mark unread</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/chat/MessageOverflowMenu.tsx` around lines 18 - 39, The
component MessageOverflowMenu is using role="menu" and role="menuitem" without
implementing menu keyboard behavior; remove the ARIA menu semantics and use
plain buttons instead: delete role="menu" from the container and remove
role="menuitem" from each button (keep the existing onClick handlers like
onEdit, onDelete, onCopyLink, onPin, onMarkUnread), ensure the container still
has an accessible label (use aria-label or visually-hidden heading if needed)
and preserve focus/keyboard activation via the native button elements.

Comment on lines +38 to +40
fetch(`/api/chat/channels/${channelId}/threads/${parentId}/messages`)
.then((r) => r.json())
.then((d) => { if (alive) setMsgs(d.messages || []); });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Thread messages are parsed with the wrong response shape.

Line 40 assumes { messages: [...] }, but this endpoint returns an array. As written, msgs is always set to [], so thread replies won’t render.

Suggested fix
     fetch(`/api/chat/channels/${channelId}/threads/${parentId}/messages`)
       .then((r) => r.json())
-      .then((d) => { if (alive) setMsgs(d.messages || []); });
+      .then((d) => {
+        if (!alive) return;
+        setMsgs(Array.isArray(d) ? d : []);
+      });
📝 Committable suggestion

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

Suggested change
fetch(`/api/chat/channels/${channelId}/threads/${parentId}/messages`)
.then((r) => r.json())
.then((d) => { if (alive) setMsgs(d.messages || []); });
fetch(`/api/chat/channels/${channelId}/threads/${parentId}/messages`)
.then((r) => r.json())
.then((d) => {
if (!alive) return;
setMsgs(Array.isArray(d) ? d : []);
});
🤖 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 38 - 40, The fetch in
ThreadPanel.tsx is treating the response as { messages: [...] } but the endpoint
returns an array, so setMsgs is always given [] — update the response handling
for fetch(`/api/chat/channels/${channelId}/threads/${parentId}/messages`) to
expect an array (e.g., if Array.isArray(d) then setMsgs(d) else setMsgs([])) and
use the existing setMsgs state updater (and keep the alive guard) so thread
replies populate correctly; locate the fetch call and adjust the .then((d) =>
...) handler accordingly, referencing setMsgs, msgs, channelId and parentId.

Comment on lines +44 to +49
async function submit() {
const content = input.trim();
if (!content) return;
setInput("");
await onSend(content, []);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t drop the draft when onSend fails.

submit() clears input before awaiting onSend. If the request fails, the user loses typed content with no recovery path.

Suggested fix
   async function submit() {
-    const content = input.trim();
-    if (!content) return;
-    setInput("");
-    await onSend(content, []);
+    const content = input.trim();
+    if (!content) return;
+    setInput("");
+    try {
+      await onSend(content, []);
+    } catch {
+      setInput(content);
+    }
   }
📝 Committable suggestion

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

Suggested change
async function submit() {
const content = input.trim();
if (!content) return;
setInput("");
await onSend(content, []);
}
async function submit() {
const content = input.trim();
if (!content) return;
setInput("");
try {
await onSend(content, []);
} catch {
setInput(content);
}
}
🤖 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 44 - 49, submit currently
clears the draft (calls setInput("")) before awaiting onSend, so failures drop
the user's text; change submit to capture const content = input.trim(), return
if empty, then await onSend(content, []), and only call setInput("") after
onSend succeeds (or in a try/catch restore the original input on failure) —
update the submit function to call setInput("") on success and preserve or
restore input on error, referencing the submit, input, setInput and onSend
symbols.

Comment on lines +542 to +554
/* ---- deep-link scroll on ?msg=<id> ---- */
useEffect(() => {
if (!selectedChannel || messages.length === 0) return;
const params = new URLSearchParams(window.location.search);
const msgId = params.get("msg");
if (!msgId) return;
const el = document.querySelector(`[data-message-id="${msgId}"]`) as HTMLElement | null;
if (el) {
el.scrollIntoView({ behavior: "smooth", block: "center" });
el.classList.add("data-highlight");
setTimeout(() => el.classList.remove("data-highlight"), 2000);
}
}, [selectedChannel, messages.length]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Deep-link scrolling keeps re-triggering after the initial jump.

This effect is keyed on messages.length, and it never clears or latches the ?msg= param. Once a deep-linked channel is open, every new message will scroll the viewport back to the original target and reapply the highlight.

Please make this one-shot per msg value, or clear the query param after the first successful jump.

🤖 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 542 - 554, The deep-link
effect in MessagesApp.tsx (the useEffect that reads URLSearchParams and queries
`[data-message-id="${msgId}"]`) keeps retriggering because it is keyed on
messages.length and does not clear the ?msg param; after a successful
scroll+highlight you should prevent re-running by either removing the msg param
from the URL (use URLSearchParams to delete "msg" and call
window.history.replaceState with the new search) or by recording the handled id
in a ref (e.g., handledMsgRef.current = msgId) and returning early if the same
id is seen; implement one of these in the effect right after the successful
el.scrollIntoView / class toggle so subsequent message updates won’t reapply the
jump/highlight.

Comment on lines +818 to +835
const handleSaveEdit = async (msgId: string, content: string) => {
try {
await apiEditMessage(msgId, content);
setEditingMessageId(null);
} catch (e) {
setSendError((e as Error).message);
}
};

const handleDelete = async (msgId: string) => {
setOverflowMenu(null);
if (!window.confirm("Delete this message?")) return;
try {
await apiDeleteMessage(msgId);
} catch (e) {
setSendError((e as Error).message);
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Edit/delete success paths never reconcile local message state.

Both handlers just await the API and close UI state; they do not patch messages or refetch. That would be fine if the socket reducer handled the new backend events, but the backend now broadcasts message_update for edits while this component only listens for message_edit, and the delete path later removes the row entirely instead of turning it into a tombstone. The result is that edits stay stale and soft-deletes do not render MessageTombstone until a full reload.

Please either update local state here or align the websocket reducer with the new event payloads before relying on realtime reconciliation.

🤖 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 818 - 835, The edit and delete
handlers (handleSaveEdit and handleDelete) currently only call
apiEditMessage/apiDeleteMessage and close UI state, so local messages state
isn't updated; either update messages directly here after the API call (use
setMessages to replace the edited message content and metadata for msgId in
messages, and for deletes convert the message to a tombstone entry compatible
with MessageTombstone) or adjust the websocket reducer to handle the backend's
new "message_update" event (and treat delete payloads as soft-deletes) so
realtime reconciliation occurs; pick one approach and implement the
corresponding fix referencing handleSaveEdit, handleDelete, setMessages,
messages, apiEditMessage, apiDeleteMessage, the websocket reducer event
handlers, and MessageTombstone.

@@ -0,0 +1 @@
import{r,j as e}from"./vendor-react-l6srOxy7.js";import{B as o,I as we,C as Y,c as Z}from"./toolbar-UW6q5pkx.js";import{$ as W,g as Q,ap as z,a9 as A,aN as ee,a5 as se,l as je,y as X}from"./vendor-icons-wm645Jsx.js";import"./vendor-radix-BhM7AEEG.js";import"./vendor-layout-B-pp9n1f.js";async function y(a,s,n){try{const c=await fetch(a,{...n,headers:{Accept:"application/json",...n==null?void 0:n.headers}});return!c.ok||!(c.headers.get("content-type")??"").includes("application/json")?s:await c.json()}catch{return s}}async function te(a,s,n){return y(a,n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})}async function ae(a,s,n){return y(a,n,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})}async function Ne(a){const n=await y("/api/agent-browsers/profiles",{profiles:[]});return Array.isArray(n.profiles)?n.profiles:[]}async function ye(a,s,n){try{const c=await fetch("/api/agent-browsers/profiles",{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify({profile_name:a,agent_name:s??null,node:n??"local"})});return!c.ok||!(c.headers.get("content-type")??"").includes("application/json")?null:await c.json()}catch{return null}}async function ve(a){try{return(await fetch(`/api/agent-browsers/profiles/${encodeURIComponent(a)}`,{method:"DELETE",headers:{Accept:"application/json"}})).ok}catch{return!1}}async function ke(a){try{return(await fetch(`/api/agent-browsers/profiles/${encodeURIComponent(a)}/data`,{method:"DELETE",headers:{Accept:"application/json"}})).ok}catch{return!1}}async function Ce(a){return await te(`/api/agent-browsers/profiles/${encodeURIComponent(a)}/start`,{},null)}async function Se(a){return await te(`/api/agent-browsers/profiles/${encodeURIComponent(a)}/stop`,{},null)}async function ze(a){return(await y(`/api/agent-browsers/profiles/${encodeURIComponent(a)}/screenshot`,{})).data??null}async function Ae(a){try{const s=await fetch(`/api/agent-browsers/profiles/${encodeURIComponent(a)}/login-status`,{headers:{Accept:"application/json"}});return!s.ok||!(s.headers.get("content-type")??"").includes("application/json")?null:await s.json()}catch{return null}}async function De(a,s){return await ae(`/api/agent-browsers/profiles/${encodeURIComponent(a)}/assign`,{agent_name:s},null)}async function Pe(a,s){return await ae(`/api/agent-browsers/profiles/${encodeURIComponent(a)}/move`,{node:s},null)}const ne=[{key:"x",label:"X / Twitter"},{key:"github",label:"GitHub"},{key:"youtube",label:"YouTube"},{key:"reddit",label:"Reddit"}];function re({status:a}){const s=a==="running"?"bg-green-500/15 text-green-400 border border-green-500/30":a==="error"?"bg-red-500/15 text-red-400 border border-red-500/30":"bg-white/10 text-shell-text-tertiary border border-white/10";return e.jsx("span",{className:`inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium ${s}`,children:a})}function le({node:a}){return e.jsx("span",{className:"inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-accent/10 text-accent border border-accent/20",children:a})}function $e({status:a}){return e.jsx("div",{className:"flex gap-1","aria-label":"Login status indicators",children:ne.map(({key:s,label:n})=>e.jsx("span",{title:n,"aria-label":`${n}: ${a?a[s]?"logged in":"not logged in":"unknown"}`,className:`w-2 h-2 rounded-full ${a?a[s]?"bg-green-400":"bg-white/20":"bg-white/10"}`},s))})}function Ie({profile:a,loginStatus:s,selected:n,onSelect:c,onToggle:x,toggling:m}){return e.jsx(Y,{role:"button",tabIndex:0,"aria-selected":n,"aria-label":`Browser profile: ${a.profile_name}`,onClick:c,onKeyDown:h=>{(h.key==="Enter"||h.key===" ")&&(h.preventDefault(),c())},className:`cursor-pointer transition-colors select-none ${n?"border-accent/50 bg-accent/5":"border-white/5 hover:border-white/15 hover:bg-white/3"}`,children:e.jsxs(Z,{className:"p-3 space-y-2",children:[e.jsxs("div",{className:"flex items-start justify-between gap-2",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsx("p",{className:"text-sm font-semibold truncate",children:a.profile_name}),a.agent_name&&e.jsx("p",{className:"text-xs text-shell-text-tertiary truncate",children:a.agent_name})]}),e.jsx(re,{status:a.status})]}),e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx(le,{node:a.node}),e.jsx($e,{status:s})]}),e.jsx(o,{variant:"ghost",size:"sm","aria-label":a.status==="running"?"Stop browser":"Start browser",disabled:m,onClick:x,className:"h-6 w-6 p-0 shrink-0",children:a.status==="running"?e.jsx(ee,{size:12,className:"text-red-400"}):e.jsx(se,{size:12,className:"text-green-400"})})]})]})})}function Te({onSelect:a,selected:s}){return e.jsx(Y,{role:"button",tabIndex:0,"aria-label":"Create new browser profile","aria-selected":s,onClick:a,onKeyDown:n=>{(n.key==="Enter"||n.key===" ")&&(n.preventDefault(),a())},className:`cursor-pointer transition-colors border-dashed ${s?"border-accent/50 bg-accent/5":"border-white/10 hover:border-accent/30 hover:bg-white/3"}`,children:e.jsxs(Z,{className:"p-3 flex items-center gap-2 text-shell-text-tertiary",children:[e.jsx(Q,{size:14}),e.jsx("span",{className:"text-sm",children:"New Profile"})]})})}function Ue({windowId:a}){const[s,n]=r.useState(null),[c,x]=r.useState(null),[m,h]=r.useState([]),[D,ie]=r.useState({}),[P,ce]=r.useState({}),[$,oe]=r.useState([]),[de,I]=r.useState(!0),[v,T]=r.useState(null),[w,L]=r.useState(!1),[f,B]=r.useState(""),[k,E]=r.useState(""),[C,_]=r.useState(!1),[xe,j]=r.useState(!1),[g,R]=r.useState(""),[S,U]=r.useState("local"),d=typeof window<"u"&&window.innerWidth<640,[he,p]=r.useState(!1),N=r.useCallback(async()=>{I(!0);const t=await Ne();h(t),I(!1)},[]);r.useEffect(()=>{N()},[N]);const O=r.useCallback(async()=>{try{const t=await fetch("/api/agents",{headers:{Accept:"application/json"}});if(t.ok&&(t.headers.get("content-type")??"").includes("application/json")){const l=await t.json();Array.isArray(l)&&oe(l.map(u=>({name:String(u.name??"unknown"),color:String(u.color??"#3b82f6")})))}}catch{}},[]);r.useEffect(()=>{O()},[O]);const V=r.useCallback(async t=>{const i=await Ae(t);i&&ie(l=>({...l,[t]:i}))},[]);r.useEffect(()=>{for(const t of m)V(t.id)},[m,V]);const b=r.useCallback(async t=>{L(!0);const i=await ze(t);i&&ce(l=>({...l,[t]:i})),L(!1)},[]),ue=r.useCallback(t=>{n(t),x("detail"),j(!1),R(t.agent_name??""),U(t.node),t.status==="running"&&b(t.id),d&&p(!0)},[b,d]),me=r.useCallback(()=>{n(null),x("create"),B(""),E(""),d&&p(!0)},[d]),F=r.useCallback(()=>{p(!1),x(null),n(null)},[]),J=r.useCallback(async(t,i)=>{i==null||i.stopPropagation(),T(t.id);let l=null;t.status==="running"?l=await Se(t.id):l=await Ce(t.id),l&&(h(u=>u.map(H=>H.id===l.id?l:H)),(s==null?void 0:s.id)===l.id&&(n(l),l.status==="running"&&b(l.id))),T(null)},[s,b]),M=r.useCallback(async()=>{if(!f.trim())return;_(!0),await ye(f.trim(),k||void 0,"local")&&(await N(),x(null),n(null),d&&p(!1)),_(!1)},[f,k,N,d]),fe=r.useCallback(async()=>{if(!s)return;await ve(s.id)&&(h(i=>i.filter(l=>l.id!==s.id)),n(null),x(null),d&&p(!1))},[s,d]),ge=r.useCallback(async()=>{if(!s)return;await ke(s.id)&&j(!1)},[s]),pe=r.useCallback(async()=>{if(!s||!g)return;const t=await De(s.id,g);t&&(h(i=>i.map(l=>l.id===t.id?t:l)),n(t))},[s,g]),be=r.useCallback(async()=>{if(!s)return;const t=await Pe(s.id,S);t&&(h(i=>i.map(l=>l.id===t.id?t:l)),n(t))},[s,S]),q=e.jsxs("div",{className:"flex flex-col h-full","aria-label":"Create new browser profile",children:[e.jsxs("div",{className:"flex items-center gap-2 px-4 py-3 border-b border-white/5 shrink-0",children:[d&&e.jsx(o,{variant:"ghost",size:"sm","aria-label":"Back",onClick:F,className:"h-7 w-7 p-0 mr-1",children:e.jsx(W,{size:14})}),e.jsx(Q,{size:14,className:"text-accent"}),e.jsx("h2",{className:"text-sm font-semibold",children:"New Profile"})]}),e.jsxs("div",{className:"flex-1 overflow-y-auto p-4 space-y-4",children:[e.jsxs("div",{className:"space-y-1.5",children:[e.jsx("label",{htmlFor:"new-profile-name",className:"text-xs text-shell-text-tertiary",children:"Profile name"}),e.jsx(we,{id:"new-profile-name",placeholder:"e.g. research-main",value:f,onChange:t=>B(t.target.value),onKeyDown:t=>{t.key==="Enter"&&M()},"aria-required":"true"})]}),e.jsxs("div",{className:"space-y-1.5",children:[e.jsx("label",{htmlFor:"new-profile-agent",className:"text-xs text-shell-text-tertiary",children:"Assign agent (optional)"}),e.jsxs("select",{id:"new-profile-agent",value:k,onChange:t=>E(t.target.value),className:"w-full h-9 rounded-md border border-white/10 bg-shell-surface/50 px-3 text-sm text-shell-text focus:outline-none focus:ring-1 focus:ring-accent",children:[e.jsx("option",{value:"",children:"Unassigned"}),$.map(t=>e.jsx("option",{value:t.name,children:t.name},t.name))]})]}),e.jsx(o,{onClick:M,disabled:!f.trim()||C,className:"w-full","aria-busy":C,children:C?"Creating…":"Create Profile"})]})]}),G=s?e.jsxs("div",{className:"flex flex-col h-full","aria-label":`Browser profile details: ${s.profile_name}`,children:[e.jsxs("div",{className:"flex items-center gap-2 px-4 py-3 border-b border-white/5 shrink-0",children:[d&&e.jsx(o,{variant:"ghost",size:"sm","aria-label":"Back",onClick:F,className:"h-7 w-7 p-0 mr-1",children:e.jsx(W,{size:14})}),e.jsx(z,{size:14,className:"text-accent shrink-0"}),e.jsxs("div",{className:"flex-1 min-w-0",children:[e.jsx("h2",{className:"text-sm font-semibold truncate",children:s.profile_name}),s.agent_name&&e.jsx("p",{className:"text-xs text-shell-text-tertiary truncate",children:s.agent_name})]}),e.jsxs("div",{className:"flex items-center gap-1.5 shrink-0",children:[e.jsx(le,{node:s.node}),e.jsx(re,{status:s.status})]})]}),e.jsxs("div",{className:"flex-1 overflow-y-auto p-4 space-y-4",children:[e.jsxs("section",{"aria-labelledby":"screenshot-heading",children:[e.jsx("h3",{id:"screenshot-heading",className:"text-xs font-medium text-shell-text-tertiary uppercase tracking-wider mb-2",children:"Preview"}),e.jsx("div",{className:"relative w-full aspect-video bg-shell-surface/50 border border-white/5 rounded-md overflow-hidden flex items-center justify-center",children:w?e.jsxs("div",{className:"flex items-center gap-2 text-shell-text-tertiary text-xs",children:[e.jsx(A,{size:12,className:"animate-spin"}),e.jsx("span",{children:"Loading preview…"})]}):P[s.id]?e.jsx("img",{src:P[s.id],alt:`Screenshot of ${s.profile_name}`,className:"w-full h-full object-contain"}):e.jsx("p",{className:"text-xs text-shell-text-tertiary text-center px-4",children:s.status==="running"?"No screenshot available":"Start browser to see preview"})})]}),e.jsxs("section",{"aria-labelledby":"login-status-heading",children:[e.jsx("h3",{id:"login-status-heading",className:"text-xs font-medium text-shell-text-tertiary uppercase tracking-wider mb-2",children:"Login Status"}),e.jsx("div",{className:"space-y-1",children:ne.map(({key:t,label:i})=>{const l=D[s.id],u=l?l[t]:null;return e.jsxs("div",{className:"flex items-center gap-2 text-sm",children:[e.jsx("span",{className:`w-2 h-2 rounded-full shrink-0 ${u===!0?"bg-green-400":u===!1?"bg-red-400/60":"bg-white/20"}`,"aria-hidden":"true"}),e.jsx("span",{className:"text-shell-text-secondary",children:i}),e.jsx("span",{className:"ml-auto text-xs text-shell-text-tertiary",children:u===!0?"Logged in":u===!1?"Not logged in":"Unknown"})]},t)})})]}),e.jsxs("section",{"aria-labelledby":"actions-heading",children:[e.jsx("h3",{id:"actions-heading",className:"text-xs font-medium text-shell-text-tertiary uppercase tracking-wider mb-2",children:"Actions"}),e.jsxs("div",{className:"space-y-2",children:[e.jsxs("div",{className:"flex gap-2",children:[e.jsx(o,{variant:s.status==="running"?"secondary":"default",size:"sm",disabled:v===s.id,onClick:()=>J(s),"aria-busy":v===s.id,className:"flex-1 flex items-center gap-1.5",children:s.status==="running"?e.jsxs(e.Fragment,{children:[e.jsx(ee,{size:12}),"Stop"]}):e.jsxs(e.Fragment,{children:[e.jsx(se,{size:12}),"Start"]})}),e.jsxs(o,{variant:"secondary",size:"sm",disabled:s.status!=="running",title:"Opens browser in a taOS window","aria-label":"Connect to browser via noVNC — opens browser in a taOS window",className:"flex-1 flex items-center gap-1.5",onClick:()=>{},children:[e.jsx(je,{size:12}),"Connect"]})]}),s.status==="running"&&e.jsxs(o,{variant:"ghost",size:"sm",onClick:()=>b(s.id),disabled:w,"aria-busy":w,className:"w-full flex items-center gap-1.5 text-xs",children:[e.jsx(A,{size:11,className:w?"animate-spin":""}),"Refresh screenshot"]}),e.jsxs("div",{className:"space-y-1",children:[e.jsx("label",{htmlFor:"assign-agent-select",className:"text-xs text-shell-text-tertiary",children:"Assign agent"}),e.jsxs("div",{className:"flex gap-2",children:[e.jsxs("select",{id:"assign-agent-select",value:g,onChange:t=>R(t.target.value),className:"flex-1 h-8 rounded-md border border-white/10 bg-shell-surface/50 px-2 text-xs text-shell-text focus:outline-none focus:ring-1 focus:ring-accent",children:[e.jsx("option",{value:"",children:"Unassigned"}),$.map(t=>e.jsx("option",{value:t.name,children:t.name},t.name))]}),e.jsx(o,{variant:"secondary",size:"sm",onClick:pe,disabled:!g,className:"shrink-0",children:"Assign"})]})]}),e.jsxs("div",{className:"space-y-1",children:[e.jsx("label",{htmlFor:"move-node-select",className:"text-xs text-shell-text-tertiary",children:"Node"}),e.jsxs("div",{className:"flex gap-2",children:[e.jsx("select",{id:"move-node-select",value:S,onChange:t=>U(t.target.value),className:"flex-1 h-8 rounded-md border border-white/10 bg-shell-surface/50 px-2 text-xs text-shell-text focus:outline-none focus:ring-1 focus:ring-accent",children:e.jsx("option",{value:"local",children:"local"})}),e.jsx(o,{variant:"secondary",size:"sm",onClick:be,className:"shrink-0",children:"Move"})]})]})]})]}),e.jsxs("section",{"aria-labelledby":"danger-heading",children:[e.jsx("h3",{id:"danger-heading",className:"text-xs font-medium text-red-400/70 uppercase tracking-wider mb-2",children:"Danger Zone"}),e.jsxs("div",{className:"space-y-2",children:[e.jsxs(o,{variant:"ghost",size:"sm",onClick:fe,className:"w-full flex items-center gap-1.5 text-red-400 hover:text-red-300 hover:bg-red-500/10 border border-red-500/20","aria-label":"Delete container",children:[e.jsx(X,{size:12}),"Delete container"]}),xe?e.jsxs("div",{className:"rounded-md border border-red-500/30 bg-red-500/5 p-3 space-y-2",children:[e.jsx("p",{className:"text-xs text-red-300",children:"This permanently removes all passwords, bookmarks, cookies, and browsing history."}),e.jsxs("div",{className:"flex gap-2",children:[e.jsx(o,{variant:"ghost",size:"sm",onClick:()=>j(!1),className:"flex-1 text-xs",children:"Cancel"}),e.jsx(o,{size:"sm",onClick:ge,className:"flex-1 text-xs bg-red-600 hover:bg-red-700 text-white border-0","aria-label":"Confirm delete all browser data",children:"Delete all data"})]})]}):e.jsxs(o,{variant:"ghost",size:"sm",onClick:()=>j(!0),className:"w-full flex items-center gap-1.5 text-red-400 hover:text-red-300 hover:bg-red-500/10 border border-red-500/20","aria-label":"Delete browser data",children:[e.jsx(X,{size:12}),"Delete data"]})]})]})]})]}):null,K=e.jsxs("div",{className:"flex flex-col h-full",role:"region","aria-label":"Browser profiles",children:[e.jsxs("div",{className:"flex items-center gap-2 px-4 py-3 border-b border-white/5 shrink-0",children:[e.jsx(z,{size:15,className:"text-accent"}),e.jsx("h1",{className:"text-sm font-semibold",children:"Agent Browsers"})]}),e.jsx("div",{className:"flex-1 overflow-y-auto p-3",children:de?e.jsxs("div",{className:"flex items-center justify-center h-24 text-shell-text-tertiary text-sm",children:[e.jsx(A,{size:14,className:"animate-spin mr-2"}),"Loading profiles…"]}):e.jsxs("div",{role:"list","aria-label":"Browser profile cards",className:"grid grid-cols-1 gap-2",children:[m.map(t=>e.jsx("div",{role:"listitem",children:e.jsx(Ie,{profile:t,loginStatus:D[t.id]??null,selected:(s==null?void 0:s.id)===t.id,onSelect:()=>ue(t),onToggle:i=>J(t,i),toggling:v===t.id})},t.id)),e.jsx("div",{role:"listitem",children:e.jsx(Te,{onSelect:me,selected:c==="create"})})]})})]});return d?e.jsx("div",{className:"w-full h-full bg-shell-bg text-shell-text overflow-hidden",children:he?c==="create"?q:G:K}):e.jsxs("div",{className:"w-full h-full bg-shell-bg text-shell-text flex overflow-hidden",children:[e.jsx("div",{className:"w-72 shrink-0 border-r border-white/5 flex flex-col overflow-hidden",children:K}),e.jsx("div",{className:"flex-1 min-w-0 overflow-hidden",children:c==="create"?q:c==="detail"&&s?G:e.jsx("div",{className:"flex items-center justify-center h-full text-shell-text-tertiary",children:e.jsxs("div",{className:"text-center space-y-2",children:[e.jsx(z,{size:32,className:"mx-auto opacity-20"}),e.jsx("p",{className:"text-sm",children:"Select a profile to view details"}),e.jsx("p",{className:"text-xs opacity-60",children:"or create a new one"})]})})})]})}export{Ue as AgentBrowsersApp};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't ship an enabled Connect action with a no-op handler.

The detail view exposes a real-looking "Connect" button for running profiles, but its handler is onClick: ()=>{}. That makes the control appear available while doing nothing. Either wire it to the actual taOS/noVNC entrypoint or hide/disable it until the backend flow exists.

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

In `@static/desktop/assets/AgentBrowsersApp-CFn8GY-5.js` at line 1, The Connect
button in the AgentBrowsersApp detail view (inside Ue) currently uses a no-op
onClick: ()=>{} making it look functional; replace that by either wiring it to
the real taOS/noVNC entrypoint (call the actual connect function you have for
opening a noVNC session) or disable/hide it until the backend flow
exists—specifically update the o button with aria-label "Connect to browser via
noVNC — opens browser in a taOS window" (and the onClick prop currently set to
()=>{}) to either call the proper connect handler (e.g.,
openNoVNC/openTaOSSession) passing the profile id, or set disabled={true} (and
adjust title/aria-disabled) so it isn’t presented as actionable.

⚠️ Potential issue | 🟠 Major

Don't overwrite profile state with action envelopes.

The start/stop/assign/move handlers (J, pe, be) all replace entries in m and s with the raw mutation response. Those endpoints return {status, id, ...} action payloads, not a full profile record, so the UI will lose profile_name/node/timestamps and start rendering values like "started" or "assigned" as the profile status. Refresh the profile after the mutation, or merge only the changed fields into the existing object. Cross-file evidence: tinyagentos/routes/agent_browsers.py:43-170.

Suggested direction
-const next = await Ce(profile.id);
-if (next) {
-  setProfiles(items => items.map(p => p.id === next.id ? next : p));
-  setSelected(next);
-}
+const result = await Ce(profile.id);
+if (result) {
+  await reloadProfiles();
+  if (selected?.id === profile.id) {
+    const refreshed = await getProfile(profile.id);
+    if (refreshed) setSelected(refreshed);
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@static/desktop/assets/AgentBrowsersApp-CFn8GY-5.js` at line 1, The handlers
J, pe, and be are replacing profile objects in state m (and selected s) with
action-envelope responses (which only include status/id/etc.), causing loss of
fields like profile_name/node; instead, after receiving the mutation response,
fetch the full updated profile or merge only the changed fields into the
existing profile object before updating state. Concretely: in J, pe, and be,
avoid directly using the returned envelope to replace items in h (m) and n (s);
either call the profile refresh N() (or an API to GET the single profile) and
use that full profile to update h and n, or merge the envelope onto the existing
profile (e.g., map h and for the matching id spread the old profile and the
returned fields) so profile_name, node, timestamps remain intact.

⚠️ Potential issue | 🟠 Major

Fetch screenshots as binary, not through the JSON helper.

ze() calls y() against /screenshot, but that route returns image/png. Because y() only accepts JSON, it falls back to {}, so P[s.id] never gets populated and the preview stays empty even for running profiles. Cross-file evidence: tinyagentos/routes/agent_browsers.py:98-105, tinyagentos/agent_browsers.py:323-338.

Suggested fix
-async function ze(id) {
-  return (await y(`/api/agent-browsers/profiles/${encodeURIComponent(id)}/screenshot`, {})).data ?? null;
-}
+async function ze(id) {
+  try {
+    const res = await fetch(`/api/agent-browsers/profiles/${encodeURIComponent(id)}/screenshot`);
+    if (!res.ok || !res.headers.get("content-type")?.includes("image/png")) return null;
+    return URL.createObjectURL(await res.blob());
+  } catch {
+    return null;
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@static/desktop/assets/AgentBrowsersApp-CFn8GY-5.js` at line 1, The screenshot
helper ze() currently calls the JSON-only helper y() so image/png responses are
ignored; update ze() to perform a raw fetch to
`/api/agent-browsers/profiles/${encodeURIComponent(a)}/screenshot` that reads
the response as a Blob (or arrayBuffer) and converts it to a data URL (or
createObjectURL) before returning it; ensure the b callback (function b / const
b = r.useCallback(...)) continues to call ze(t) and stores the returned data URL
into state P via ce(l=>({...l,[t]:i})), so P[s.id] is populated and the preview
image displays.

@@ -0,0 +1 @@
import{r as i,j as e}from"./vendor-react-l6srOxy7.js";import{B as x,I as Be,C as H,a as O,c as E,S as Te,d as Ae,e as Ge,f as M,g as U}from"./toolbar-UW6q5pkx.js";import{M as He}from"./MobileSplitView-CtNEF6zb.js";import{u as Oe}from"./use-is-mobile-v5lglusa.js";import{aX as y,aY as w,B as ie,aR as oe,aZ as v,a_ as N,am as ce,r as Ee,S as de,D as q,aL as W,a1 as Me,$ as P,ay as K,a$ as F,aF as xe,ac as Ue}from"./vendor-icons-wm645Jsx.js";import"./vendor-radix-BhM7AEEG.js";import"./vendor-layout-B-pp9n1f.js";async function k(l,c,s){try{const d=await fetch(l,{...s,headers:{Accept:"application/json",...s==null?void 0:s.headers}});return!d.ok||!(d.headers.get("content-type")??"").includes("application/json")?c:await d.json()}catch{return c}}async function qe(l,c,s){return k(l,s,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c)})}async function We(l){const s=new URLSearchParams().toString(),d=`/api/github/starred${s?`?${s}`:""}`,n=await k(d,{repos:[],total:0});return{repos:Array.isArray(n.repos)?n.repos:[],total:n.total??0}}async function Pe(){const l=await k("/api/github/notifications",{notifications:[],unread_count:0});return{notifications:Array.isArray(l.notifications)?l.notifications:[],unread_count:l.unread_count??0}}async function Ke(l,c){try{const s=await fetch(`/api/github/repo/${encodeURIComponent(l)}/${encodeURIComponent(c)}`,{headers:{Accept:"application/json"}});return!s.ok||!(s.headers.get("content-type")??"").includes("application/json")?null:await s.json()}catch{return null}}async function Fe(l,c,s){try{const d=await fetch(`/api/github/repo/${encodeURIComponent(l)}/${encodeURIComponent(c)}/issues/${s}`,{headers:{Accept:"application/json"}});return!d.ok||!(d.headers.get("content-type")??"").includes("application/json")?null:await d.json()}catch{return null}}async function Ve(l,c){const s=await k(`/api/github/repo/${encodeURIComponent(l)}/${encodeURIComponent(c)}/releases`,{releases:[]});return Array.isArray(s.releases)?s.releases:[]}async function Je(){return k("/api/github/auth/status",{authenticated:!1})}async function Ye(l){return qe("/api/knowledge/ingest",{url:l,title:"",text:"",categories:[],source:"github-browser"},null)}const j=l=>{if(!l)return"";const c=new Date(l),s=(Date.now()-c.getTime())/1e3;return s<60?"just now":s<3600?`${Math.floor(s/60)}m ago`:s<86400?`${Math.floor(s/3600)}h ago`:s<604800?`${Math.floor(s/86400)}d ago`:c.toLocaleDateString()},Xe=l=>l<1024?`${l} B`:l<1048576?`${(l/1024).toFixed(1)} KB`:`${(l/1048576).toFixed(1)} MB`,he=l=>l==="open"?"bg-green-500/15 text-green-400 border-green-500/30":l==="closed"?"bg-red-500/15 text-red-400 border-red-500/30":l==="merged"?"bg-slate-500/15 text-slate-400 border-slate-500/30":"bg-white/10 text-shell-text-tertiary border-white/10";function Ze({comment:l,depth:c=0}){const[s,d]=i.useState(c>=3);return e.jsxs("div",{className:`border-l-2 ${c===0?"border-white/10":"border-white/5"} pl-3 py-1`,style:{marginLeft:c>0?`${c*12}px`:0},children:[e.jsxs("div",{className:"flex items-center gap-2 mb-1",children:[e.jsx("span",{className:"text-xs font-medium text-shell-text-secondary",children:l.author}),e.jsx("span",{className:"text-[10px] text-shell-text-tertiary",children:j(l.created_at)}),c>=3&&e.jsx("button",{className:"text-[10px] text-accent hover:underline ml-1",onClick:()=>d(n=>!n),"aria-expanded":!s,"aria-label":s?"Expand comment":"Collapse comment",children:s?"expand":"collapse"})]}),!s&&e.jsxs(e.Fragment,{children:[e.jsx("p",{className:"text-xs text-shell-text-secondary whitespace-pre-wrap leading-relaxed mb-1",children:l.body}),Object.keys(l.reactions??{}).length>0&&e.jsx("div",{className:"flex gap-1.5 flex-wrap mb-1",children:Object.entries(l.reactions).map(([n,u])=>u>0?e.jsxs("span",{className:"px-1.5 py-0.5 rounded bg-white/5 border border-white/10 text-[10px] text-shell-text-secondary","aria-label":`${n}: ${u}`,children:[n," ",u]},n):null)})]})]})}function nt({windowId:l}){const[,c]=i.useState("list"),[s,d]=i.useState(null),[n,u]=i.useState("starred"),[S,V]=i.useState("repos"),[pe,ue]=i.useState(null),[J,be]=i.useState([]),[Y,me]=i.useState([]),[C,fe]=i.useState(0),[X]=i.useState([]),[Z,$]=i.useState(!0),[p,D]=i.useState(""),[Q,z]=i.useState(!1),[ge,ee]=i.useState([]),[je,te]=i.useState(!1),[m,se]=i.useState(!1),[h,L]=i.useState(!1),[R,ye]=i.useState({authenticated:!1}),f=Oe(),ae=i.useCallback(async()=>{const t=await Je();ye(t)},[]),I=i.useCallback(async()=>{$(!0);const t=await We();be(t.repos),$(!1)},[]),_=i.useCallback(async()=>{$(!0);const t=await Pe();me(t.notifications),fe(t.unread_count),$(!1)},[]);i.useEffect(()=>{ae(),I(),_()},[ae,I,_]),i.useEffect(()=>{c("list"),d(null),D(""),n==="starred"||n==="watched"?I():n==="notifications"&&_()},[n,I,_]);const B=i.useCallback(async t=>{c("detail"),d({type:"repo",repo:t}),L(!1),te(!1),z(!0);const[a,r]=await Promise.all([Ve(t.owner,t.name),Ke(t.owner,t.name)]);ee(a),r&&d({type:"repo",repo:r}),z(!1)},[]),T=i.useCallback(async t=>{c("detail"),d({type:"issue",issue:t}),L(!1),z(!0);const[a,r]=t.repo.split("/");if(a&&r){const o=await Fe(a,r,t.number);o&&d({type:"issue",issue:o})}z(!1)},[]),re=i.useCallback((t,a)=>{c("detail"),d({type:"release",release:{...t,repo:a}}),L(!1)},[]),b=i.useCallback(()=>{c("list"),d(null),ee([])},[]),le=i.useMemo(()=>s?s.type==="repo"&&s.repo?`repo:${s.repo.owner}/${s.repo.name}`:s.type==="issue"&&s.issue?`issue:${s.issue.repo}#${s.issue.number}`:s.type==="release"&&s.release?`release:${s.release.tag}`:null:null,[s]),A=i.useCallback(async t=>{se(!0);const a=await Ye(t);se(!1),a&&L(!0)},[]),g=i.useMemo(()=>n==="starred"||n==="watched"?(n==="watched"?X:J).filter(a=>{var o;if(!p)return!0;const r=p.toLowerCase();return a.name.toLowerCase().includes(r)||a.owner.toLowerCase().includes(r)||((o=a.description)==null?void 0:o.toLowerCase().includes(r))}):n==="notifications"?Y.filter(t=>{if(!p)return!0;const a=p.toLowerCase();return t.title.toLowerCase().includes(a)||t.repo.toLowerCase().includes(a)}):[],[n,J,X,Y,p]),we=e.jsxs("nav",{className:"w-52 shrink-0 border-r border-white/5 bg-shell-surface/30 flex flex-col overflow-hidden","aria-label":"GitHub Browser navigation",children:[e.jsxs("div",{className:"flex items-center gap-2 px-3 py-3 border-b border-white/5 shrink-0",children:[e.jsx(y,{size:15,className:"text-accent","aria-hidden":"true"}),e.jsx("h1",{className:"text-sm font-semibold",children:"GitHub"})]}),e.jsxs("div",{className:"flex-1 overflow-y-auto p-2 space-y-4",children:[e.jsx("section",{"aria-label":"Sections",children:e.jsxs("div",{className:"space-y-0.5",children:[e.jsxs(x,{variant:n==="starred"?"secondary":"ghost",size:"sm","aria-pressed":n==="starred",onClick:()=>u("starred"),className:"w-full justify-start text-xs h-7 px-2 gap-1.5",children:[e.jsx(w,{size:11,"aria-hidden":"true"}),"Starred Repos"]}),e.jsxs(x,{variant:n==="notifications"?"secondary":"ghost",size:"sm","aria-pressed":n==="notifications",onClick:()=>u("notifications"),className:"w-full justify-between text-xs h-7 px-2",children:[e.jsxs("span",{className:"flex items-center gap-1.5",children:[e.jsx(ie,{size:11,"aria-hidden":"true"}),"Notifications"]}),C>0&&e.jsx("span",{className:"px-1.5 py-0.5 rounded-full bg-accent text-white text-[10px] tabular-nums","aria-label":`${C} unread`,children:C})]}),e.jsxs(x,{variant:n==="watched"?"secondary":"ghost",size:"sm","aria-pressed":n==="watched",onClick:()=>u("watched"),className:"w-full justify-start text-xs h-7 px-2 gap-1.5",children:[e.jsx(oe,{size:11,"aria-hidden":"true"}),"Watched"]})]})}),e.jsxs("section",{"aria-label":"Content type",children:[e.jsx("p",{className:"text-[10px] uppercase tracking-wider text-shell-text-tertiary px-2 mb-1.5",children:"Content"}),e.jsx("div",{className:"space-y-0.5",children:[{id:"repos",label:"Repos",icon:y},{id:"issues",label:"Issues",icon:v},{id:"prs",label:"Pull Requests",icon:N},{id:"releases",label:"Releases",icon:ce}].map(({id:t,label:a,icon:r})=>e.jsxs(x,{variant:S===t?"secondary":"ghost",size:"sm","aria-pressed":S===t,onClick:()=>V(t),className:"w-full justify-start text-xs h-7 px-2 gap-1.5",children:[e.jsx(r,{size:11,"aria-hidden":"true"}),a]},t))})]}),e.jsxs("section",{"aria-label":"Status filter",children:[e.jsx("p",{className:"text-[10px] uppercase tracking-wider text-shell-text-tertiary px-2 mb-1.5",children:"Status"}),e.jsx("div",{className:"space-y-0.5",children:["open","closed","merged"].map(t=>{const a=pe===t;return e.jsx(x,{variant:a?"secondary":"ghost",size:"sm","aria-pressed":a,onClick:()=>ue(r=>r===t?null:t),className:"w-full justify-start text-xs h-7 px-2 capitalize",children:t},t)})})]})]}),e.jsx("div",{className:"shrink-0 border-t border-white/5 px-3 py-2",children:R.authenticated?e.jsxs("div",{className:"space-y-0.5",children:[e.jsx("p",{className:"text-[10px] text-shell-text-tertiary capitalize",children:R.method??"connected"}),e.jsxs("p",{className:"text-xs text-shell-text-secondary truncate",children:["@",R.username]})]}):e.jsx("button",{className:"text-xs text-accent hover:underline",onClick:()=>{},"aria-label":"Connect GitHub account",children:"Connect GitHub"})})]}),ne=R.authenticated?null:e.jsxs("div",{className:"flex items-center gap-3 px-4 py-2 bg-amber-500/10 border-b border-amber-500/20 text-xs text-amber-300 shrink-0",role:"banner","aria-label":"GitHub authentication notice",children:[e.jsx(Ee,{size:13,"aria-hidden":"true"}),e.jsx("span",{children:"Connect GitHub for starred repos and notifications."}),e.jsx("button",{className:"ml-auto underline hover:text-amber-200","aria-label":"Open Secrets app to connect GitHub",children:"Connect"})]}),ve=t=>e.jsxs(H,{className:"cursor-pointer hover:border-white/15 transition-colors",onClick:()=>B(t),onKeyDown:a=>{(a.key==="Enter"||a.key===" ")&&(a.preventDefault(),B(t))},tabIndex:0,role:"button","aria-label":`Open ${t.owner}/${t.name}`,children:[e.jsxs(O,{className:"pb-1 p-3",children:[e.jsxs("div",{className:"flex items-start justify-between gap-2",children:[e.jsxs("h3",{className:"text-sm font-medium leading-snug",children:[e.jsxs("span",{className:"text-shell-text-tertiary",children:[t.owner,"/"]}),t.name]}),t.language&&e.jsx("span",{className:"shrink-0 text-[10px] px-1.5 py-0.5 rounded bg-accent/10 text-accent border border-accent/20",children:t.language})]}),t.description&&e.jsx("p",{className:"text-[11px] text-shell-text-secondary line-clamp-1 leading-relaxed mt-0.5",children:t.description})]}),e.jsx(E,{className:"pt-0 px-3 pb-3",children:e.jsxs("div",{className:"flex items-center gap-3 text-[10px] text-shell-text-tertiary",children:[e.jsxs("span",{className:"flex items-center gap-1","aria-label":`${t.stars} stars`,children:[e.jsx(w,{size:10,"aria-hidden":"true"}),t.stars.toLocaleString()]}),e.jsxs("span",{className:"flex items-center gap-1","aria-label":`${t.forks} forks`,children:[e.jsx(W,{size:10,"aria-hidden":"true"}),t.forks.toLocaleString()]}),e.jsx("span",{className:"ml-auto",children:j(t.updated_at)})]})})]},`${t.owner}/${t.name}`),Ne=t=>e.jsxs(H,{className:"cursor-pointer hover:border-white/15 transition-colors",onClick:()=>T(t),onKeyDown:a=>{(a.key==="Enter"||a.key===" ")&&(a.preventDefault(),T(t))},tabIndex:0,role:"button","aria-label":`Open ${t.is_pull_request?"PR":"issue"}: ${t.title}`,children:[e.jsx(O,{className:"pb-1 p-3",children:e.jsxs("div",{className:"flex items-start gap-2",children:[t.is_pull_request?e.jsx(N,{size:13,className:"mt-0.5 shrink-0 text-accent","aria-hidden":"true"}):e.jsx(v,{size:13,className:"mt-0.5 shrink-0 text-green-400","aria-hidden":"true"}),e.jsxs("div",{className:"flex-1 min-w-0",children:[e.jsx("h3",{className:"text-sm font-medium leading-snug line-clamp-1",children:t.title}),e.jsx("p",{className:"text-[11px] text-shell-text-tertiary mt-0.5",children:t.repo})]}),e.jsx("span",{className:`shrink-0 text-[10px] px-1.5 py-0.5 rounded border ${he(t.state)}`,"aria-label":`Status: ${t.state}`,children:t.state})]})}),e.jsxs(E,{className:"pt-0 px-3 pb-3 space-y-1.5",children:[t.labels.length>0&&e.jsx("div",{className:"flex flex-wrap gap-1","aria-label":"Labels",children:t.labels.map(a=>e.jsx("span",{className:"px-1.5 py-0.5 rounded bg-white/5 border border-white/10 text-[10px] text-shell-text-secondary",children:a},a))}),e.jsxs("div",{className:"flex items-center gap-3 text-[10px] text-shell-text-tertiary",children:[e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx(Me,{size:10,"aria-hidden":"true"}),t.comments.length]}),e.jsx("span",{children:t.author}),e.jsx("span",{className:"ml-auto",children:j(t.created_at)})]})]})]},`${t.repo}#${t.number}`),ke=(t,a="")=>e.jsxs(H,{className:"cursor-pointer hover:border-white/15 transition-colors",onClick:()=>re(t,a),onKeyDown:r=>{(r.key==="Enter"||r.key===" ")&&(r.preventDefault(),re(t,a))},tabIndex:0,role:"button","aria-label":`Open release ${t.tag}`,children:[e.jsx(O,{className:"pb-1 p-3",children:e.jsxs("div",{className:"flex items-start justify-between gap-2",children:[e.jsxs("div",{children:[e.jsxs("h3",{className:"text-sm font-medium leading-snug flex items-center gap-1.5",children:[e.jsx(xe,{size:11,"aria-hidden":"true",className:"text-accent"}),t.tag]}),a&&e.jsx("p",{className:"text-[11px] text-shell-text-tertiary mt-0.5",children:a})]}),t.prerelease&&e.jsx("span",{className:"shrink-0 text-[10px] px-1.5 py-0.5 rounded bg-amber-500/15 text-amber-400 border border-amber-500/30",children:"pre-release"})]})}),e.jsx(E,{className:"pt-0 px-3 pb-3",children:e.jsx("p",{className:"text-[10px] text-shell-text-tertiary",children:j(t.published_at)})})]},t.tag),Se=e.jsxs("main",{className:"flex-1 flex flex-col overflow-hidden","aria-label":"GitHub content list",children:[e.jsx("div",{className:"flex items-center gap-2 px-4 py-3 border-b border-white/5 shrink-0",children:e.jsxs("div",{className:"relative flex-1",children:[e.jsx(de,{size:14,className:"absolute left-3 top-1/2 -translate-y-1/2 text-shell-text-tertiary pointer-events-none z-10","aria-hidden":"true"}),e.jsx(Be,{type:"search",value:p,onChange:t=>D(t.target.value),placeholder:"Search…",className:"pl-8 h-8","aria-label":"Search GitHub content"})]})}),e.jsx("div",{className:"flex-1 overflow-y-auto p-3 space-y-2",role:"list","aria-label":"GitHub items",children:Z?e.jsx("div",{className:"flex items-center justify-center h-full text-shell-text-tertiary text-sm",role:"status","aria-live":"polite",children:"Loading…"}):g.length===0?e.jsxs("div",{className:"flex flex-col items-center justify-center h-full gap-3 text-shell-text-tertiary",children:[e.jsx(y,{size:36,className:"opacity-20","aria-hidden":"true"}),e.jsx("p",{className:"text-sm",children:p?"No results for your search":"Nothing here yet"})]}):n==="notifications"?g.map(t=>e.jsx("div",{role:"listitem",children:Ne(t)},`${t.repo}#${t.number}`)):g.map(t=>e.jsx("div",{role:"listitem",children:ve(t)},`${t.owner}/${t.name}`))})]}),Ce=t=>{const a=`https://github.com/${t.owner}/${t.name}`,r=ge[0]??null;return e.jsx("main",{className:"flex-1 flex flex-col overflow-hidden","aria-label":`${t.owner}/${t.name} detail`,children:e.jsxs("div",{className:"flex-1 overflow-y-auto",children:[e.jsxs("div",{className:"px-5 pt-4 pb-3 border-b border-white/5",children:[!f&&e.jsxs(x,{variant:"ghost",size:"sm",onClick:b,className:"text-xs mb-3 -ml-1 text-shell-text-secondary","aria-label":"Back to list",onKeyDown:o=>o.key==="Escape"&&b(),children:[e.jsx(P,{size:14,"aria-hidden":"true"}),"Back"]}),e.jsxs("h2",{className:"text-lg font-semibold leading-snug mb-1",children:[e.jsxs("span",{className:"text-shell-text-tertiary",children:[t.owner,"/"]}),t.name]}),t.description&&e.jsx("p",{className:"text-sm text-shell-text-secondary mb-3",children:t.description}),e.jsxs("div",{className:"flex flex-wrap gap-2 mb-3",children:[e.jsxs("span",{className:"flex items-center gap-1 text-[11px] px-2 py-0.5 rounded bg-white/5 border border-white/10 text-shell-text-secondary","aria-label":`${t.stars} stars`,children:[e.jsx(w,{size:10,"aria-hidden":"true"}),t.stars.toLocaleString()," stars"]}),e.jsxs("span",{className:"flex items-center gap-1 text-[11px] px-2 py-0.5 rounded bg-white/5 border border-white/10 text-shell-text-secondary","aria-label":`${t.forks} forks`,children:[e.jsx(W,{size:10,"aria-hidden":"true"}),t.forks.toLocaleString()," forks"]}),t.language&&e.jsx("span",{className:"text-[11px] px-2 py-0.5 rounded bg-accent/10 text-accent border border-accent/20",children:t.language}),t.license&&e.jsx("span",{className:"text-[11px] px-2 py-0.5 rounded bg-white/5 border border-white/10 text-shell-text-secondary",children:t.license})]}),t.topics.length>0&&e.jsx("div",{className:"flex flex-wrap gap-1 mb-2","aria-label":"Topics",children:t.topics.map(o=>e.jsx("span",{className:"px-1.5 py-0.5 rounded-full bg-blue-500/10 text-blue-400 text-[10px] border border-blue-500/20",children:o},o))})]}),t.readme_content&&e.jsxs("div",{className:"px-5 py-4 border-b border-white/5",children:[e.jsx("h3",{className:"text-xs font-semibold text-shell-text-tertiary uppercase tracking-wider mb-2",children:"README"}),e.jsx("div",{className:"rounded-lg bg-white/[0.02] border border-white/5 p-3 max-h-64 overflow-y-auto",children:e.jsx("pre",{className:"text-xs text-shell-text-secondary whitespace-pre-wrap leading-relaxed font-sans",children:Q?"Loading…":t.readme_content})})]}),r&&e.jsxs("div",{className:"px-5 py-4 border-b border-white/5",children:[e.jsx("h3",{className:"text-xs font-semibold text-shell-text-tertiary uppercase tracking-wider mb-2",children:"Latest Release"}),ke(r,`${t.owner}/${t.name}`)]}),e.jsxs("div",{className:"px-5 py-3 border-b border-white/5 flex items-center justify-between",children:[e.jsx("label",{htmlFor:`monitor-${t.name}`,className:"text-xs text-shell-text-secondary cursor-pointer",children:"Monitor releases"}),e.jsx(Te,{id:`monitor-${t.name}`,checked:je,onCheckedChange:te,"aria-label":"Monitor releases for this repository"})]}),e.jsxs("div",{className:"px-5 py-3 flex flex-wrap gap-2",children:[e.jsxs(x,{size:"sm",variant:"ghost",className:"text-xs gap-1.5",onClick:()=>window.open(a,"_blank","noopener,noreferrer"),"aria-label":"Open on GitHub",children:[e.jsx(K,{size:13,"aria-hidden":"true"}),"Open on GitHub"]}),e.jsxs(x,{size:"sm",variant:h?"secondary":"outline",className:"text-xs gap-1.5",onClick:()=>A(a),disabled:m||h,"aria-label":h?"Saved to library":"Save to Library",children:[e.jsx(F,{size:13,"aria-hidden":"true"}),h?"Saved":m?"Saving…":"Save to Library"]})]})]})})},$e=t=>{const a=`https://github.com/${t.repo}/${t.is_pull_request?"pull":"issues"}/${t.number}`;return e.jsx("main",{className:"flex-1 flex flex-col overflow-hidden","aria-label":`Issue ${t.number} detail`,children:e.jsxs("div",{className:"flex-1 overflow-y-auto",children:[e.jsxs("div",{className:"px-5 pt-4 pb-3 border-b border-white/5",children:[!f&&e.jsxs(x,{variant:"ghost",size:"sm",onClick:b,className:"text-xs mb-3 -ml-1 text-shell-text-secondary","aria-label":"Back to list",onKeyDown:r=>r.key==="Escape"&&b(),children:[e.jsx(P,{size:14,"aria-hidden":"true"}),"Back"]}),e.jsxs("div",{className:"flex items-start gap-2 mb-2",children:[t.is_pull_request?e.jsx(N,{size:16,className:"mt-0.5 shrink-0 text-accent","aria-hidden":"true"}):e.jsx(v,{size:16,className:"mt-0.5 shrink-0 text-green-400","aria-hidden":"true"}),e.jsx("h2",{className:"text-base font-semibold leading-snug flex-1",children:t.title}),e.jsx("span",{className:`shrink-0 text-[10px] px-1.5 py-0.5 rounded border ${he(t.state)}`,"aria-label":`Status: ${t.state}`,children:t.state})]}),e.jsxs("p",{className:"text-xs text-shell-text-tertiary mb-2",children:[t.repo," · ",t.author," · ",j(t.created_at)]}),t.labels.length>0&&e.jsx("div",{className:"flex flex-wrap gap-1 mb-2","aria-label":"Labels",children:t.labels.map(r=>e.jsx("span",{className:"px-1.5 py-0.5 rounded bg-white/5 border border-white/10 text-[10px] text-shell-text-secondary",children:r},r))})]}),e.jsx("div",{className:"px-5 py-3 flex-1",children:e.jsxs(Ae,{defaultValue:"discussion",children:[e.jsxs(Ge,{children:[e.jsx(M,{value:"discussion",children:"Discussion"}),e.jsx(M,{value:"history",children:"History"}),e.jsx(M,{value:"metadata",children:"Metadata"})]}),e.jsxs(U,{value:"discussion",children:[t.body&&e.jsx("div",{className:"rounded-lg bg-white/[0.02] border border-white/5 p-3 mb-3 mt-3",children:e.jsx("p",{className:"text-xs text-shell-text-secondary whitespace-pre-wrap leading-relaxed",children:Q?"Loading…":t.body})}),t.comments.length>0&&e.jsxs("div",{className:"space-y-2 mt-2","aria-label":"Comments",children:[e.jsxs("p",{className:"text-[10px] uppercase tracking-wider text-shell-text-tertiary mb-1",children:[t.comments.length," comment",t.comments.length!==1?"s":""]}),t.comments.map((r,o)=>e.jsx(Ze,{comment:r,depth:0},o))]})]}),e.jsx(U,{value:"history",children:e.jsx("div",{className:"mt-3 text-xs text-shell-text-tertiary italic",children:"Issue history not available in this view."})}),e.jsx(U,{value:"metadata",children:e.jsx("div",{className:"mt-3 space-y-2",children:[{label:"Number",value:`#${t.number}`},{label:"State",value:t.state},{label:"Author",value:t.author},{label:"Repo",value:t.repo},{label:"Type",value:t.is_pull_request?"Pull Request":"Issue"},{label:"Created",value:t.created_at}].map(({label:r,value:o})=>e.jsxs("div",{className:"flex justify-between text-xs",children:[e.jsx("span",{className:"text-shell-text-tertiary",children:r}),e.jsx("span",{className:"text-shell-text-secondary",children:o})]},r))})})]})}),e.jsxs("div",{className:"px-5 py-3 flex flex-wrap gap-2 border-t border-white/5",children:[e.jsxs(x,{size:"sm",variant:"ghost",className:"text-xs gap-1.5",onClick:()=>window.open(a,"_blank","noopener,noreferrer"),"aria-label":"Open on GitHub",children:[e.jsx(K,{size:13,"aria-hidden":"true"}),"Open on GitHub"]}),e.jsxs(x,{size:"sm",variant:h?"secondary":"outline",className:"text-xs gap-1.5",onClick:()=>A(a),disabled:m||h,"aria-label":h?"Saved to library":"Save to Library",children:[e.jsx(F,{size:13,"aria-hidden":"true"}),h?"Saved":m?"Saving…":"Save to Library"]})]})]})})},ze=t=>{const a=t.repo??"",r=a?`https://github.com/${a}/releases/tag/${encodeURIComponent(t.tag)}`:"#";return e.jsx("main",{className:"flex-1 flex flex-col overflow-hidden","aria-label":`Release ${t.tag} detail`,children:e.jsxs("div",{className:"flex-1 overflow-y-auto",children:[e.jsxs("div",{className:"px-5 pt-4 pb-3 border-b border-white/5",children:[!f&&e.jsxs(x,{variant:"ghost",size:"sm",onClick:b,className:"text-xs mb-3 -ml-1 text-shell-text-secondary","aria-label":"Back to list",onKeyDown:o=>o.key==="Escape"&&b(),children:[e.jsx(P,{size:14,"aria-hidden":"true"}),"Back"]}),e.jsxs("div",{className:"flex items-start gap-2 mb-1",children:[e.jsx(xe,{size:16,className:"mt-0.5 shrink-0 text-accent","aria-hidden":"true"}),e.jsx("h2",{className:"text-lg font-semibold leading-snug",children:t.tag}),t.prerelease&&e.jsx("span",{className:"text-[10px] px-1.5 py-0.5 rounded bg-amber-500/15 text-amber-400 border border-amber-500/30",children:"pre-release"})]}),a&&e.jsx("p",{className:"text-xs text-shell-text-tertiary mb-1",children:a}),e.jsxs("p",{className:"text-xs text-shell-text-tertiary",children:[t.author," · ",j(t.published_at)]})]}),t.body&&e.jsxs("div",{className:"px-5 py-4 border-b border-white/5",children:[e.jsx("h3",{className:"text-xs font-semibold text-shell-text-tertiary uppercase tracking-wider mb-2",children:"Release Notes"}),e.jsx("pre",{className:"text-xs text-shell-text-secondary whitespace-pre-wrap leading-relaxed font-sans",children:t.body})]}),t.assets.length>0&&e.jsxs("div",{className:"px-5 py-4 border-b border-white/5",children:[e.jsxs("h3",{className:"text-xs font-semibold text-shell-text-tertiary uppercase tracking-wider mb-2",children:["Assets (",t.assets.length,")"]}),e.jsx("div",{className:"space-y-1.5",role:"list","aria-label":"Release assets",children:t.assets.map(o=>e.jsxs("div",{className:"flex items-center gap-3 px-3 py-2 rounded-lg bg-white/[0.02] border border-white/5 text-xs",role:"listitem",children:[e.jsx(Ue,{size:11,"aria-hidden":"true",className:"text-shell-text-tertiary shrink-0"}),e.jsx("span",{className:"flex-1 truncate text-shell-text-secondary font-mono",children:o.name}),e.jsx("span",{className:"text-shell-text-tertiary shrink-0",children:Xe(o.size)}),e.jsxs("span",{className:"text-shell-text-tertiary shrink-0","aria-label":`${o.download_count} downloads`,children:[o.download_count.toLocaleString()," dl"]})]},o.name))})]}),e.jsxs("div",{className:"px-5 py-3 flex flex-wrap gap-2",children:[e.jsxs(x,{size:"sm",variant:"ghost",className:"text-xs gap-1.5",onClick:()=>window.open(r,"_blank","noopener,noreferrer"),"aria-label":"Open on GitHub",children:[e.jsx(K,{size:13,"aria-hidden":"true"}),"Open on GitHub"]}),e.jsxs(x,{size:"sm",variant:h?"secondary":"outline",className:"text-xs gap-1.5",onClick:()=>A(r),disabled:m||h||r==="#","aria-label":h?"Saved to library":"Save to Library",children:[e.jsx(F,{size:13,"aria-hidden":"true"}),h?"Saved":m?"Saving…":"Save to Library"]})]})]})})},Le=s?s.type==="repo"&&s.repo?Ce(s.repo):s.type==="issue"&&s.issue?$e(s.issue):s.type==="release"&&s.release?ze(s.release):null:null,Re=i.useMemo(()=>s?s.type==="repo"&&s.repo?`${s.repo.owner}/${s.repo.name}`:s.type==="issue"&&s.issue?s.issue.title:s.type==="release"&&s.release?s.release.tag:"":"",[s]),Ie=!f||le===null,_e=e.jsxs("div",{style:{display:"flex",flexDirection:"column",height:"100%"},children:[ne,e.jsx("div",{style:{padding:"8px 0 4px",borderBottom:"1px solid rgba(255,255,255,0.05)",flexShrink:0},children: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:[{id:"starred",label:"Starred Repos",icon:w,badge:null},{id:"notifications",label:"Notifications",icon:ie,badge:C},{id:"watched",label:"Watched",icon:oe,badge:null}].map(({id:t,label:a,icon:r,badge:o},G,De)=>e.jsxs("button",{type:"button",onClick:()=>u(t),"aria-pressed":n===t,"aria-label":a,style:{display:"flex",alignItems:"center",gap:10,width:"100%",padding:"14px 16px",background:n===t?"rgba(255,255,255,0.08)":"none",border:"none",borderBottom:G===De.length-1?"none":"1px solid rgba(255,255,255,0.06)",cursor:"pointer",color:"inherit",textAlign:"left"},children:[e.jsx(r,{size:15,style:{color:"rgba(255,255,255,0.6)",flexShrink:0},"aria-hidden":"true"}),e.jsx("span",{style:{flex:1,fontSize:15,fontWeight:500,color:"rgba(255,255,255,0.9)"},children:a}),o!=null&&o>0&&e.jsx("span",{style:{fontSize:11,padding:"1px 7px",borderRadius:20,background:"var(--accent, #7c6be8)",color:"#fff",fontWeight:600},"aria-label":`${o} unread`,children:o}),e.jsx(q,{size:14,style:{color:"rgba(255,255,255,0.3)",flexShrink:0},"aria-hidden":"true"})]},t))})}),e.jsxs("div",{style:{padding:"8px 0 4px",borderBottom:"1px solid rgba(255,255,255,0.05)",flexShrink:0},children:[e.jsx("div",{style:{fontSize:12,textTransform:"uppercase",letterSpacing:.5,color:"rgba(255,255,255,0.45)",padding:"0 20px 6px",fontWeight:600},children:"Content"}),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:[{id:"repos",label:"Repos",icon:y},{id:"issues",label:"Issues",icon:v},{id:"prs",label:"Pull Requests",icon:N},{id:"releases",label:"Releases",icon:ce}].map(({id:t,label:a,icon:r},o,G)=>e.jsxs("button",{type:"button",onClick:()=>V(t),"aria-pressed":S===t,"aria-label":a,style:{display:"flex",alignItems:"center",gap:10,width:"100%",padding:"12px 16px",background:S===t?"rgba(255,255,255,0.08)":"none",border:"none",borderBottom:o===G.length-1?"none":"1px solid rgba(255,255,255,0.06)",cursor:"pointer",color:"inherit",textAlign:"left"},children:[e.jsx(r,{size:14,style:{color:"rgba(255,255,255,0.6)",flexShrink:0},"aria-hidden":"true"}),e.jsx("span",{style:{flex:1,fontSize:14,color:"rgba(255,255,255,0.85)"},children:a})]},t))})]}),e.jsxs("div",{style:{flex:1,overflowY:"auto",padding:"8px 0 16px"},children:[e.jsx("div",{style:{fontSize:12,textTransform:"uppercase",letterSpacing:.5,color:"rgba(255,255,255,0.45)",padding:"4px 20px 8px",fontWeight:600},children:n==="notifications"?"Notifications":n==="watched"?"Watched":"Starred"}),e.jsx("div",{style:{padding:"0 12px 8px"},children:e.jsxs("div",{style:{position:"relative"},children:[e.jsx(de,{size:13,style:{position:"absolute",left:10,top:"50%",transform:"translateY(-50%)",color:"rgba(255,255,255,0.4)",pointerEvents:"none"},"aria-hidden":"true"}),e.jsx("input",{type:"search",value:p,onChange:t=>D(t.target.value),placeholder:"Search…","aria-label":"Search GitHub content",style:{width:"100%",padding:"8px 12px 8px 30px",borderRadius:10,background:"rgba(255,255,255,0.06)",border:"1px solid rgba(255,255,255,0.1)",color:"inherit",fontSize:13,outline:"none",boxSizing:"border-box"}})]})}),Z?e.jsx("div",{style:{padding:"24px 20px",textAlign:"center",fontSize:13,color:"rgba(255,255,255,0.4)"},role:"status","aria-live":"polite",children:"Loading…"}):g.length===0?e.jsx("div",{style:{padding:"32px 20px",textAlign:"center",fontSize:13,color:"rgba(255,255,255,0.4)"},children:p?"No results for your search":"Nothing here 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"},role:"list","aria-label":"GitHub items",children:n==="notifications"?g.map((t,a,r)=>e.jsxs("button",{type:"button",role:"listitem",onClick:()=>T(t),"aria-label":`Open ${t.is_pull_request?"PR":"issue"}: ${t.title}`,style:{display:"flex",alignItems:"center",gap:10,width:"100%",padding:"14px 16px",background:"none",border:"none",borderBottom:a===r.length-1?"none":"1px solid rgba(255,255,255,0.06)",cursor:"pointer",color:"inherit",textAlign:"left"},children:[t.is_pull_request?e.jsx(N,{size:13,style:{flexShrink:0,color:"rgba(130,140,255,0.9)"},"aria-hidden":"true"}):e.jsx(v,{size:13,style:{flexShrink:0,color:"rgba(80,200,120,0.9)"},"aria-hidden":"true"}),e.jsxs("div",{style:{flex:1,minWidth:0},children:[e.jsx("div",{style:{fontSize:14,fontWeight:500,color:"rgba(255,255,255,0.9)",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",marginBottom:2},children:t.title}),e.jsx("div",{style:{fontSize:12,color:"rgba(255,255,255,0.45)"},children:t.repo})]}),e.jsx(q,{size:14,style:{color:"rgba(255,255,255,0.3)",flexShrink:0},"aria-hidden":"true"})]},`${t.repo}#${t.number}`)):g.map((t,a,r)=>e.jsxs("button",{type:"button",role:"listitem",onClick:()=>B(t),"aria-label":`Open ${t.owner}/${t.name}`,style:{display:"flex",alignItems:"center",gap:10,width:"100%",padding:"14px 16px",background:"none",border:"none",borderBottom:a===r.length-1?"none":"1px solid rgba(255,255,255,0.06)",cursor:"pointer",color:"inherit",textAlign:"left"},children:[e.jsxs("div",{style:{flex:1,minWidth:0},children:[e.jsxs("div",{style:{fontSize:14,fontWeight:600,color:"rgba(255,255,255,0.95)",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",marginBottom:2},children:[e.jsxs("span",{style:{color:"rgba(255,255,255,0.5)"},children:[t.owner,"/"]}),t.name]}),t.description&&e.jsx("div",{style:{fontSize:12,color:"rgba(255,255,255,0.45)",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:t.description}),e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:10,marginTop:4,fontSize:11,color:"rgba(255,255,255,0.35)"},children:[e.jsxs("span",{style:{display:"flex",alignItems:"center",gap:3},"aria-label":`${t.stars} stars`,children:[e.jsx(w,{size:9,"aria-hidden":"true"})," ",t.stars.toLocaleString()]}),e.jsxs("span",{style:{display:"flex",alignItems:"center",gap:3},"aria-label":`${t.forks} forks`,children:[e.jsx(W,{size:9,"aria-hidden":"true"})," ",t.forks.toLocaleString()]}),t.language&&e.jsx("span",{children:t.language})]})]}),e.jsx(q,{size:14,style:{color:"rgba(255,255,255,0.3)",flexShrink:0},"aria-hidden":"true"})]},`${t.owner}/${t.name}`))})]})]});return e.jsxs("div",{className:"flex flex-col h-full min-h-0 overflow-hidden bg-shell-surface text-shell-text select-none relative",children:[Ie&&e.jsx("div",{className:"flex items-center justify-between px-4 py-3 border-b border-white/5 shrink-0",children:e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(y,{size:15,className:"text-accent shrink-0","aria-hidden":"true"}),e.jsx("h1",{className:"text-sm font-semibold",children:"GitHub"})]})}),e.jsx(He,{selectedId:le,onBack:b,listTitle:"GitHub",detailTitle:Re,listWidth:208,list:f?_e:e.jsxs("div",{className:"flex h-full overflow-hidden",children:[we,e.jsxs("div",{className:"flex-1 flex flex-col overflow-hidden",children:[ne,Se]})]}),detail:Le??(f?null:e.jsx("div",{className:"flex items-center justify-center h-full text-shell-text-tertiary text-sm",children:"Select an item to view details"}))})]})}export{nt as GitHubApp};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The GitHub connect CTAs cannot complete the task.

The unauthenticated footer button uses onClick:()=>{} and the banner "Connect" button has no handler, so users who land here without a token have no way to recover from this screen. Separately, the auth route returns { authenticated, source }, but this UI reads method and username, so authenticated users will get misleading footer text.

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

In `@static/desktop/assets/GitHubApp-IYMAlDty.js` at line 1, The footer and banner
"Connect" buttons in the nt component are no-ops and the UI reads non-existent
R.method causing misleading text; implement real connect handlers and map auth
response fields correctly: hook the unauthenticated footer button (in nt render)
and the banner "Connect" button (ne) to launch the GitHub auth flow (e.g. open
the OAuth start endpoint or navigate to the auth route) so users can complete
sign-in, and update the display logic that currently uses R.method to instead
use the auth response field returned by Je (R.source) and ensure the displayed
username reads the actual user field provided by the auth response (e.g.
R.username or R.user.login) so the footer shows correct status.

⚠️ Potential issue | 🟠 Major

The watched/content/status controls are currently cosmetic.

watched is never populated, and the repos/issues/prs/releases plus open/closed/merged selections only affect button state. The actual fetch/render path still just switches between starred repos and notifications, so users can select views that never change the data. Either wire these controls into real queries/filtering or hide them until they exist.

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

In `@static/desktop/assets/GitHubApp-IYMAlDty.js` at line 1, The controls for
"watched", content type (S) and status filter (pe) are only updating button
state and not changing fetched data — watched (X) is never populated and the
list/filter logic in the memoized g only switches between J (starred) and Y
(notifications). Fix by wiring the UI handlers to real fetches and filters:
populate X when u("watched") is selected (call a fetch like We or a new
fetchWatched function inside the I handler or a dedicated fetchWatched), update
the selection callbacks V (content type) and the status onClick (ue) to trigger
data reloads or apply client-side filters, and incorporate S and pe into the g
useMemo filter logic so items are filtered by content type
(repos/issues/prs/releases) and state (open/closed/merged); alternatively hide
the watched/content/status controls until the corresponding data-fetch functions
(I, fetchWatched, Fe/Ke/Ve) and filters are implemented.

⚠️ Potential issue | 🔴 Critical

Normalize GitHub responses before this component renders them.

This bundle is consuming /api/github/starred and /api/github/notifications as if they were already flattened into app-specific shapes (owner, stars, forks, title, repo, number, etc.), but the provided backend/client contracts return raw GitHub-style payloads instead (owner.login, stargazers_count, repository.full_name, subject.title, ...). That means the starred list can render an object as owner or crash in search, and notifications/detail selection will be built from missing fields. Please add a normalization layer here or reuse the shared client contract before rendering.

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

In `@static/desktop/assets/GitHubApp-IYMAlDty.js` at line 1, The component assumes
flattened app shapes but the API returns raw GitHub payloads; normalize
responses before they reach render by transforming data in the fetch helpers
We(...) and Pe(...) (and the callers I() and _() that set state via be(...) and
me(...)). In We, map each repo from GitHub shape (owner.login, stargazers_count,
forks_count, description, language, updated_at, name, etc.) into the app shape
(owner, stars, forks, description, language, updated_at, name, etc.) before
returning; in Pe, map notification items (repository.full_name -> repo,
subject.title -> title, subject.url/number -> number if present,
updated_at/created_at -> created_at) into the app shape the UI expects; keep the
same return shape (repos:[],notifications:[]) and default values when fields are
missing so downstream code (I(), _(), g memo, rendering functions ve/Ne/etc.)
never sees raw GitHub keys.

@@ -0,0 +1 @@
import{r as l,j as t}from"./vendor-react-l6srOxy7.js";import{L as U,C as k,c as C,B as p}from"./toolbar-UW6q5pkx.js";import{ab as f,ak as B,y as M,an as O}from"./vendor-icons-wm645Jsx.js";import"./vendor-radix-BhM7AEEG.js";import"./vendor-layout-B-pp9n1f.js";const g=[".txt",".md",".pdf",".html",".json",".csv"],L=["text/plain","text/markdown","application/pdf","text/html","application/json","text/csv"];function R(c){return c<1024?`${c} B`:c<1024*1024?`${(c/1024).toFixed(1)} KB`:`${(c/(1024*1024)).toFixed(1)} MB`}function Y({windowId:c}){const[S,D]=l.useState([]),[r,E]=l.useState(""),[i,b]=l.useState([]),[A,x]=l.useState(!1),[h,j]=l.useState(!1),[u,v]=l.useState(0),[y,w]=l.useState(!1),[d,o]=l.useState(null),m=l.useRef(null);l.useEffect(()=>{(async()=>{try{const e=await fetch("/api/agents",{headers:{Accept:"application/json"}});if(e.ok&&(e.headers.get("content-type")??"").includes("application/json")){const a=await e.json();Array.isArray(a)&&a.length>0&&D(a.map(n=>String(n.name??"unknown")))}}catch{}})()},[]);const $=l.useCallback(e=>{var a;const s="."+((a=e.name.split(".").pop())==null?void 0:a.toLowerCase());return g.includes(s)||L.includes(e.type)},[]);function N(e){const a=e.filter($).map(n=>({id:`${n.name}-${Date.now()}-${Math.random().toString(36).slice(2,6)}`,file:n,name:n.name,size:n.size}));b(n=>[...n,...a]),o(null)}function z(e){e.preventDefault(),x(!1);const s=Array.from(e.dataTransfer.files);N(s)}function F(e){e.target.files&&N(Array.from(e.target.files)),e.target.value=""}function I(e){b(s=>s.filter(a=>a.id!==e))}async function T(){if(!r||i.length===0)return;j(!0),v(0),o(null);const e=i.length;let s=0;for(const a of i){const n=new FormData;n.append("file",a.file),n.append("agent",r);try{await fetch("/api/import/upload",{method:"POST",body:n})}catch{}s++,v(Math.round(s/e*100))}j(!1),o(`Uploaded ${e} file${e!==1?"s":""} for ${r}`)}async function P(){if(r){w(!0),o(null);try{(await fetch("/api/import/embed",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({agent:r})})).ok?o("Embedding complete. Memory updated."):o("Embedding request sent. Check agent memory.")}catch{o("Could not reach embed endpoint. API may not be available.")}w(!1)}}return t.jsxs("div",{className:"flex flex-col h-full bg-shell-bg text-shell-text select-none",children:[t.jsxs("div",{className:"flex items-center gap-2 px-4 py-3 border-b border-white/5",children:[t.jsx(f,{size:18,className:"text-accent"}),t.jsx("h1",{className:"text-sm font-semibold",children:"Import"})]}),t.jsxs("div",{className:"flex-1 overflow-auto p-4 space-y-4",children:[t.jsxs("div",{className:"space-y-1.5",children:[t.jsx(U,{htmlFor:"import-agent",children:"Target Agent"}),t.jsxs("select",{id:"import-agent",value:r,onChange:e=>E(e.target.value),className:"flex h-9 w-full max-w-sm rounded-lg border border-white/10 bg-shell-bg-deep px-3 py-1 text-sm text-shell-text focus-visible:outline-none focus-visible:border-accent/40 focus-visible:ring-2 focus-visible:ring-accent/20",children:[t.jsx("option",{value:"",children:"Select an agent..."}),S.map(e=>t.jsx("option",{value:e,children:e},e))]})]}),t.jsx(k,{onDragOver:e=>{e.preventDefault(),x(!0)},onDragLeave:()=>x(!1),onDrop:z,className:`border-2 border-dashed transition-colors cursor-pointer ${A?"border-accent bg-accent/5":"border-white/10 hover:border-white/20"}`,onClick:()=>{var e;return(e=m.current)==null?void 0:e.click()},role:"button","aria-label":"Drop files here or click to browse",tabIndex:0,onKeyDown:e=>{var s;(e.key==="Enter"||e.key===" ")&&(e.preventDefault(),(s=m.current)==null||s.click())},children:t.jsxs(C,{className:"flex flex-col items-center justify-center gap-3 p-8",children:[t.jsx(f,{size:32,className:"text-shell-text-tertiary"}),t.jsxs("div",{className:"text-center",children:[t.jsx("p",{className:"text-sm text-shell-text-secondary",children:"Drag and drop files here"}),t.jsx("p",{className:"text-xs text-shell-text-tertiary mt-1",children:g.join(", ")})]}),t.jsx(p,{variant:"secondary",size:"sm",onClick:e=>{var s;e.stopPropagation(),(s=m.current)==null||s.click()},children:"Browse"}),t.jsx("input",{ref:m,type:"file",multiple:!0,accept:g.join(","),onChange:F,className:"hidden","aria-label":"Select files to import"})]})}),i.length>0&&t.jsxs("div",{className:"space-y-1.5",children:[t.jsxs("h2",{className:"text-xs text-shell-text-secondary font-medium",children:["Queued Files (",i.length,")"]}),i.map(e=>t.jsx(k,{children:t.jsxs(C,{className:"flex items-center gap-3 px-3.5 py-2.5",children:[t.jsx(B,{size:14,className:"text-shell-text-tertiary shrink-0"}),t.jsx("span",{className:"text-sm flex-1 truncate",children:e.name}),t.jsx("span",{className:"text-xs text-shell-text-tertiary tabular-nums shrink-0",children:R(e.size)}),t.jsx(p,{variant:"ghost",size:"icon",onClick:()=>I(e.id),className:"h-7 w-7 hover:text-red-400 hover:bg-red-500/15","aria-label":`Remove ${e.name}`,children:t.jsx(M,{size:14})})]})},e.id))]}),h&&t.jsxs("div",{className:"space-y-1.5",children:[t.jsxs("div",{className:"flex items-center justify-between text-xs text-shell-text-secondary",children:[t.jsx("span",{children:"Uploading..."}),t.jsxs("span",{className:"tabular-nums",children:[u,"%"]})]}),t.jsx("div",{className:"h-2 w-full rounded-full bg-white/5",role:"progressbar","aria-valuenow":u,"aria-valuemin":0,"aria-valuemax":100,children:t.jsx("div",{className:"h-full rounded-full bg-accent transition-all",style:{width:`${u}%`}})})]}),d&&t.jsx("p",{className:`text-xs ${d.includes("complete")||d.includes("Uploaded")?"text-emerald-400":"text-amber-400"}`,children:d}),t.jsxs("div",{className:"flex gap-2",children:[t.jsxs(p,{onClick:T,disabled:!r||i.length===0||h,children:[t.jsx(f,{size:14}),h?"Uploading...":"Upload"]}),t.jsxs(p,{variant:"secondary",onClick:P,disabled:!r||y,className:"bg-violet-600 text-white hover:bg-violet-500",children:[t.jsx(O,{size:14}),y?"Embedding...":"Embed"]})]})]})]})}export{Y as ImportApp};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't report failed uploads as successful.

The upload loop ignores response.ok and swallows network errors, then always ends with Uploaded N files.... A 413/500 or transient failure will still advance progress and show success, which makes partial uploads indistinguishable from complete ones.

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

In `@static/desktop/assets/ImportApp-DBAV17Xb.js` at line 1, The upload loop in
function T currently ignores response.ok and swallows errors, then always
reports "Uploaded N files..."; update T to check each
fetch("/api/import/upload", ...) response.ok and treat non-ok or thrown errors
as failures (do not increment the success count or progress for failed uploads),
collect failed file names/IDs into a list, and after the loop set o(...) to a
success/failure message that reflects how many succeeded vs failed (e.g.,
"Uploaded X of Y files; failed: [...]") and set j(false) appropriately; ensure
the catch block for each file records the failure and does not advance the
success counter or show a misleading completed state.

@@ -0,0 +1 @@
import{r as l,j as e}from"./vendor-react-l6srOxy7.js";import{M as Me}from"./MobileSplitView-CtNEF6zb.js";import{u as Re}from"./use-is-mobile-v5lglusa.js";import{B as c,I as M,C as P,a as Oe,c as W,d as Ie,e as Ee,f as H,g as Y}from"./toolbar-UW6q5pkx.js";import{s as De,l as Fe,a as Te,b as $e,c as Be,g as ie,d as Ue,e as Pe,f as We,i as He,h as Ye,j as qe}from"./knowledge-ES9kK4zW.js";import{aE as Ve,h as Ke,A as Ge,r as Je,S as Xe,aU as Ze,$ as Qe,ay as et,a9 as tt,ac as st,y as ce,aM as at}from"./vendor-icons-wm645Jsx.js";import"./vendor-radix-BhM7AEEG.js";import"./vendor-layout-B-pp9n1f.js";const rt='a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';function oe(o){return o?Array.from(o.querySelectorAll(rt)):[]}function lt(o,a){const f=l.useRef(null);l.useEffect(()=>{if(!a||!o.current)return;f.current=document.activeElement;const m=oe(o.current);m.length>0&&m[0].focus();const b=d=>{if(d.key!=="Tab")return;const p=oe(o.current);if(p.length===0)return;const S=p[0],u=p[p.length-1];d.shiftKey&&document.activeElement===S?(d.preventDefault(),u.focus()):!d.shiftKey&&document.activeElement===u&&(d.preventDefault(),S.focus())},N=o.current;return N.addEventListener("keydown",b),()=>{var d;N.removeEventListener("keydown",b),(d=f.current)==null||d.focus()}},[o,a])}const de=["reddit","youtube","github","x","article","file","manual"],xe=["ready","processing","error"],C={reddit:"Reddit",youtube:"YouTube",github:"GitHub",x:"X",article:"Articles",file:"Files",manual:"Manual"},R=o=>{const a=Date.now()/1e3-o;return a<60?"just now":a<3600?`${Math.floor(a/60)}m ago`:a<86400?`${Math.floor(a/3600)}h ago`:a<604800?`${Math.floor(a/86400)}d ago`:new Date(o*1e3).toLocaleDateString()},q=o=>o==="ready"?"bg-green-500/15 text-green-400 border-green-500/30":o==="processing"?"bg-amber-500/15 text-amber-400 border-amber-500/30":o==="error"?"bg-red-500/15 text-red-400 border-red-500/30":"bg-white/10 text-shell-text-tertiary border-white/10";function ut({windowId:o}){const[a,f]=l.useState(null),[m,b]=l.useState([]),[N,d]=l.useState(!0),[p,S]=l.useState(""),[u,V]=l.useState("keyword"),[y,he]=l.useState("newest"),[K]=l.useState(0),[n,G]=l.useState({source_type:null,category:null,status:null,monitor:null}),[_,O]=l.useState([]),[pe,J]=l.useState(!1),[ue,z]=l.useState(!1),[X,I]=l.useState(!1),[me,A]=l.useState(!1),[E,fe]=l.useState([]),[Z,be]=l.useState([]),[Q,D]=l.useState(!1),ee=l.useRef(null);lt(ee,Q);const[te,F]=l.useState([]),[T,ge]=l.useState(!1),[x,v]=l.useState({pattern:"",match_on:"source_url",category:"",priority:10}),h=Re(),[se,ae]=l.useState(!1),re=l.useCallback(async()=>{d(!0);try{if(p.trim()){const t=await De(p.trim(),u);b(t.results)}else{const t={limit:50,offset:K};n.source_type&&(t.source_type=n.source_type),n.category&&(t.category=n.category),n.status&&(t.status=n.status);const s=await Fe(t);b(s.items)}}catch{b([])}d(!1)},[n,p,u,K]);l.useEffect(()=>{re()},[re]);const le=l.useCallback(async()=>{try{const t=await fetch("/api/agents",{headers:{Accept:"application/json"}});if(t.ok&&(t.headers.get("content-type")??"").includes("application/json")){const r=await t.json();Array.isArray(r)&&be(r.map(i=>({name:String(i.name??"unknown"),color:String(i.color??"#3b82f6")})))}}catch{}},[]);l.useEffect(()=>{le()},[le]);const j=l.useCallback(async()=>{const t=await Te();fe(t)},[]);l.useEffect(()=>{j()},[j]);const ye=l.useCallback(async()=>{const t=await $e();F(t)},[]),$=l.useCallback(async t=>{f(t),A(!1),I(!1),J(!0);try{const s=await Be(t.id);O(s)}catch{O([])}J(!1),z(!0);try{const s=await ie(t.id);s&&f(s)}catch{}z(!1)},[]),L=l.useCallback(()=>{f(null),O([]),A(!1)},[]),B=l.useCallback(t=>{const s=new Set(t.categories),r=new Set;for(const i of E)s.has(i.category)&&r.add(i.agent_name);return Array.from(r)},[E]),je=l.useCallback(async(t,s)=>{const r=t.categories.length>0?t.categories:["default"];for(const i of r)await Ue({agent_name:s,category:i,auto_ingest:!0});await j()},[j]),Ne=l.useCallback(async(t,s)=>{const r=t.categories.length>0?t.categories:["default"];for(const i of r)await Pe(s,i);await j()},[j]),ne=l.useMemo(()=>{const t=[...m];return y==="newest"?t.sort((s,r)=>r.created_at-s.created_at):y==="updated"?t.sort((s,r)=>r.updated_at-s.updated_at):y==="alpha"&&t.sort((s,r)=>s.title.localeCompare(r.title)),t},[m,y]),w=l.useMemo(()=>ne.filter(t=>{if(!n.monitor)return!0;const s=t.monitor.current_interval??0;return n.monitor==="recent"?t.monitor.last_poll!=null&&s>0:n.monitor==="active"?s>0&&s<2592e3:n.monitor==="slow"?s>=2592e3:!0}),[ne,n.monitor]),k=l.useMemo(()=>{const t={};for(const s of m)for(const r of s.categories)t[r]=(t[r]??0)+1;return t},[m]),ve=l.useCallback(async()=>{if(!a)return;await We(a.id)&&(b(s=>s.filter(r=>r.id!==a.id)),L())},[a,L]),we=l.useCallback(async()=>{if(!(a!=null&&a.source_url))return;await He(a.source_url,{title:a.title,categories:a.categories}),z(!0);const t=await ie(a.id);t&&f(t),z(!1)},[a]),ke=l.useCallback(async()=>{if(!x.pattern||!x.category)return;const t=await Ye(x);t!=null&&(F(s=>[...s,{id:t,...x}]),v({pattern:"",match_on:"source_url",category:"",priority:10}))},[x]),Ce=l.useCallback(async t=>{await qe(t),F(s=>s.filter(r=>r.id!==t))},[]),g=(t,s)=>{G(r=>({...r,[t]:r[t]===s?null:s}))},Se=e.jsxs("nav",{className:"w-52 shrink-0 border-r border-white/5 bg-shell-surface/30 flex flex-col overflow-hidden","aria-label":"Library filters",children:[!h&&e.jsxs("div",{className:"flex items-center gap-2 px-3 py-3 border-b border-white/5 shrink-0",children:[e.jsx(Ve,{size:15,className:"text-accent"}),e.jsx("h1",{className:"text-sm font-semibold",children:"Library"})]}),e.jsxs("div",{className:"flex-1 overflow-y-auto p-2 space-y-4",children:[e.jsxs("section",{children:[e.jsx("p",{className:"text-[10px] uppercase tracking-wider text-shell-text-tertiary px-2 mb-1.5",children:"Sources"}),e.jsx("div",{className:"space-y-0.5",children:de.map(t=>{const s=n.source_type===t;return e.jsx(c,{variant:s?"secondary":"ghost",size:"sm","aria-pressed":s,onClick:()=>g("source_type",t),className:"w-full justify-start text-xs h-7 px-2",children:C[t]??t},t)})})]}),e.jsxs("section",{children:[e.jsx("p",{className:"text-[10px] uppercase tracking-wider text-shell-text-tertiary px-2 mb-1.5",children:"Categories"}),e.jsxs("div",{className:"space-y-0.5",children:[Object.entries(k).map(([t,s])=>{const r=n.category===t;return e.jsxs(c,{variant:r?"secondary":"ghost",size:"sm","aria-pressed":r,onClick:()=>g("category",t),className:"w-full justify-between text-xs h-7 px-2",children:[e.jsx("span",{className:"truncate",children:t}),e.jsx("span",{className:"text-shell-text-tertiary tabular-nums ml-1",children:s})]},t)}),e.jsx(c,{variant:"ghost",size:"sm",className:"w-full justify-start text-xs h-7 px-2 text-accent",onClick:()=>{ye(),D(!0)},"aria-label":"Manage categories",children:"+ Manage"})]})]}),e.jsxs("section",{children:[e.jsx("p",{className:"text-[10px] uppercase tracking-wider text-shell-text-tertiary px-2 mb-1.5",children:"Status"}),e.jsx("div",{className:"space-y-0.5",children:xe.map(t=>{const s=n.status===t;return e.jsx(c,{variant:s?"secondary":"ghost",size:"sm","aria-pressed":s,onClick:()=>g("status",t),className:"w-full justify-start text-xs h-7 px-2 capitalize",children:t},t)})})]}),e.jsxs("section",{children:[e.jsx("p",{className:"text-[10px] uppercase tracking-wider text-shell-text-tertiary px-2 mb-1.5",children:"Monitoring"}),e.jsx("div",{className:"space-y-0.5",children:[{id:"recent",label:"Recent changes",icon:Ke},{id:"active",label:"Active polls",icon:Ge},{id:"slow",label:"Slow items",icon:Je}].map(({id:t,label:s,icon:r})=>{const i=n.monitor===t;return e.jsxs(c,{variant:i?"secondary":"ghost",size:"sm","aria-pressed":i,onClick:()=>g("monitor",t),className:"w-full justify-start text-xs h-7 px-2 gap-1.5",children:[e.jsx(r,{size:11}),s]},t)})})]})]})]}),U=n.source_type!=null||n.category!=null||n.status!=null||n.monitor!=null,_e=e.jsxs("main",{className:"flex-1 flex flex-col overflow-hidden",children:[e.jsxs("div",{className:"flex flex-col gap-2 px-4 py-3 border-b border-white/5 shrink-0",children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsxs("div",{className:"relative flex-1",children:[e.jsx(Xe,{size:14,className:"absolute left-3 top-1/2 -translate-y-1/2 text-shell-text-tertiary pointer-events-none z-10"}),e.jsx(M,{type:"text",value:p,onChange:t=>S(t.target.value),placeholder:"Search knowledge base...",className:"pl-8 h-8","aria-label":"Search knowledge base"})]}),h&&e.jsxs(c,{variant:U?"secondary":"outline",size:"sm","aria-pressed":se,onClick:()=>ae(t=>!t),className:"text-xs shrink-0","aria-label":"Toggle filters",children:["Filters",U?" •":""]}),!h&&e.jsx("div",{className:"flex items-center gap-1 shrink-0",role:"radiogroup","aria-label":"Search mode",children:["keyword","semantic"].map(t=>e.jsx(c,{variant:u===t?"secondary":"outline",size:"sm",role:"radio","aria-checked":u===t,onClick:()=>V(t),className:"capitalize text-xs",children:t},t))})]}),h&&e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"flex items-center gap-1",role:"radiogroup","aria-label":"Search mode",children:["keyword","semantic"].map(t=>e.jsx(c,{variant:u===t?"secondary":"outline",size:"sm",role:"radio","aria-checked":u===t,onClick:()=>V(t),className:"capitalize text-xs",children:t},t))}),se&&e.jsxs("div",{className:"border-t border-white/5 pt-2 space-y-3",children:[e.jsxs("div",{children:[e.jsx("p",{className:"text-[10px] uppercase tracking-wider text-shell-text-tertiary mb-1.5",children:"Sources"}),e.jsx("div",{className:"flex flex-wrap gap-1",children:de.map(t=>{const s=n.source_type===t;return e.jsx("button",{type:"button",onClick:()=>g("source_type",t),"aria-pressed":s,className:`text-xs px-2.5 py-1 rounded-full border transition-colors ${s?"bg-accent/20 border-accent/40 text-accent":"bg-white/5 border-white/10 text-shell-text-secondary"}`,children:C[t]??t},t)})})]}),Object.keys(k).length>0&&e.jsxs("div",{children:[e.jsx("p",{className:"text-[10px] uppercase tracking-wider text-shell-text-tertiary mb-1.5",children:"Categories"}),e.jsx("div",{className:"flex flex-wrap gap-1",children:Object.entries(k).map(([t])=>{const s=n.category===t;return e.jsx("button",{type:"button",onClick:()=>g("category",t),"aria-pressed":s,className:`text-xs px-2.5 py-1 rounded-full border transition-colors ${s?"bg-accent/20 border-accent/40 text-accent":"bg-white/5 border-white/10 text-shell-text-secondary"}`,children:t},t)})})]}),e.jsxs("div",{children:[e.jsx("p",{className:"text-[10px] uppercase tracking-wider text-shell-text-tertiary mb-1.5",children:"Status"}),e.jsx("div",{className:"flex flex-wrap gap-1",children:xe.map(t=>{const s=n.status===t;return e.jsx("button",{type:"button",onClick:()=>g("status",t),"aria-pressed":s,className:`text-xs px-2.5 py-1 rounded-full border capitalize transition-colors ${s?"bg-accent/20 border-accent/40 text-accent":"bg-white/5 border-white/10 text-shell-text-secondary"}`,children:t},t)})})]}),U&&e.jsx("button",{type:"button",onClick:()=>{G({source_type:null,category:null,status:null,monitor:null}),ae(!1)},className:"text-xs text-shell-text-tertiary underline",children:"Clear all filters"})]})]}),e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsxs("span",{className:"text-[11px] text-shell-text-tertiary",children:[w.length," item",w.length!==1?"s":""]}),e.jsx("div",{className:"flex items-center gap-1 ml-auto",role:"radiogroup","aria-label":"Sort order",children:[{id:"newest",label:"Newest"},{id:"updated",label:"Updated"},{id:"alpha",label:"A–Z"}].map(({id:t,label:s})=>e.jsx(c,{variant:y===t?"secondary":"ghost",size:"sm",role:"radio","aria-checked":y===t,onClick:()=>he(t),className:"text-xs h-6 px-2",children:s},t))})]})]}),e.jsx("div",{className:h?"flex-1 overflow-y-auto":"flex-1 overflow-y-auto p-3 space-y-2",children:N?e.jsx("div",{className:"flex items-center justify-center h-full text-shell-text-tertiary text-sm",children:"Loading library..."}):w.length===0?e.jsxs("div",{className:"flex flex-col items-center justify-center h-full gap-3 text-shell-text-tertiary",children:[e.jsx(Ze,{size:36,className:"opacity-30"}),e.jsx("p",{className:"text-sm",children:p?"No results for your search":"No items in library"})]}):h?e.jsx("div",{style:{padding:"8px 0 16px"},children: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:w.map((t,s,r)=>{const i=B(t);return e.jsxs("button",{type:"button",onClick:()=>$(t),"aria-label":`Open ${t.title}`,style:{display:"flex",alignItems:"center",gap:10,width:"100%",padding:"14px 16px",background:"none",border:"none",borderBottom:s===r.length-1?"none":"1px solid rgba(255,255,255,0.06)",cursor:"pointer",color:"inherit",textAlign:"left"},children:[e.jsxs("div",{style:{flex:1,minWidth:0},children:[e.jsx("div",{style:{fontSize:15,fontWeight:600,color:"rgba(255,255,255,0.95)",marginBottom:3,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:t.title||"Untitled"}),e.jsx("div",{style:{fontSize:12,color:"rgba(255,255,255,0.45)",marginBottom:i.length>0?4:0},children:[t.author,C[t.source_type]??t.source_type,R(t.created_at)].filter(Boolean).join(" · ")}),i.length>0&&e.jsxs("div",{style:{fontSize:11,color:"rgba(255,255,255,0.35)"},children:["Shared: ",i.join(", ")]})]}),e.jsx("span",{className:`shrink-0 text-[10px] px-1.5 py-0.5 rounded border mr-2 ${q(t.status)}`,children:t.status}),e.jsx("svg",{width:"8",height:"14",viewBox:"0 0 8 14",fill:"none",style:{color:"rgba(255,255,255,0.3)",flexShrink:0},children:e.jsx("path",{d:"M1 1L7 7L1 13",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round"})})]},t.id)})})}):w.map(t=>{const s=B(t);return e.jsxs(P,{className:"cursor-pointer hover:border-white/15 transition-colors",onClick:()=>$(t),onKeyDown:r=>{(r.key==="Enter"||r.key===" ")&&(r.preventDefault(),$(t))},tabIndex:0,role:"button","aria-label":`Open ${t.title}`,children:[e.jsxs(Oe,{className:"pb-1 p-3",children:[e.jsxs("div",{className:"flex items-start justify-between gap-2",children:[e.jsx("h3",{className:"text-sm font-medium leading-snug line-clamp-1",children:t.title||"Untitled"}),e.jsx("span",{className:`shrink-0 text-[10px] px-1.5 py-0.5 rounded border ${q(t.status)}`,children:t.status})]}),e.jsx("p",{className:"text-[11px] text-shell-text-tertiary",children:[t.author,C[t.source_type]??t.source_type,R(t.created_at)].filter(Boolean).join(" · ")})]}),e.jsxs(W,{className:"pt-0 px-3 pb-3 space-y-2",children:[t.summary&&e.jsx("p",{className:"text-xs text-shell-text-secondary line-clamp-2 leading-relaxed",children:t.summary}),t.categories.length>0&&e.jsx("div",{className:"flex flex-wrap gap-1",children:t.categories.map(r=>e.jsx("span",{className:"px-1.5 py-0.5 rounded bg-accent/10 text-accent text-[10px] border border-accent/20",children:r},r))}),s.length>0&&e.jsxs("p",{className:"text-[10px] text-shell-text-tertiary",children:["Shared with: ",s.join(", ")]})]})]},t.id)})})]}),ze=a?e.jsxs("main",{className:"flex-1 flex flex-col overflow-hidden",children:[e.jsxs("div",{className:"flex-1 overflow-y-auto",children:[e.jsxs("div",{className:"px-5 pt-4 pb-3 border-b border-white/5",children:[!h&&e.jsxs(c,{variant:"ghost",size:"sm",onClick:L,className:"text-xs mb-3 -ml-1 text-shell-text-secondary","aria-label":"Back to library",children:[e.jsx(Qe,{size:14}),"Back to library"]}),e.jsx("h2",{className:"text-lg font-semibold leading-snug mb-1",children:ue?"Loading...":a.title||"Untitled"}),e.jsx("p",{className:"text-xs text-shell-text-tertiary mb-2",children:[a.author,C[a.source_type]??a.source_type,R(a.created_at)].filter(Boolean).join(" · ")}),e.jsxs("div",{className:"flex flex-wrap items-center gap-1.5 mb-3",children:[a.categories.map(t=>e.jsx("span",{className:"px-2 py-0.5 rounded-full bg-accent/10 text-accent text-[11px] border border-accent/20",children:t},t)),e.jsx("span",{className:`px-2 py-0.5 rounded-full text-[11px] border ${q(a.status)}`,children:a.status}),(a.monitor.current_interval??0)>0&&e.jsx("span",{className:"px-2 py-0.5 rounded-full bg-blue-500/10 text-blue-400 text-[11px] border border-blue-500/20",children:"monitoring"})]}),(()=>{const t=B(a),s=Z.filter(r=>!t.includes(r.name));return e.jsxs("div",{className:"flex items-center flex-wrap gap-1 text-xs text-shell-text-secondary relative",children:[e.jsx("span",{children:"Shared with:"}),t.length===0?e.jsx("span",{className:"text-shell-text-tertiary italic",children:"no agents"}):t.map(r=>{const i=Z.find(Le=>Le.name===r);return e.jsxs("button",{onClick:()=>Ne(a,r),className:"flex items-center gap-1 px-1.5 py-0.5 rounded bg-white/5 border border-white/10 hover:border-red-500/40 hover:bg-red-500/10 transition-colors text-[11px]",title:`Remove ${r}`,"aria-label":`Remove ${r} from shared agents`,children:[i&&e.jsx("span",{className:"w-2 h-2 rounded-full",style:{backgroundColor:i.color},"aria-hidden":"true"}),r]},r)}),e.jsxs("div",{className:"relative",children:[e.jsx(c,{variant:"ghost",size:"sm",className:"h-6 px-2 text-[11px] text-accent",onClick:()=>I(r=>!r),"aria-label":"Add agent","aria-expanded":X,"aria-haspopup":"listbox",children:"+ add agent"}),X&&s.length>0&&e.jsx("div",{className:"absolute top-full left-0 mt-1 z-50 bg-shell-surface border border-white/10 rounded-lg shadow-lg py-1 min-w-[140px]",role:"listbox","aria-label":"Select agent to add",children:s.map(r=>e.jsxs("button",{className:"w-full flex items-center gap-2 px-3 py-1.5 text-xs hover:bg-white/5 text-left",role:"option","aria-selected":!1,onClick:()=>{je(a,r.name),I(!1)},children:[e.jsx("span",{className:"w-2 h-2 rounded-full shrink-0",style:{backgroundColor:r.color},"aria-hidden":"true"}),r.name]},r.name))})]})]})})()]}),a.summary&&e.jsx("div",{className:"px-5 py-3 border-b border-white/5",children:e.jsx(P,{className:"bg-white/[0.02]",children:e.jsx(W,{className:"px-4 py-3",children:e.jsx("p",{className:"text-xs text-shell-text-secondary leading-relaxed",children:a.summary})})})}),e.jsx("div",{className:"px-5 py-3 flex-1",children:e.jsxs(Ie,{defaultValue:"content",children:[e.jsxs(Ee,{children:[e.jsx(H,{value:"content",children:"Content"}),e.jsxs(H,{value:"history",children:["History",_.length>0?` (${_.length})`:""]}),e.jsx(H,{value:"metadata",children:"Metadata"})]}),e.jsx(Y,{value:"content",children:e.jsx("div",{className:"max-h-[320px] overflow-y-auto rounded-lg bg-white/[0.02] border border-white/5 p-3",children:a.content?e.jsx("pre",{className:"text-xs text-shell-text-secondary whitespace-pre-wrap leading-relaxed font-sans",children:a.content}):e.jsx("p",{className:"text-xs text-shell-text-tertiary italic",children:"No content available"})})}),e.jsx(Y,{value:"history",children:e.jsx("div",{className:"space-y-2 max-h-[320px] overflow-y-auto",children:pe?e.jsx("p",{className:"text-xs text-shell-text-tertiary py-4 text-center",children:"Loading history..."}):_.length===0?e.jsx("p",{className:"text-xs text-shell-text-tertiary py-4 text-center italic",children:"No snapshots recorded yet"}):_.map(t=>e.jsx(P,{className:"bg-white/[0.02]",children:e.jsxs(W,{className:"px-3 py-2.5 space-y-1.5",children:[e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsx("span",{className:"text-[11px] font-medium text-shell-text-secondary",children:new Date(t.snapshot_at*1e3).toLocaleString()}),e.jsxs("span",{className:"text-[10px] text-shell-text-tertiary font-mono",children:["#",t.content_hash.slice(0,7)]})]}),t.diff_json&&Object.keys(t.diff_json).length>0&&e.jsx("div",{className:"flex flex-wrap gap-1",children:Object.entries(t.diff_json).map(([s,r])=>e.jsxs("span",{className:"px-1.5 py-0.5 rounded bg-amber-500/10 text-amber-400 text-[10px] border border-amber-500/20",children:[s,": ",String(r)]},s))})]})},t.id))})}),e.jsx(Y,{value:"metadata",children:e.jsxs("div",{className:"max-h-[320px] overflow-y-auto space-y-3",children:[Object.keys(a.metadata).length>0&&e.jsx("table",{className:"w-full text-xs",children:e.jsx("tbody",{children:Object.entries(a.metadata).map(([t,s])=>e.jsxs("tr",{className:"border-b border-white/5",children:[e.jsx("td",{className:"py-1.5 pr-3 text-shell-text-tertiary font-medium w-40 align-top",children:t}),e.jsx("td",{className:"py-1.5 text-shell-text-secondary break-all",children:typeof s=="object"?JSON.stringify(s):String(s)})]},t))})}),e.jsxs("div",{children:[e.jsx("p",{className:"text-[10px] uppercase tracking-wider text-shell-text-tertiary mb-2",children:"Monitor Config"}),e.jsx("table",{className:"w-full text-xs",children:e.jsx("tbody",{children:[["Current interval",a.monitor.current_interval!=null?`${a.monitor.current_interval}s`:"—"],["Frequency",a.monitor.frequency!=null?`${a.monitor.frequency}s`:"—"],["Decay rate",a.monitor.decay_rate??"—"],["Pinned",a.monitor.pinned?"Yes":"No"],["Last polled",a.monitor.last_poll!=null?R(a.monitor.last_poll):"Never"]].map(([t,s])=>e.jsxs("tr",{className:"border-b border-white/5",children:[e.jsx("td",{className:"py-1.5 pr-3 text-shell-text-tertiary w-40",children:t}),e.jsx("td",{className:"py-1.5 text-shell-text-secondary",children:s})]},t))})})]})]})})]})})]}),e.jsxs("div",{className:"border-t border-white/5 px-5 py-2.5 flex items-center gap-2 shrink-0",children:[a.source_url&&e.jsxs(c,{variant:"outline",size:"sm",className:"text-xs gap-1.5",onClick:()=>window.open(a.source_url,"_blank"),"aria-label":"Open source URL",children:[e.jsx(et,{size:12}),"Open source"]}),e.jsxs(c,{variant:"outline",size:"sm",className:"text-xs gap-1.5",onClick:we,"aria-label":"Re-ingest this item",children:[e.jsx(tt,{size:12}),"Re-ingest"]}),a.media_path&&e.jsxs(c,{variant:"outline",size:"sm",className:"text-xs gap-1.5",onClick:()=>{if(a.media_path){const t=document.createElement("a");t.href=a.media_path,t.download=a.title.replace(/\s+/g,"-"),t.click()}},"aria-label":"Download media",children:[e.jsx(st,{size:12}),"Download media"]}),e.jsx("div",{className:"ml-auto",children:me?e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("span",{className:"text-xs text-red-400",children:"Confirm delete?"}),e.jsx(c,{variant:"outline",size:"sm",className:"text-xs border-red-500/40 text-red-400 hover:bg-red-500/15",onClick:ve,"aria-label":"Confirm delete item",children:"Yes, delete"}),e.jsx(c,{variant:"ghost",size:"sm",className:"text-xs",onClick:()=>A(!1),"aria-label":"Cancel delete",children:"Cancel"})]}):e.jsxs(c,{variant:"ghost",size:"sm",className:"text-xs gap-1.5 hover:text-red-400 hover:bg-red-500/15",onClick:()=>A(!0),"aria-label":"Delete this item",children:[e.jsx(ce,{size:12}),"Delete"]})})]})]}):null,Ae=Q?e.jsx("div",{className:h?"fixed inset-0 z-50 flex items-end bg-black/50 backdrop-blur-sm":"fixed inset-0 z-50 flex items-center justify-center bg-black/50",role:"dialog","aria-modal":"true","aria-label":"Category manager",onClick:t=>{t.target===t.currentTarget&&D(!1)},children:e.jsxs("div",{ref:ee,className:"bg-shell-surface border border-white/10 shadow-2xl flex flex-col",style:h?{borderRadius:"20px 20px 0 0",width:"100%",maxHeight:"92%",overflowY:"auto"}:{borderRadius:12,width:560,maxWidth:"90vw",maxHeight:"80vh"},children:[e.jsxs("div",{className:"flex items-center justify-between px-5 py-4 border-b border-white/5",children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(at,{size:15,className:"text-accent"}),e.jsx("h2",{className:"text-sm font-semibold",children:"Category Manager"})]}),e.jsx(c,{variant:"ghost",size:"sm",onClick:()=>D(!1),"aria-label":"Close category manager",className:"text-xs",children:"Close"})]}),e.jsxs("div",{className:"flex-1 overflow-y-auto p-5 space-y-4",children:[e.jsxs("section",{children:[e.jsx("p",{className:"text-[10px] uppercase tracking-wider text-shell-text-tertiary mb-2",children:"Categories"}),Object.keys(k).length===0?e.jsx("p",{className:"text-xs text-shell-text-tertiary italic",children:"No categories yet"}):e.jsx("div",{className:"space-y-1",children:Object.entries(k).map(([t,s])=>{const r=E.filter(i=>i.category===t).map(i=>i.agent_name);return e.jsx("div",{className:"flex items-center justify-between px-3 py-2 rounded-lg bg-white/[0.03] border border-white/5",children:e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("span",{className:"text-xs font-medium",children:t}),e.jsxs("span",{className:"text-[10px] text-shell-text-tertiary",children:[s," item",s!==1?"s":""]}),r.length>0&&e.jsxs("span",{className:"text-[10px] text-shell-text-tertiary",children:["· ",r.join(", ")]})]})},t)})})]}),e.jsxs("section",{children:[e.jsxs("button",{className:"flex items-center gap-2 text-[10px] uppercase tracking-wider text-shell-text-tertiary hover:text-shell-text transition-colors w-full text-left",onClick:()=>ge(t=>!t),"aria-expanded":T,children:[e.jsx("span",{children:T?"▾":"▸"}),"Advanced: Rules"]}),T&&e.jsxs("div",{className:"mt-3 space-y-3",children:[te.length>0?e.jsxs("table",{className:"w-full text-xs",children:[e.jsx("thead",{children:e.jsxs("tr",{className:"text-shell-text-tertiary",children:[e.jsx("th",{className:"text-left pb-1.5 font-normal",children:"Pattern"}),e.jsx("th",{className:"text-left pb-1.5 font-normal",children:"Match on"}),e.jsx("th",{className:"text-left pb-1.5 font-normal",children:"Category"}),e.jsx("th",{className:"text-left pb-1.5 font-normal w-8",children:"Pri"}),e.jsx("th",{className:"pb-1.5 w-8"})]})}),e.jsx("tbody",{children:te.map(t=>e.jsxs("tr",{className:"border-t border-white/5",children:[e.jsx("td",{className:"py-1.5 pr-2 font-mono text-[11px]",children:t.pattern}),e.jsx("td",{className:"py-1.5 pr-2 text-shell-text-secondary",children:t.match_on}),e.jsx("td",{className:"py-1.5 pr-2 text-shell-text-secondary",children:t.category}),e.jsx("td",{className:"py-1.5 pr-2 text-shell-text-tertiary",children:t.priority}),e.jsx("td",{className:"py-1.5",children:e.jsx(c,{variant:"ghost",size:"icon",className:"h-6 w-6 hover:text-red-400 hover:bg-red-500/15",onClick:()=>Ce(t.id),"aria-label":`Delete rule for ${t.pattern}`,children:e.jsx(ce,{size:11})})})]},t.id))})]}):e.jsx("p",{className:"text-xs text-shell-text-tertiary italic",children:"No rules yet"}),e.jsxs("div",{className:"border-t border-white/5 pt-3 space-y-2",children:[e.jsx("p",{className:"text-[10px] text-shell-text-tertiary uppercase tracking-wider",children:"Add rule"}),e.jsxs("div",{className:"grid grid-cols-2 gap-2",children:[e.jsxs("div",{className:"space-y-1",children:[e.jsx("label",{className:"text-[10px] text-shell-text-tertiary",htmlFor:"rule-pattern",children:"Pattern (glob)"}),e.jsx(M,{id:"rule-pattern",value:x.pattern,onChange:t=>v(s=>({...s,pattern:t.target.value})),placeholder:"*.reddit.com/*",className:"h-7 text-xs"})]}),e.jsxs("div",{className:"space-y-1",children:[e.jsx("label",{className:"text-[10px] text-shell-text-tertiary",htmlFor:"rule-match-on",children:"Match on"}),e.jsx("select",{id:"rule-match-on",value:x.match_on,onChange:t=>v(s=>({...s,match_on:t.target.value})),className:"flex h-7 w-full rounded-lg border border-white/10 bg-shell-bg-deep px-2 text-xs text-shell-text focus-visible:outline-none focus-visible:border-accent/40",children:["source_url","source_type","author","title"].map(t=>e.jsx("option",{value:t,children:t},t))})]}),e.jsxs("div",{className:"space-y-1",children:[e.jsx("label",{className:"text-[10px] text-shell-text-tertiary",htmlFor:"rule-category",children:"Category"}),e.jsx(M,{id:"rule-category",value:x.category,onChange:t=>v(s=>({...s,category:t.target.value})),placeholder:"AI/ML",className:"h-7 text-xs"})]}),e.jsxs("div",{className:"space-y-1",children:[e.jsx("label",{className:"text-[10px] text-shell-text-tertiary",htmlFor:"rule-priority",children:"Priority"}),e.jsx(M,{id:"rule-priority",type:"number",value:x.priority,onChange:t=>v(s=>({...s,priority:parseInt(t.target.value)||10})),className:"h-7 text-xs"})]})]}),e.jsx(c,{size:"sm",className:"text-xs",onClick:ke,disabled:!x.pattern||!x.category,"aria-label":"Add rule",children:"Add rule"})]})]})]})]})]})}):null;return e.jsxs("div",{className:"flex flex-col h-full min-h-0 overflow-hidden bg-shell-bg text-shell-text select-none relative",children:[e.jsx(Me,{selectedId:(a==null?void 0:a.id)??null,onBack:L,listTitle:"Library",detailTitle:a?a.title||"Untitled":void 0,listWidth:700,list:e.jsxs("div",{className:"flex h-full min-h-0 overflow-hidden",children:[!h&&Se,_e]}),detail:ze??(h?null:e.jsx("div",{className:"flex items-center justify-center h-full text-shell-text-tertiary text-sm",children:N?"Loading...":m.length===0?"Add items to get started":"Select an item"}))}),Ae]})}export{ut as LibraryApp};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard optional metadata before calling Object.keys.

desktop/src/lib/knowledge.ts marks KnowledgeItem.metadata as optional, but this detail view renders Object.keys(a.metadata) unconditionally. Any item without metadata will crash the detail pane with Cannot convert undefined or null to object before the Metadata tab is opened. Please default this to {} first and render the table from that safe value instead.

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

In `@static/desktop/assets/LibraryApp-Cdo_EHou.js` at line 1, The detail view
reads Object.keys(a.metadata) without guarding for a.metadata being undefined;
change the code in the LibraryApp component (inside function ut, where a is the
selected item and metadata is referenced) to default a.metadata to an empty
object (e.g., const metadata = a?.metadata ?? {}) and use that safe metadata
variable for checks and rendering (replace Object.keys(a.metadata) and any
direct a.metadata access with Object.keys(metadata) and metadata) so the
Metadata tab won't crash when metadata is absent.

@@ -1,2 +1,2 @@
import{r as l,j as e}from"./vendor-react-l6srOxy7.js";import{B as k,C as T,I as E,T as Q,L as R,S as ee}from"./toolbar-UW6q5pkx.js";import{M as te}from"./MobileSplitView-qc4KfHBU.js";import{a as se,b as ae,g as le}from"./main-BXOeBesV.js";import{ap as I,Y as $,am as ne,a6 as ie,aO as re,R as ce,y as M,g as O,aH as oe,X as U,f as D,ax as de,r as xe}from"./vendor-icons-DcMSPw1y.js";import"./vendor-radix-BhM7AEEG.js";import"./vendor-layout-B-pp9n1f.js";import"./tokens-DIiUixLu.js";import"./vendor-codemirror-Byxbuxf1.js";const me={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"},he={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[h,r]=l.useState(""),b=n.length>=3,u=!b||h===t.id,g=l.useRef(null);l.useEffect(()=>{var m;(m=g.current)==null||m.focus()},[]);const f=n.map(m=>m.scope_kind==="all"?"all agents":m.scope_kind==="agent"?`agent: ${m.scope_id}`:`group: ${m.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:h,onChange:m=>r(m.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:h}){const[r,b]=l.useState("all"),[u,g]=l.useState(""),[f,m]=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 H(a){y(w=>{const C=new Set(w);return C.has(a)?C.delete(a):C.add(a),C})}function K(){s(a=>[...a,""])}function V(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 Y(){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 q=r==="all"?"all agents":r==="agent"?f?`${f}`:"the selected agent":f?`group ${f}`:"the selected group",X=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:h,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),m(""),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:()=>m(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:()=>H(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:K,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=>V(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:q})," will be able to call:"," ",e.jsx("span",{className:"font-medium",children:X}),".",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:h,disabled:x,children:"Cancel"}),e.jsxs(k,{size:"sm",onClick:Y,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] ${me[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 ${he[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 h=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=h[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 h=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:h})]}),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),[h,r]=l.useState([]),[b,u]=l.useState([]),[g,f]=l.useState([]),[m,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(m===s.id?null:s.id),className:"text-shell-text-secondary hover:text-shell-text transition-colors","aria-label":m===s.id?"Collapse details":"Expand details","aria-expanded":m===s.id,children:e.jsx(oe,{size:14,className:`transition-transform ${m===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})})]}),m===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:h,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),[h,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 m(){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:m,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:h,className:"self-start","aria-label":"Save environment variables",children:[g?e.jsx(D,{size:13,className:"mr-1 text-emerald-400"}):h?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),[h,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 m=!0;try{JSON.parse(n)}catch{m=!1}async function N(){if(m){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 ${m?"":"border-red-500/50"}`,"aria-label":"Server configuration JSON","aria-invalid":!m,spellCheck:!1}),!m&&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:!m||h,className:"self-start shrink-0","aria-label":"Save configuration",children:[g?e.jsx(D,{size:13,className:"mr-1 text-emerald-400"}):h?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),[h,r]=l.useState(!1),[b,u]=l.useState(!1),g=l.useRef(null),f=l.useRef(!1),m=l.useRef(null);f.current=h,l.useEffect(()=>{const d=new EventSource(`/api/mcp/servers/${encodeURIComponent(t)}/logs/stream`);return m.current=d,d.onopen=()=>p(!0),d.onerror=()=>p(!1),d.onmessage=y=>{f.current||o(S=>[...S.slice(-500),y.data])},()=>{d.close(),m.current=null}},[t]),l.useEffect(()=>{!h&&g.current&&(g.current.scrollTop=g.current.scrollHeight)},[n,h]);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"}),h&&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(h=>h.json()).then(h=>o(Array.isArray(h)?h:[])).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"),[h,r]=l.useState([]),[b,u]=l.useState([]),[g,f]=l.useState(null),[m,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:h,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})]}),m&&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),[h,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===h)??null;function f(){const j=le("store");j&&b("store",j.defaultSize)}const m=e.jsx(be,{servers:n,loading:i,selectedId:h,onSelect:r,onOpenStore:f}),N=g?e.jsx(ke,{server:g,onRefreshList:u,onDeselect:()=>r(null)}):null;return e.jsx(te,{list:m,detail:N,selectedId:h,onBack:()=>r(null),listTitle:"MCP",detailTitle:(g==null?void 0:g.name)??""})}export{Ie as MCPApp};
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-Bs5bQgxi.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-B9Rl30P8.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(`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Critical mismatch: PR objectives describe chat features, but this file contains MCP server management code.

The PR is titled "Chat Phase 2b-2a — per-message affordances + pinning" and describes implementing message editing, pinning, deep-linking, and mark-unread features. However, this file (MCPApp-JMsboybS.js) contains only MCP (Model Context Protocol) server management functionality—server lists, permissions, attachments, environment configuration, and logs. There is no chat-related code visible in this bundle.

This suggests either:

  • The actual chat feature source files are missing from this review
  • This build artifact was included incorrectly
  • The PR description does not match the changes

Please confirm that the correct files are included in this PR.


⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the source files that likely implement the chat features described in PR objectives

# Search for source files related to message editing, pinning, and affordances
fd -e ts -e tsx -e js -e jsx . | rg -i "(message|chat|pin|edit|delete|afford)" | head -20

# Look for component files in desktop/src that might contain the new UI
fd . desktop/src -e tsx -e ts | rg -i "(message|pin|overflow|menu)" | head -20

Repository: jaylfc/tinyagentos

Length of output: 1844


Review the source TypeScript/TSX files instead of this minified build artifact.

The file MCPApp-JMsboybS.js is a production minified bundle that cannot be meaningfully reviewed. Suitable source files exist in desktop/src/apps/chat/ (MessageEditor.tsx, MessageHoverActions.tsx, MessageOverflowMenu.tsx, PinBadge.tsx, PinRequestAffordance.tsx, PinnedMessagesPopover.tsx, etc.) and should be reviewed instead of build artifacts.

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

In `@static/desktop/assets/MCPApp-JMsboybS.js` at line 1, Summary: The PR
currently includes a minified production bundle (MCPApp-JMsboybS.js) which is
not reviewable; instead the original TSX source files under
desktop/src/apps/chat/ should be reviewed. Fix: remove the minified bundle from
the review (or exclude it from the commit/PR) and update the PR to include or
point reviewers to the corresponding source files (e.g., MessageEditor.tsx,
MessageHoverActions.tsx, MessageOverflowMenu.tsx, PinBadge.tsx,
PinRequestAffordance.tsx, PinnedMessagesPopover.tsx) so reviewers can inspect
functions/components (e.g., fe, je, ge, be, ve) in their original TypeScript/TSX
form; alternatively add a note in the PR description clarifying which source
files to review and why the bundle can be ignored.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant