Chat Phase 2b-2a — per-message affordances + pinning#237
Chat Phase 2b-2a — per-message affordances + pinning#237
Conversation
… + 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.
…licy + thread context
…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
… badge, deep-link scroll into MessagesApp
Code Review SummaryStatus: 10 Issues Found | Recommendation: Address before merge Fix these issues in Kilo Cloud Overview
Issue Details (click to expand)CRITICAL
WARNING
SUGGESTION
Files Reviewed (16 files)
Reviewed by seed-2-0-pro-260328 · 171,325 tokens |
📝 WalkthroughWalkthroughImplement 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
Sequence DiagramssequenceDiagram
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
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
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
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly Related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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 | 🟡 MinorUse 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 | 🟡 MinorDon'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 | 🟡 MinorUpdate documentation to match the actual test implementation.
The example in the documentation uses JavaScript regex literal syntax (
/pattern/i), but the actual test file attests/e2e/test_chat_phase2b1.pyline 52 correctly uses Python'sre.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 | 🟡 MinorSet 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 | 🟡 MinorSet an explicit button type to avoid unintended form submits.
At Line 10, this
<button>defaults totype="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 | 🟡 MinorExplicitly 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 | 🟡 MinorOrder 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 | 🟡 MinorReset navigation state when
rootchanges.
currentPathandselectedsurvive arootchange, so if I browse intofoo/barfor 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
useEffectthat clearscurrentPathandselectedonrootchanges 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 | 🟡 MinorAlign the schema section with the
chat_pinsdesign.This spec says pin state lives on
chat_messagesviapinned_at/pinned_by, but the plan and backend changes use the separatechat_pinstable 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 foronOpen.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 theonCloseaction path.
onCloseis 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 callonCancel.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/jsonwill 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
📒 Files selected for processing (128)
desktop/src/apps/MessagesApp.tsxdesktop/src/apps/chat/AttachmentGallery.tsxdesktop/src/apps/chat/AttachmentLightbox.tsxdesktop/src/apps/chat/AttachmentsBar.tsxdesktop/src/apps/chat/MessageEditor.tsxdesktop/src/apps/chat/MessageHoverActions.tsxdesktop/src/apps/chat/MessageOverflowMenu.tsxdesktop/src/apps/chat/MessageTombstone.tsxdesktop/src/apps/chat/PinBadge.tsxdesktop/src/apps/chat/PinRequestAffordance.tsxdesktop/src/apps/chat/PinnedMessagesPopover.tsxdesktop/src/apps/chat/ThreadIndicator.tsxdesktop/src/apps/chat/ThreadPanel.tsxdesktop/src/apps/chat/__tests__/AttachmentGallery.test.tsxdesktop/src/apps/chat/__tests__/AttachmentsBar.test.tsxdesktop/src/apps/chat/__tests__/MessageEditor.test.tsxdesktop/src/apps/chat/__tests__/MessageHoverActions.test.tsxdesktop/src/apps/chat/__tests__/MessageOverflowMenu.test.tsxdesktop/src/apps/chat/__tests__/MessageTombstone.test.tsxdesktop/src/apps/chat/__tests__/PinBadge.test.tsxdesktop/src/apps/chat/__tests__/PinRequestAffordance.test.tsxdesktop/src/apps/chat/__tests__/PinnedMessagesPopover.test.tsxdesktop/src/apps/chat/__tests__/ThreadIndicator.test.tsxdesktop/src/lib/__tests__/chat-attachments-api.test.tsdesktop/src/lib/__tests__/chat-messages-api.test.tsdesktop/src/lib/chat-attachments-api.tsdesktop/src/lib/chat-messages-api.tsdesktop/src/lib/use-thread-panel.tsdesktop/src/shell/FilePicker.tsxdesktop/src/shell/VfsBrowser.tsxdesktop/src/shell/__tests__/FilePicker.test.tsxdesktop/src/shell/__tests__/VfsBrowser.test.tsxdesktop/src/shell/file-picker-api.tsdesktop/src/theme/tokens.cssdesktop/tsconfig.tsbuildinfodocs/chat-guide.mddocs/superpowers/plans/2026-04-19-chat-phase-2b-1-threads-attachments.mddocs/superpowers/plans/2026-04-19-chat-phase-2b-2a-per-msg.mddocs/superpowers/specs/2026-04-19-chat-phase-2b-2a-per-msg-design.mdstatic/desktop/assets/ActivityApp-CG-PW6E_.jsstatic/desktop/assets/AgentBrowsersApp-CFn8GY-5.jsstatic/desktop/assets/AgentBrowsersApp-wWjBRYht.jsstatic/desktop/assets/AgentsApp-1_BLyIy2.jsstatic/desktop/assets/BrowserApp-FjtUA0FW.jsstatic/desktop/assets/CalendarApp-BJnvuKGY.jsstatic/desktop/assets/CalendarApp-DSaV9uPb.jsstatic/desktop/assets/ChannelsApp-BMXzpUI6.jsstatic/desktop/assets/ClusterApp-DzgzEDRn.jsstatic/desktop/assets/ContactsApp-CmwPWf7s.jsstatic/desktop/assets/FilesApp-Bm-rxwrE.jsstatic/desktop/assets/GitHubApp-CJvVZ0RH.jsstatic/desktop/assets/GitHubApp-IYMAlDty.jsstatic/desktop/assets/ImageViewerApp-D7vhXACc.jsstatic/desktop/assets/ImagesApp-DfCeUrhn.jsstatic/desktop/assets/ImportApp-AV3jmR5U.jsstatic/desktop/assets/ImportApp-DBAV17Xb.jsstatic/desktop/assets/LibraryApp-Cdo_EHou.jsstatic/desktop/assets/LibraryApp-NzJAyw3P.jsstatic/desktop/assets/MCPApp-JMsboybS.jsstatic/desktop/assets/MemoryApp-eDECkdBk.jsstatic/desktop/assets/MessagesApp-BsK4ioIn.jsstatic/desktop/assets/MessagesApp-DJJbqaHc.jsstatic/desktop/assets/MobileSplitView-CtNEF6zb.jsstatic/desktop/assets/MobileSplitView-qc4KfHBU.jsstatic/desktop/assets/ModelsApp-COpOwo4V.jsstatic/desktop/assets/ProvidersApp-C0T_x8YG.jsstatic/desktop/assets/RedditApp-BOuG46mh.jsstatic/desktop/assets/RedditApp-CkwARPpU.jsstatic/desktop/assets/SecretsApp-C1umTVfg.jsstatic/desktop/assets/SettingsApp-BmLIU-FB.jsstatic/desktop/assets/StoreApp-CNUGjBHW.jsstatic/desktop/assets/TasksApp-BLKBbvXY.jsstatic/desktop/assets/TextEditorApp-US6Eef1_.jsstatic/desktop/assets/XApp-E7cm6999.jsstatic/desktop/assets/YouTubeApp-Bv-vMHrm.jsstatic/desktop/assets/YouTubeApp-DPW-GRB6.jsstatic/desktop/assets/chat-DWaQ_wPY.jsstatic/desktop/assets/index-0OnUwbQt.jsstatic/desktop/assets/index-5RjMGAa1.jsstatic/desktop/assets/index-BEgWFDZf.jsstatic/desktop/assets/index-B_XPm7mm.jsstatic/desktop/assets/index-C7isKigO.jsstatic/desktop/assets/index-CH8xqmNE.jsstatic/desktop/assets/index-CTe7-jHC.jsstatic/desktop/assets/index-C_KJzFJ_.jsstatic/desktop/assets/index-C_qAIZSt.jsstatic/desktop/assets/index-CoNKmJJQ.jsstatic/desktop/assets/index-CoQ45O6-.jsstatic/desktop/assets/index-D-E10IgF.jsstatic/desktop/assets/index-DTh72AYJ.jsstatic/desktop/assets/index-DdCLyul1.jsstatic/desktop/assets/index-Dw2m-Rvd.jsstatic/desktop/assets/index-DwzRNNkz.jsstatic/desktop/assets/index-Dza7_6d-.jsstatic/desktop/assets/main-Bs5bQgxi.jsstatic/desktop/assets/tokens-B9Rl30P8.jsstatic/desktop/assets/tokens-gmaKUSZd.cssstatic/desktop/assets/tokens-ib1qRNqW.cssstatic/desktop/assets/vendor-codemirror-CL2HhW7v.jsstatic/desktop/assets/vendor-icons-wm645Jsx.jsstatic/desktop/chat.htmlstatic/desktop/index.htmltests/e2e/test_chat_phase2b1.pytests/e2e/test_chat_phase2b2a.pytests/test_agent_chat_router.pytests/test_bridge_session_phase1.pytests/test_chat_attachments.pytests/test_chat_edit_delete.pytests/test_chat_help.pytests/test_chat_mark_unread.pytests/test_chat_messages.pytests/test_chat_pin_request.pytests/test_chat_pins.pytests/test_chat_threads.pytests/test_routes_agents.pytinyagentos/agent_chat_router.pytinyagentos/chat/channel_store.pytinyagentos/chat/help.pytinyagentos/chat/message_store.pytinyagentos/chat/reactions.pytinyagentos/chat/threads.pytinyagentos/routes/chat.pytinyagentos/scripts/install_hermes.shtinyagentos/scripts/install_langroid.shtinyagentos/scripts/install_openai-agents-sdk.shtinyagentos/scripts/install_openai_agents_sdk.shtinyagentos/scripts/install_pocketflow.shtinyagentos/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
| 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> |
There was a problem hiding this comment.
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.
| 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.
| fetch(`/api/chat/channels/${channelId}/threads/${parentId}/messages`) | ||
| .then((r) => r.json()) | ||
| .then((d) => { if (alive) setMsgs(d.messages || []); }); |
There was a problem hiding this comment.
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.
| 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.
| async function submit() { | ||
| const content = input.trim(); | ||
| if (!content) return; | ||
| setInput(""); | ||
| await onSend(content, []); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| /* ---- 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]); |
There was a problem hiding this comment.
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.
| 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); | ||
| } | ||
| }; |
There was a problem hiding this comment.
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}; | |||
There was a problem hiding this comment.
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.
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.
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}; | |||
There was a problem hiding this comment.
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.
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.
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}; | |||
There was a problem hiding this comment.
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}; | |||
There was a problem hiding this comment.
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(` | |||
There was a problem hiding this comment.
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.
🧩 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 -20Repository: 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.
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.(edited)marker in author row.Pinning
New backend endpoints
Based on #236 (Phase 2b-1)
Branched off `feat/chat-phase-2b-1-threads-attachments`. Should merge after #236.
Test plan
Summary by CodeRabbit