Chat Phase 2b-2a — per-message affordances + pinning#237
Chat Phase 2b-2a — per-message affordances + pinning#237
Conversation
Code Review SummaryStatus: 20 Issues Found | Recommendation: Address before merge ✅ Secrets have been removed from the PR. The previously committed auth token must still be revoked. Fix these issues in Kilo Cloud Overview
✅ SECURITY FIXED (click to expand)
Original Issues Status (click to expand)
New Issues Introduced (click to expand)CRITICAL
WARNING
Files Reviewed (4 files changed)
Reviewed by seed-2-0-pro-260328 · 294,618 tokens |
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 0 minutes and 4 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (14)
📝 WalkthroughWalkthroughA comprehensive chat system enhancement introducing per-message affordances including message editing and soft deletion, human-only pinning with a 50-message cap and agent pin-request flagging, deep-link scrolling via Changes
Sequence Diagram(s)sequenceDiagram
participant User as User (Browser)
participant Client as MessagesApp
participant WebSocket as WebSocket/Server
participant DB as Database
User->>Client: Click "More" on message
Client->>Client: Show MessageOverflowMenu
alt Edit Message
User->>Client: Click "Edit"
Client->>Client: Show MessageEditor with initial content
User->>Client: Enter new content, press Enter
Client->>WebSocket: PATCH /api/chat/messages/{id}
WebSocket->>DB: Update content, set edited_at
DB-->>WebSocket: Updated message
WebSocket-->>Client: Broadcast message_edit event
Client->>Client: Update message, render with "(edited)" marker
else Delete Message
User->>Client: Click "Delete"
Client->>WebSocket: DELETE /api/chat/messages/{id}
WebSocket->>DB: Set deleted_at timestamp
DB-->>WebSocket: Soft-delete confirmed
WebSocket-->>Client: Broadcast message_delete event
Client->>Client: Render MessageTombstone
else Pin Message (Human)
User->>Client: Click "Pin"
Client->>WebSocket: POST /api/chat/messages/{id}/pin
WebSocket->>DB: Insert into chat_pins table
DB-->>WebSocket: Pin confirmed (or 409 if cap reached)
WebSocket-->>Client: Broadcast pin event
Client->>Client: Show PinBadge, trigger pin list fetch
else Pin Request (Agent)
User->>Client: Agent reacts 📌 to own message
Client->>WebSocket: Send reaction event
WebSocket->>DB: Set metadata.pin_requested = true
DB-->>WebSocket: Flag set
WebSocket-->>Client: Broadcast reaction event
Client->>Client: Render PinRequestAffordance
User->>Client: Click "Pin this" button
Client->>WebSocket: POST /api/chat/messages/{id}/pin
WebSocket->>DB: Create pin, clear pin_requested flag
DB-->>WebSocket: Pin confirmed
else Mark Unread
User->>Client: Click "Mark unread"
Client->>WebSocket: POST /api/chat/channels/{id}/read-cursor/rewind
WebSocket->>DB: Rewind chat_read_positions.last_read_at
DB-->>WebSocket: Cursor rewound
WebSocket-->>Client: Rewind confirmed
else Deep Link Navigation
User->>Client: Navigate to ?msg={id}
Client->>Client: Fetch messages for selected channel
Client->>Client: Locate message by data-message-id
Client->>Client: Scroll into view + apply .data-highlight
Client->>Client: Yellow outline + animation
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 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 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
| 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.
| @@ -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.
- chat-attachments-api: normalize upload response to AttachmentRecord (backend returns content_type, not mime_type; no source field) - FilePicker: reconcile (not append) VFS multi-select; disk stays additive - ThreadPanel: preserve draft on send failure; AbortController on load; surface load/send errors; disable textarea while sending - MessagesApp: skip WS fallback send when slash-handled (no double-post); HTTP POST fallback when WS is down (attachments + text still land); deep-link scroll latches per (channel, msg) so later messages don't rescroll; isHuman=true for overflow pin menu (viewer is always human on desktop); ThreadPanel onSend propagates 4xx as thrown Error - MessageOverflowMenu: ArrowUp/Down/Home/End/Escape keyboard nav, focus first item on mount (proper menu a11y) - routes/chat.py: PATCH broadcasts message_edit (matches WS handler); message_delete payload includes deleted_at for tombstone render - MessagesApp WS: message_delete sets deleted_at on row (tombstone) instead of filtering the row out - rebuilt desktop bundle
Per-message affordances - Hover `⋯` opens overflow: Edit, Delete, Copy link, Pin/Unpin, Mark unread. - Edit-own: text-only, always editable, `(edited)` marker. - Delete-own: soft delete via `deleted_at`; tombstone preserves thread anchors. - Copy link: `https://<host>/chat/<channel>?msg=<id>` deep-links with scroll + highlight on open (latched so late WS messages don't re-trigger). - Mark unread: rewinds `chat_read_positions.last_read_at`. Pinning - Humans pin directly (`⋯` → Pin). 50/channel cap. Channel header shows `📌 N` badge with popover and "Jump to →" navigation. - Agents request pins by adding 📌 to their own messages → humans see inline "@agent wants to pin this" pill with one-click approval. Backend endpoints - `GET /api/chat/channels/{id}/pins` - `POST /api/chat/messages/{id}/pin` (human-only; 409 when cap hit) - `DELETE /api/chat/messages/{id}/pin` - `PATCH /api/chat/messages/{id}` (author-only text edit) - `DELETE /api/chat/messages/{id}` (author-only soft delete, idempotent) - `POST /api/chat/channels/{id}/read-cursor/rewind` Includes review-feedback fixes: normalize upload response, reconcile VFS multi-select, preserve draft on send failure, drop WS gate on HTTP paths, fix pin menu isHuman, keyboard nav on overflow menu, align WS message_edit/message_delete events with tombstone rendering.
3e468ae to
813982a
Compare
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (1)
desktop/src/apps/chat/ThreadPanel.tsx (1)
43-50:⚠️ Potential issue | 🟠 MajorParse thread replies as an array and surface HTTP failures.
This endpoint returns an array, not
{ messages: [...] }, sosetMsgs(d.messages || [])still drops all replies. Also, coercing non-OK responses to{ messages: [] }means the new error banner never appears on HTTP failures.Suggested fix
useEffect(() => { const controller = new AbortController(); + setLoadError(null); fetch(`/api/chat/channels/${channelId}/threads/${parentId}/messages`, { signal: controller.signal }) - .then((r) => (r.ok ? r.json() : { messages: [] })) - .then((d) => setMsgs(d.messages || [])) + .then(async (r) => { + if (!r.ok) throw new Error(`HTTP ${r.status}`); + const d = await r.json(); + return Array.isArray(d) ? d : []; + }) + .then((d) => setMsgs(d)) .catch((e) => { if ((e as Error).name !== "AbortError") setLoadError("couldn't load this thread"); });🤖 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 43 - 50, The fetch in the useEffect for loading thread replies mis-parses the response (endpoint returns an array) and hides HTTP errors by coercing non-OK responses to {messages: []}; update the fetch handling in the useEffect that creates controller so that: if response.ok parse r.json() as an array and call setMsgs(parsedArray || []), otherwise throw an Error (or create one with status) so the .catch path runs and sets setLoadError; preserve the AbortError check in the catch so aborts don't show the load error; reference the existing symbols useEffect, controller, fetch(`/api/chat/channels/${channelId}/threads/${parentId}/messages`), setMsgs and setLoadError when making the change.
🧹 Nitpick comments (4)
desktop/src/lib/chat-messages-api.ts (1)
18-32: Add concrete response types to this API surface.Returning
unknown[]andunknownthrows away the backend contract right at the client boundary, so consumers lose compile-time checks for fields likeid,author_id,pinned_by, andedited_at.A small exported
PinnedMessagetype plus anEditedMessage/ChatMessageresponse type here would keep the new affordance code type-safe.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@desktop/src/lib/chat-messages-api.ts` around lines 18 - 32, The listPins and editMessage functions currently return unknown types which discards the backend contract; define and export concrete interfaces (e.g., PinnedMessage with fields id, author_id, pinned_by, pinned_at and ChatMessage/EditedMessage with id, author_id, content, edited_at, etc.), update listPins to return Promise<PinnedMessage[]> and editMessage to return Promise<ChatMessage> (or EditedMessage) and use these types for the parsed JSON results in listPins (body.pins as PinnedMessage[]) and editMessage (await r.json() as ChatMessage) so callers get compile-time guarantees for fields like id, author_id, pinned_by, and edited_at.tinyagentos/chat/message_store.py (2)
75-82: Catch specificsqlite3.OperationalErrorinstead of bareException.The static analysis correctly flags this. While the intent is valid (column may already exist), catching bare
Exceptioncould mask unrelated errors like connection issues or permission problems.♻️ Proposed fix
+import sqlite3 + async def init(self) -> None: await super().init() try: await self._db.execute("ALTER TABLE chat_messages ADD COLUMN deleted_at REAL") await self._db.commit() - except Exception: - # column already exists (SQLite raises on duplicate column name) + except sqlite3.OperationalError: + # Column already exists (SQLite raises OperationalError on duplicate) pass🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tinyagentos/chat/message_store.py` around lines 75 - 82, The init method currently swallows all exceptions when adding the deleted_at column; change the except to catch sqlite3.OperationalError specifically (to only ignore the "duplicate column name" SQLite error) and ensure sqlite3 is imported at the top of the module; keep the existing behavior of committing on success but re-raise other unexpected errors from the async init() that involves self._db.execute("ALTER TABLE chat_messages ADD COLUMN deleted_at REAL") so only OperationalError is suppressed.
301-325: Minor race window in pin cap enforcement.Between the cap check (line 313-318) and the insert (line 321-324), another concurrent request could also pass the cap check. This could result in slightly exceeding 50 pins. For a soft limit, this is likely acceptable.
If strict enforcement is needed, consider using a transaction with
SELECT ... FOR UPDATEsemantics or a unique partial index, but for this use case the current approach is pragmatic.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tinyagentos/chat/message_store.py` around lines 301 - 325, The pin_message method has a race between the COUNT check and the INSERT that can exceed PIN_CAP_PER_CHANNEL; fix by performing the cap check and insert atomically—either wrap the SELECT COUNT(*) and subsequent INSERT inside a single DB transaction/lock (e.g., BEGIN IMMEDIATE/EXCLUSIVE or a serializable transaction) so no concurrent writer can slip in, or add a DB-enforced constraint (partial/conditional unique or check constraint on chat_pins) and handle IntegrityError to raise the same ValueError; ensure changes reference pin_message, chat_pins, and PIN_CAP_PER_CHANNEL and still commit or rollback appropriately.desktop/src/apps/MessagesApp.tsx (1)
1261-1268: Duplicated scroll-and-highlight logic.The scroll-to-message + highlight logic appears in both the deep-link effect (lines 559-564) and the pinned-messages popover
onJumpTohandler (lines 1263-1268). Extract to a shared helper to reduce duplication and ensure consistent behavior.♻️ Proposed helper extraction
// Add near other helpers const scrollToMessage = useCallback((msgId: string) => { 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); } }, []); // Then use in both places: // Deep-link effect: scrollToMessage(msgId); // PinnedMessagesPopover onJumpTo: onJumpTo={(id) => { setPinnedPopoverOpen(false); scrollToMessage(id); }}🤖 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 1261 - 1268, Duplicate scroll-and-highlight logic in the deep-link effect and the pinned-messages popover: extract it into a shared helper (e.g., create a useCallback named scrollToMessage(msgId: string)) that performs document.querySelector(`[data-message-id="${msgId}"]`), scrollIntoView({behavior: "smooth", block: "center"}), adds the "data-highlight" class and removes it after 2000ms; then replace the inline code in the deep-link effect with scrollToMessage(msgId) and update the pinned popover onJumpTo handler to call setPinnedPopoverOpen(false) followed by scrollToMessage(id).
🤖 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/ThreadPanel.tsx`:
- Around line 31-39: The fetch in the useEffect currently maps non-OK responses
to null so setParent gets null instead of surfacing an error; instead, when
fetching `/api/chat/messages/${parentId}` inside the useEffect (using
AbortController and controller.signal) detect r.ok and if false throw an Error
(include status/text) so the catch branch runs and calls setLoadError("couldn't
load this thread"); keep the existing AbortError check in the catch so aborts
are ignored, and only call setParent with the parsed JSON on successful
responses.
In `@desktop/src/apps/MessagesApp.tsx`:
- Line 558: The code interpolates msgId (from URL params) directly into
document.querySelector(`[data-message-id="${msgId}"]`), which can be exploited
for selector injection/XSS; validate and sanitize msgId before use by enforcing
an expected format (e.g., alphanumeric, hyphen/underscore) or parsing it as an
integer if IDs are numeric, and only call document.querySelector when the
validatedMsgId matches the pattern; update the usage sites (the msgId variable,
the document.querySelector call and any callers that derive msgId) to perform
this validation and bail out or URL-decode/normalize before querying the DOM to
ensure data-message-id cannot be manipulated into breaking the selector.
In `@docs/chat-guide.md`:
- Line 377: Change the American-English spelling of "afterwards" to "afterward"
in the sentence "Hover a message → `⋯` → **Edit** (your own text only). Press
**Enter** to save, **Esc** to cancel. A small `(edited)` marker shows next to
the author line afterwards." by updating that final word ("afterwards") to
"afterward" in docs/chat-guide.md so the phrasing reads "...shows next to the
author line afterward."
In `@tests/e2e/test_chat_phase2b2a.py`:
- Around line 62-69: The test test_deep_link_scroll currently navigates to
f"{URL}?msg={msg_id}" which does not use the actual deep-link route
(/chat/<channel>?msg=<id>); update the navigation to load the channel path so
the app performs cold-navigation into the channel and scrolls to the message
(e.g., construct the URL using the channel slug you clicked — "roundtable" — and
navigate to f"{URL}/chat/roundtable?msg={msg_id}" or otherwise derive the
channel from the clicked element before calling page.goto) so the locator expect
assertion verifies the real deep-link behavior.
- Around line 22-59: These tests mutate a shared message by using
locator("[data-message-id]").first in test_edit_own_message,
test_delete_own_message_shows_tombstone, and test_pin_badge_and_popover; change
each test to create or target a dedicated fixture message before performing
actions: programmatically post a new uniquely-identified message (e.g., include
a random suffix in the message text via the composer or an API POST), then
locate that message by its unique text (or returned message id) instead of using
.first under "roundtable", and use that locator for hover(), More menu clicks,
edit/delete/pin actions and assertions so the tests no longer mutate shared
state.
In `@tinyagentos/chat/reactions.py`:
- Around line 86-93: After setting meta["pin_requested"] = True and awaiting
msg_store.set_metadata(message["id"], meta) in the pin reaction branch (the
block that checks emoji == "📌" and reactor_type == "agent" and reactor_id ==
message.get("author_id")), add a return to match other semantic reaction
handlers, and then notify clients by broadcasting the update: either fetch the
updated full message from msg_store (e.g. msg_store.get_message(message["id"]))
and call the hub broadcast used elsewhere (e.g. state.hub.broadcast) with a
suitable update payload, or broadcast a "reaction_update" event including the
message id and updated metadata so clients can render the PinRequestAffordance.
Ensure you reference the same msg_store, set_metadata, and hub broadcast
mechanism used by other metadata-update handlers.
---
Duplicate comments:
In `@desktop/src/apps/chat/ThreadPanel.tsx`:
- Around line 43-50: The fetch in the useEffect for loading thread replies
mis-parses the response (endpoint returns an array) and hides HTTP errors by
coercing non-OK responses to {messages: []}; update the fetch handling in the
useEffect that creates controller so that: if response.ok parse r.json() as an
array and call setMsgs(parsedArray || []), otherwise throw an Error (or create
one with status) so the .catch path runs and sets setLoadError; preserve the
AbortError check in the catch so aborts don't show the load error; reference the
existing symbols useEffect, controller,
fetch(`/api/chat/channels/${channelId}/threads/${parentId}/messages`), setMsgs
and setLoadError when making the change.
---
Nitpick comments:
In `@desktop/src/apps/MessagesApp.tsx`:
- Around line 1261-1268: Duplicate scroll-and-highlight logic in the deep-link
effect and the pinned-messages popover: extract it into a shared helper (e.g.,
create a useCallback named scrollToMessage(msgId: string)) that performs
document.querySelector(`[data-message-id="${msgId}"]`),
scrollIntoView({behavior: "smooth", block: "center"}), adds the "data-highlight"
class and removes it after 2000ms; then replace the inline code in the deep-link
effect with scrollToMessage(msgId) and update the pinned popover onJumpTo
handler to call setPinnedPopoverOpen(false) followed by scrollToMessage(id).
In `@desktop/src/lib/chat-messages-api.ts`:
- Around line 18-32: The listPins and editMessage functions currently return
unknown types which discards the backend contract; define and export concrete
interfaces (e.g., PinnedMessage with fields id, author_id, pinned_by, pinned_at
and ChatMessage/EditedMessage with id, author_id, content, edited_at, etc.),
update listPins to return Promise<PinnedMessage[]> and editMessage to return
Promise<ChatMessage> (or EditedMessage) and use these types for the parsed JSON
results in listPins (body.pins as PinnedMessage[]) and editMessage (await
r.json() as ChatMessage) so callers get compile-time guarantees for fields like
id, author_id, pinned_by, and edited_at.
In `@tinyagentos/chat/message_store.py`:
- Around line 75-82: The init method currently swallows all exceptions when
adding the deleted_at column; change the except to catch
sqlite3.OperationalError specifically (to only ignore the "duplicate column
name" SQLite error) and ensure sqlite3 is imported at the top of the module;
keep the existing behavior of committing on success but re-raise other
unexpected errors from the async init() that involves self._db.execute("ALTER
TABLE chat_messages ADD COLUMN deleted_at REAL") so only OperationalError is
suppressed.
- Around line 301-325: The pin_message method has a race between the COUNT check
and the INSERT that can exceed PIN_CAP_PER_CHANNEL; fix by performing the cap
check and insert atomically—either wrap the SELECT COUNT(*) and subsequent
INSERT inside a single DB transaction/lock (e.g., BEGIN IMMEDIATE/EXCLUSIVE or a
serializable transaction) so no concurrent writer can slip in, or add a
DB-enforced constraint (partial/conditional unique or check constraint on
chat_pins) and handle IntegrityError to raise the same ValueError; ensure
changes reference pin_message, chat_pins, and PIN_CAP_PER_CHANNEL and still
commit or rollback appropriately.
🪄 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: 570ae9bc-ee12-4c79-bcba-abbfe219a4cb
📒 Files selected for processing (48)
data/.auth_local_tokendata/agents.jsondesktop/src/apps/MessagesApp.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/ThreadPanel.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/lib/__tests__/chat-messages-api.test.tsdesktop/src/lib/chat-attachments-api.tsdesktop/src/lib/chat-messages-api.tsdesktop/src/shell/FilePicker.tsxdesktop/src/theme/tokens.cssdesktop/tsconfig.tsbuildinfodocs/chat-guide.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/MCPApp-CYhJxm8U.jsstatic/desktop/assets/MessagesApp-B9mz5KFg.jsstatic/desktop/assets/ProvidersApp-_k43QTNu.jsstatic/desktop/assets/SettingsApp-D7dh6TbE.jsstatic/desktop/assets/chat-CAyTzkHI.jsstatic/desktop/assets/main-BBj9B0Tq.jsstatic/desktop/assets/tokens-B9Rl30P8.jsstatic/desktop/assets/tokens-gmaKUSZd.cssstatic/desktop/chat.htmlstatic/desktop/index.htmltests/e2e/test_chat_phase2b2a.pytests/test_chat_edit_delete.pytests/test_chat_mark_unread.pytests/test_chat_messages.pytests/test_chat_pin_request.pytests/test_chat_pins.pytests/test_routes_agents.pytinyagentos/chat/channel_store.pytinyagentos/chat/message_store.pytinyagentos/chat/reactions.pytinyagentos/routes/chat.py
✅ Files skipped from review due to trivial changes (14)
- data/.auth_local_token
- desktop/src/apps/chat/tests/MessageTombstone.test.tsx
- desktop/src/apps/chat/MessageTombstone.tsx
- desktop/src/apps/chat/tests/PinBadge.test.tsx
- desktop/src/theme/tokens.css
- static/desktop/index.html
- static/desktop/chat.html
- desktop/src/apps/chat/tests/MessageEditor.test.tsx
- desktop/src/shell/FilePicker.tsx
- static/desktop/assets/ProvidersApp-_k43QTNu.js
- data/agents.json
- static/desktop/assets/chat-CAyTzkHI.js
- static/desktop/assets/SettingsApp-D7dh6TbE.js
- docs/superpowers/plans/2026-04-19-chat-phase-2b-2a-per-msg.md
🚧 Files skipped from review as they are similar to previous changes (12)
- desktop/src/apps/chat/tests/PinRequestAffordance.test.tsx
- desktop/src/apps/chat/tests/MessageHoverActions.test.tsx
- desktop/src/apps/chat/tests/PinnedMessagesPopover.test.tsx
- desktop/src/apps/chat/PinRequestAffordance.tsx
- desktop/src/apps/chat/MessageHoverActions.tsx
- desktop/src/apps/chat/PinBadge.tsx
- desktop/src/apps/chat/tests/MessageOverflowMenu.test.tsx
- desktop/src/apps/chat/PinnedMessagesPopover.tsx
- desktop/src/lib/tests/chat-messages-api.test.ts
- desktop/src/apps/chat/MessageEditor.tsx
- desktop/src/apps/chat/MessageOverflowMenu.tsx
- desktop/tsconfig.tsbuildinfo
| def test_edit_own_message(page: Page): | ||
| page.goto(URL) | ||
| page.get_by_role("button", name="Messages").click() | ||
| page.get_by_text("roundtable").first.click() | ||
| first = page.locator("[data-message-id]").first | ||
| first.hover() | ||
| page.get_by_role("button", name="More").click() | ||
| page.get_by_role("menuitem", name=re.compile("Edit", re.I)).click() | ||
| editor = page.get_by_role("textbox", name=re.compile("Edit message", re.I)) | ||
| editor.fill("edited content") | ||
| editor.press("Enter") | ||
| expect(page.get_by_text("edited content")).to_be_visible() | ||
| expect(page.get_by_text("(edited)")).to_be_visible() | ||
|
|
||
|
|
||
| def test_delete_own_message_shows_tombstone(page: Page): | ||
| page.goto(URL) | ||
| page.get_by_role("button", name="Messages").click() | ||
| page.get_by_text("roundtable").first.click() | ||
| first = page.locator("[data-message-id]").first | ||
| first.hover() | ||
| page.get_by_role("button", name="More").click() | ||
| page.on("dialog", lambda d: d.accept()) | ||
| page.get_by_role("menuitem", name=re.compile("Delete", re.I)).click() | ||
| expect(page.get_by_text("This message was deleted")).to_be_visible() | ||
|
|
||
|
|
||
| def test_pin_badge_and_popover(page: Page): | ||
| page.goto(URL) | ||
| page.get_by_role("button", name="Messages").click() | ||
| page.get_by_text("roundtable").first.click() | ||
| first = page.locator("[data-message-id]").first | ||
| first.hover() | ||
| page.get_by_role("button", name="More").click() | ||
| page.get_by_role("menuitem", name=re.compile("Pin", re.I)).click() | ||
| expect(page.get_by_role("button", name=re.compile("Pinned messages", re.I))).to_be_visible() | ||
| page.get_by_role("button", name=re.compile("Pinned messages", re.I)).click() | ||
| expect(page.get_by_role("dialog", name=re.compile("Pinned messages", re.I))).to_be_visible() |
There was a problem hiding this comment.
Avoid mutating the shared first message across these E2E cases.
These tests all target locator("[data-message-id]").first, but the precondition only guarantees that some message is authored by the test user, not the first one. On a persistent TAOS_E2E_URL environment, edit/delete/pin will also mutate shared state for later tests, so order starts to matter.
Create or target a dedicated fixture message per test instead of reusing the first existing row in roundtable.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/e2e/test_chat_phase2b2a.py` around lines 22 - 59, These tests mutate a
shared message by using locator("[data-message-id]").first in
test_edit_own_message, test_delete_own_message_shows_tombstone, and
test_pin_badge_and_popover; change each test to create or target a dedicated
fixture message before performing actions: programmatically post a new
uniquely-identified message (e.g., include a random suffix in the message text
via the composer or an API POST), then locate that message by its unique text
(or returned message id) instead of using .first under "roundtable", and use
that locator for hover(), More menu clicks, edit/delete/pin actions and
assertions so the tests no longer mutate shared state.
| def test_deep_link_scroll(page: Page): | ||
| page.goto(URL) | ||
| page.get_by_role("button", name="Messages").click() | ||
| page.get_by_text("roundtable").first.click() | ||
| msg = page.locator("[data-message-id]").first | ||
| msg_id = msg.get_attribute("data-message-id") | ||
| page.goto(f"{URL}?msg={msg_id}") | ||
| expect(page.locator(f"[data-message-id='{msg_id}']")).to_be_visible() |
There was a problem hiding this comment.
Use the actual deep-link route in this test.
The feature is documented as /chat/<channel>?msg=<id>, but this test reloads ${URL}?msg=... and never reselects the channel. That means it isn't covering the cold-navigation deep-link flow that should load the target channel and then scroll/highlight the message.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/e2e/test_chat_phase2b2a.py` around lines 62 - 69, The test
test_deep_link_scroll currently navigates to f"{URL}?msg={msg_id}" which does
not use the actual deep-link route (/chat/<channel>?msg=<id>); update the
navigation to load the channel path so the app performs cold-navigation into the
channel and scrolls to the message (e.g., construct the URL using the channel
slug you clicked — "roundtable" — and navigate to
f"{URL}/chat/roundtable?msg={msg_id}" or otherwise derive the channel from the
clicked element before calling page.goto) so the locator expect assertion
verifies the real deep-link behavior.
- ThreadPanel: treat non-OK parent fetch as error (surface via loadError) - MessagesApp: validate msgId format before querySelector (guard against selector injection via crafted ?msg= URL) - reactions.py: after setting pin_requested, broadcast message_edit with metadata so clients render PinRequestAffordance without reload - MessagesApp WS: message_edit now merges metadata from broadcast - chat-guide: afterwards → afterward (American English) - rebuilt desktop bundle
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
Release Notes
New Features
Bug Fixes
Documentation