Skip to content

Chat Phase 2b-1 — threads, attachments, shared file picker, chat guide#236

Open
jaylfc wants to merge 21 commits intomasterfrom
feat/chat-phase-2b-1-threads-attachments
Open

Chat Phase 2b-1 — threads, attachments, shared file picker, chat guide#236
jaylfc wants to merge 21 commits intomasterfrom
feat/chat-phase-2b-1-threads-attachments

Conversation

@jaylfc
Copy link
Copy Markdown
Owner

@jaylfc jaylfc commented Apr 19, 2026

Summary

Phase 2b-1 of taOS chat: Slack-style threads, message attachments, a reusable shared file-picker shell primitive, and a canonical chat guide with /help command.

Threads

  • Right-side ThreadPanel (mutex with channel settings).
  • Narrow routing: parent author (if agent) + prior repliers + @mentions; @all escalates.
  • Per-thread policy key (`channel_id:thread:parent_id`) — hops/cooldown/rate-cap scope per thread.
  • Thread-scoped context window (thread messages + parent prepended).
  • Hover toolbar: 😀 / 💬 Reply in thread / ⋯.

Attachments

  • Paperclip, drag-drop, paste — disk + my-workspace + agent-workspace sources.
  • SharedFilePickerDialog (`openFilePicker`) as reusable shell primitive.
  • Inline gallery: 1 image inline, 2+ images in 2-col grid, lightbox with ←/→/Esc.
  • File tiles for non-images. Up to 10 attachments / 100 MB per file.
  • Bridges expose attachments to agents via structured payload + text footer.

/help + guide

  • `/help [topic]` posts a system message in-channel (bypasses bare-slash guardrail).
  • docs/chat-guide.md — single source of truth covering P1 + 2a + 2b-1.
  • "?" icon in channel header linking to the guide.

New backend endpoints

  • `POST /api/chat/attachments/from-path`
  • `GET /api/chat/channels/{id}/threads/{parent_id}/messages`
  • `GET /api/chat/messages/{id}`

Test plan

  • Backend: 756 tests pass (1 pre-existing Mac-arm hardware test unrelated).
  • Desktop: 242 tests pass (3 pre-existing snap-zones unrelated).
  • Desktop bundle rebuilt and committed to `static/desktop/`.
  • Playwright E2E stubs created; gated on `TAOS_E2E_URL`.
  • Manual smoke: open a channel, hover a message, Reply in thread, send a reply, attach a file via paperclip, drop a file, paste an image, post `/help threads`.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added threaded messaging: reply to specific messages to start conversations.
    • Added file attachment support: upload and share files directly in messages.
    • Added improved message actions: hover over messages to react, reply in threads, or access more options.
    • Added comprehensive chat guide documentation.
  • Documentation

    • Added user guide covering channels, mentions, reactions, threads, attachments, and the help system.
  • Tests

    • Added test coverage for attachment, threading, and file picker components.

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

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

coderabbitai bot commented Apr 19, 2026

📝 Walkthrough

Walkthrough

This PR adds comprehensive chat enhancements including thread panel UI for message discussions, multi-file attachment support with upload/preview/gallery components, a file picker UI for selecting files from disk/workspace/agent sources, and supporting chat API helpers. Includes new components, hooks, tests, and documentation.

Changes

Cohort / File(s) Summary
Chat Core Updates
desktop/src/apps/MessagesApp.tsx
Major refactor replacing single hover reaction button with conditional MessageHoverActions, adding hoveredMessageId state tracking, extending Message interface with attachments, reply_count, lastReplyAt. Integrates thread panel flow, handles attachment uploads with pending state management, reworks message sending to POST via HTTP when attachments present or fallback to WebSocket for text-only. Adds paste handling for images and file picker support.
Thread Components
desktop/src/apps/chat/ThreadPanel.tsx, ThreadIndicator.tsx
desktop/src/lib/use-thread-panel.ts
Adds ThreadPanel component for viewing/replying to message threads with fetch-on-load pattern and Enter-to-submit composer. ThreadIndicator conditionally renders thread metadata (reply count and relative last-reply time). useThreadPanel hook manages thread open/close state with openThread, openThreadFor, and closeThread methods.
Attachment UI Components
desktop/src/apps/chat/AttachmentGallery.tsx, AttachmentLightbox.tsx, AttachmentsBar.tsx
AttachmentGallery splits attachments into images/files, renders responsive thumbnail grid (max 4 with "+N more" overlay), and files as clickable links with size labels. AttachmentLightbox provides full-screen image viewer with keyboard navigation (Escape, arrows) and counter display. AttachmentsBar displays pending uploads with uploading/error states and retry/remove buttons.
Message Hover Actions
desktop/src/apps/chat/MessageHoverActions.tsx
New toolbar component rendering three action buttons (react, reply-in-thread, more) with emoji icons and callback handlers.
Chat API Helpers
desktop/src/lib/chat-attachments-api.ts
Exports AttachmentRecord type and two async functions: uploadDiskFile (POSTs FormData to /api/chat/upload) and attachmentFromPath (POSTs JSON to /api/chat/attachments/from-path with validation/error handling).
File Picker System
desktop/src/shell/FilePicker.tsx, VfsBrowser.tsx, file-picker-api.ts
FilePicker modal dialog manages tab-based file selection from disk/workspace/agent sources with multi-select and queuing. VfsBrowser provides virtual filesystem browsing with folder navigation, entry sorting, and single/multi-select modes. file-picker-api exports openFilePicker function that mounts picker into DOM and returns Promise resolving with selections or empty array on cancel.
Component Tests
desktop/src/apps/chat/__tests__/AttachmentGallery.test.tsx, AttachmentsBar.test.tsx, MessageHoverActions.test.tsx, ThreadIndicator.test.tsx
desktop/src/lib/__tests__/chat-attachments-api.test.ts
desktop/src/shell/__tests__/FilePicker.test.tsx, VfsBrowser.test.tsx
Comprehensive test suites covering empty/rendered states, callback invocations, user interactions (clicks, keyboard), and API call assertions using Vitest and React Testing Library.
Documentation
docs/chat-guide.md
docs/superpowers/plans/2026-04-19-chat-phase-2b-1-threads-attachments.md
Chat guide documents channel types, mention semantics, throttling behavior, emoji reactions, slash menu, thread behavior, and attachments. Planning document outlines backend/frontend implementation tasks, test requirements, and integration steps.
Build Configuration
desktop/tsconfig.tsbuildinfo
Updated TypeScript build metadata to include new chat components and file picker modules in root file list.
Static Asset Bundles
static/desktop/assets/*
Multiple bundled JavaScript modules updated: existing apps (ActivityApp, BrowserApp, etc.) use new icon vendor imports; deprecated apps (CalendarApp-BJnvuKGY, GitHubApp-CkwARPpU, LibraryApp-NzJAyw3P, ImportApp-AV3jmR5U) removed; new/replaced bundles (CalendarApp-DSaV9uPb, GitHubApp-IYMAlDty, LibraryApp-Cdo_EHou, ImportApp-DBAV17Xb, RedditApp-BOuG46mh, MobileSplitView-CtNEF6zb) added with updated implementations. MessagesApp-C7hv44-7.js replaces MessagesApp-DJJbqaHc.js with thread/attachment support integrated.

Sequence Diagrams

sequenceDiagram
    participant User
    participant Frontend
    participant FileAPI
    participant Backend as Backend API
    participant Storage

    User->>Frontend: Select files (disk/workspace)
    activate Frontend
    Frontend->>Frontend: Queue files in pendingAttachments
    Frontend->>User: Display AttachmentsBar with progress
    User->>Frontend: Click Send
    activate FileAPI
    loop For each pending attachment
        FileAPI->>Backend: POST /api/chat/upload (FormData)
        activate Backend
        Backend->>Storage: Save file
        Backend-->>FileAPI: AttachmentRecord {url, filename, mime_type}
        deactivate Backend
        Frontend->>Frontend: Update attachment state (ready)
    end
    deactivate FileAPI
    Frontend->>Backend: POST /api/chat/messages with attachments[]
    activate Backend
    Backend-->>Frontend: Message created
    deactivate Backend
    Frontend->>User: Clear composer, show message
    deactivate Frontend
Loading
sequenceDiagram
    participant User
    participant Frontend
    participant ThreadPanel
    participant Backend as Backend API

    User->>Frontend: Click ThreadIndicator
    activate Frontend
    Frontend->>Frontend: Set openThread state
    Frontend->>ThreadPanel: Render with channelId, parentId
    activate ThreadPanel
    ThreadPanel->>Backend: GET /api/chat/messages/{parentId}
    Backend-->>ThreadPanel: Parent message
    ThreadPanel->>Backend: GET /api/chat/channels/{channelId}/threads/{parentId}/messages
    Backend-->>ThreadPanel: Thread messages list
    ThreadPanel->>User: Display thread conversation
    deactivate ThreadPanel
    User->>ThreadPanel: Type reply + click Send
    ThreadPanel->>Backend: POST /api/chat/messages {thread_id, content, attachments}
    activate Backend
    Backend-->>ThreadPanel: Message created
    deactivate Backend
    ThreadPanel->>Frontend: Call onSend callback
    Frontend->>Frontend: Refetch thread/close panel
    deactivate Frontend
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • #235: Modifies MessagesApp UI surface and introduces interlocking chat-side panels (settings/channel admin, slash menu, threads/attachments) with coordinated open/close state management.

Poem

🐰 Threads and files now dance with glee,
In folders deep or workspace free,
A rabbit hops through galleries bright,
While attachments upload through the night! 📎✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.02% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: implementing threads, attachments, a shared file picker, and documentation, which aligns with the extensive feature additions throughout the changeset.

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

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

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

@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot bot commented Apr 19, 2026

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (18 files)
  • desktop/src/apps/MessagesApp.tsx
  • desktop/src/apps/chat/AttachmentGallery.tsx
  • desktop/src/apps/chat/AttachmentLightbox.tsx
  • desktop/src/apps/chat/AttachmentsBar.tsx
  • desktop/src/apps/chat/MessageHoverActions.tsx
  • desktop/src/apps/chat/ThreadIndicator.tsx
  • desktop/src/apps/chat/ThreadPanel.tsx
  • desktop/src/apps/chat/__tests__/AttachmentGallery.test.tsx
  • desktop/src/apps/chat/__tests__/AttachmentsBar.test.tsx
  • desktop/src/apps/chat/__tests__/MessageHoverActions.test.tsx
  • desktop/src/apps/chat/__tests__/ThreadIndicator.test.tsx
  • desktop/src/lib/__tests__/chat-attachments-api.test.ts
  • desktop/src/lib/chat-attachments-api.ts
  • desktop/src/lib/use-thread-panel.ts
  • desktop/src/shell/FilePicker.tsx
  • desktop/src/shell/VfsBrowser.tsx
  • desktop/src/shell/__tests__/FilePicker.test.tsx

Fix these issues in Kilo Cloud


Reviewed by seed-2-0-pro-260328 · 204,884 tokens

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Note

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
desktop/src/apps/MessagesApp.tsx (1)

603-621: ⚠️ Potential issue | 🟠 Major

Successful slash POSTs still fall through to WebSocket send.

That was safe only while the POST was acting as validation. In this PR /help is handled in /api/chat/messages, so a 200 here can already have posted the system message before the same slash text is emitted over WS.

Suggested fix
     if (text.startsWith("/")) {
       try {
         const r = await fetch("/api/chat/messages", {
           method: "POST",
           headers: { "Content-Type": "application/json" },
           body: JSON.stringify({ channel_id: selectedChannel, content: text }),
         });
         if (r.status === 400) {
           const body = await r.json().catch(() => ({}));
           setSendError((body as { error?: string }).error || "couldn't send message");
           return;
         }
+        const body = await r.json().catch(() => ({}));
+        if ((body as { handled?: string }).handled) {
+          setSendError(null);
+          setInput("");
+          autoScrollRef.current = true;
+          if (inputRef.current) inputRef.current.style.height = "auto";
+          return;
+        }
       } catch {
         /* network error — fall through to WS send */
       }
     }
🤖 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 603 - 621, The current
slash-command flow posts to /api/chat/messages but always falls through to
wsRef.current.send, causing duplicate handling when the POST succeeded; update
the slash handling in MessagesApp (the block that calls fetch for
"/api/chat/messages" and then later calls wsRef.current.send) so that when the
POST returns a success status (e.g., 200) you treat the command as handled and
return early (do not call wsRef.current.send), keep the existing catch behavior
to fall through on network errors, and continue to use setSendError for 4xx
responses; ensure selectedChannel and setSendError are preserved in the
early-return path.
🟡 Minor comments (9)
static/desktop/assets/CalendarApp-DSaV9uPb.js-1-1 (1)

1-1: ⚠️ Potential issue | 🟡 Minor

Today uses a render-captured date, which can become stale.

On Line 1, y() reads s from render scope. If the window stays open across midnight, clicking Today can reset using yesterday’s date context. Use a fresh Date inside the handler.

💡 Proposed fix
- function y(){i(s.getFullYear()),a(s.getMonth())}
+ function y(){const e=new Date;i(e.getFullYear()),a(e.getMonth())}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@static/desktop/assets/CalendarApp-DSaV9uPb.js` at line 1, The Today button
handler y() captures the render-time Date stored in s, so if the app stays open
across midnight it may reset to the previous day; update y() in the CalendarApp
component (function T) to construct a fresh Date() inside the handler and then
call the state setters i(...) and a(...) with that new date's getFullYear() and
getMonth() instead of using the render-scoped s.
static/desktop/assets/MobileSplitView-CtNEF6zb.js-1-1 (1)

1-1: ⚠️ Potential issue | 🟡 Minor

Handle undefined selectedId in mobile selection logic.

Line 1 uses const w = l !== null, which treats undefined as “selected” and can open the detail pane unexpectedly on mobile. Use a nullish check instead.

💡 Suggested fix
-const w=l!==null;
+const w=l!=null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@static/desktop/assets/MobileSplitView-CtNEF6zb.js` at line 1, The mobile
selection boolean currently uses const w = l !== null inside function b (where l
is the selectedId), which treats undefined as a selected value; change that
check to a nullish check (e.g., const w = l != null) so both null and undefined
are considered "no selection" and the detail pane won't open unexpectedly on
mobile.
static/desktop/assets/RedditApp-BOuG46mh.js-1-1 (1)

1-1: ⚠️ Potential issue | 🟡 Minor

The thread detail keeps stale saved-state after a successful save.

ve() refreshes the library and then immediately looks up the saved item in the old v snapshot, so h can stay null until the thread is reopened. The detail view will keep showing the unsaved actions even after the ingest succeeds.

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

In `@static/desktop/assets/RedditApp-BOuG46mh.js` at line 1, The detail view uses
a stale v snapshot after saving because ve() calls await b() but then still
searches the old v; update b (the useCallback that fetches library items) to
return the fetched items (the t array) and change ve() to use the returned array
(e.g. const updated = await b()) and then find the saved item from updated
(updated.find(...)) before calling B(...), so the new saved item is picked from
the fresh data rather than the stale v; reference: ve, b, v, B, and i.post.url.
desktop/src/apps/chat/ThreadIndicator.tsx-11-13 (1)

11-13: ⚠️ Potential issue | 🟡 Minor

Use nullish check for lastReplyAt instead of truthy check.

Line 11 treats 0 as absent. Prefer an explicit null/undefined check so valid numeric timestamps are handled consistently.

Suggested fix
-  const label = lastReplyAt
+  const label = lastReplyAt != null
     ? `💬 ${replyCount} repl${replyCount === 1 ? "y" : "ies"} · last reply ${relative(lastReplyAt)}`
     : `💬 ${replyCount} repl${replyCount === 1 ? "y" : "ies"}`;
🤖 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 11 - 13, The
conditional for building label uses a truthy check on lastReplyAt which treats 0
as absent; update the ternary that assigns label to test explicitly for
null/undefined (e.g., lastReplyAt != null) so numeric timestamps like 0 are
treated as present while leaving replyCount and the relative(lastReplyAt) usage
unchanged; locate the label declaration in ThreadIndicator.tsx to update the
condition.
desktop/src/apps/chat/MessageHoverActions.tsx-16-18 (1)

16-18: ⚠️ Potential issue | 🟡 Minor

Set explicit button types to prevent accidental form submission.

Line 16–18 buttons currently default to type="submit". If this toolbar is ever rendered inside a form, clicks can trigger unintended submits.

Suggested 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={onMore} 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={onMore} 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 rendered in MessageHoverActions (the elements with onClick
handlers onReact, onReplyInThread, and onMore) lack an explicit type and can act
as type="submit" inside forms; update each button to include type="button" to
avoid accidental form submissions while keeping their existing aria-labels
onClick handlers and classes unchanged.
docs/chat-guide.md-317-320 (1)

317-320: ⚠️ Potential issue | 🟡 Minor

Add languages to these fenced code blocks.

markdownlint is already flagging both fences with MD040. text would fit the attachment footer example, and bash or text would fit the /help command examples.

Also applies to: 334-346

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

In `@docs/chat-guide.md` around lines 317 - 320, Add explicit fenced code block
languages for the examples that currently use plain triple-backticks: change the
attachment footer block containing "User attached: doc.pdf (application/pdf, 200
KB)..." to use ```text (or ```text+markdown) and change the /help command
example blocks to use ```bash (or ```text if non-shell) so markdownlint MD040 is
satisfied; look for the fenced blocks containing the attachment lines and the
blocks showing the `/help` command output and update their opening fences
accordingly.
desktop/src/shell/VfsBrowser.tsx-54-59 (1)

54-59: ⚠️ Potential issue | 🟡 Minor

Reset the browser state when root changes.

currentPath and selected survive a root prop change, so switching from one workspace root to another can reopen the new root inside a stale subdirectory and immediately 404. This shows up in the agent-workspace picker when the selected agent changes.

Suggested fix
 export function VfsBrowser({ root, onSelect, multi = false }: VfsBrowserProps) {
   const [currentPath, setCurrentPath] = useState("");
   const [entries, setEntries] = useState<VfsEntry[]>([]);
   const [selected, setSelected] = useState<Set<string>>(new Set());
   const [loading, setLoading] = useState(false);
   const [error, setError] = useState<string | null>(null);
+
+  useEffect(() => {
+    setCurrentPath("");
+    setSelected(new Set());
+    setError(null);
+  }, [root]);
 
   useEffect(() => {

Also applies to: 61-95

🤖 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 54 - 59, When the VfsBrowser's
root prop changes the component doesn't reset state, so currentPath and selected
(and related state) can point into the previous root; update VfsBrowser to reset
state when root changes by adding a useEffect that watches root and calls
setCurrentPath(""), setSelected(new Set()), setEntries([]) and setError(null)
(and optionally setLoading(false)) to clear stale state; reference the
VfsBrowser component and the state setters setCurrentPath, setSelected,
setEntries, setError, setLoading so you can locate and update the logic.
desktop/src/apps/MessagesApp.tsx-1335-1340 (1)

1335-1340: ⚠️ Potential issue | 🟡 Minor

The retry affordance is wired as a no-op.

Clicking Retry upload never re-runs the upload; it only replaces the error text. Either keep the original upload payload so this can actually retry, or hide the retry button until the behavior exists.

🤖 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 1335 - 1340, The Retry button
is currently a no-op—onRetry just replaces the error text instead of re-running
the upload. Fix by ensuring each pendingAttachments item retains the original
upload payload (e.g., file/blob under a property like file or payload) and
implement onRetry to set that item to uploading: true, clear error, and call the
existing upload routine (e.g., uploadAttachment(item) or the same function used
when initially adding attachments) so the upload is retried; if no upload
routine exists yet, remove or conditionally hide the retry affordance in
AttachmentsBar until retry behavior is implemented. Reference: AttachmentsBar,
pendingAttachments, setPendingAttachments, and the upload function used for
initial uploads.
docs/superpowers/plans/2026-04-19-chat-phase-2b-1-threads-attachments.md-1649-1663 (1)

1649-1663: ⚠️ Potential issue | 🟡 Minor

The agent-workspace example uses the wrong VFS root.

VfsBrowser only special-cases /workspaces/agent/<slug>, but this snippet uses /workspaces/${selectedAgent}. If someone copies it verbatim, it falls through to the user-workspace endpoint.

Suggested fix
-                <VfsBrowser root={`/workspaces/${selectedAgent}`} onSelect={onAgentWorkspacePick} multi={multi} />
+                <VfsBrowser root={`/workspaces/agent/${selectedAgent}`} onSelect={onAgentWorkspacePick} multi={multi} />
🤖 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 1649 - 1663, The VfsBrowser root is incorrect — it should use the
agent namespace so the component hits the agent-specific endpoint; change the
root prop on the VfsBrowser (where selectedAgent is used) from
`/workspaces/${selectedAgent}` to `/workspaces/agent/${selectedAgent}` so
VfsBrowser's special-case for `/workspaces/agent/<slug>` is exercised (leave
onAgentWorkspacePick and multi as-is).
🧹 Nitpick comments (3)
desktop/src/apps/chat/ThreadIndicator.tsx (1)

15-19: Set explicit button type for safety in form contexts.

This button should be type="button" to avoid accidental form submission if placement changes later.

Suggested fix
-    <button
+    <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"
🤖 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 (the JSX element using onClick={onOpen} and rendering
{label}) needs an explicit type to avoid accidental form submission; update the
<button> element to include type="button" (keeping existing props like
onClick={onOpen}, className, and aria-label) so it behaves safely if moved
inside a form.
desktop/src/apps/chat/AttachmentsBar.tsx (1)

34-36: Set explicit button types for action controls.

This prevents accidental submit behavior if the bar is ever rendered inside a <form>.

Proposed small hardening change
-            <button aria-label="Retry upload" onClick={() => onRetry(it.id)} className="text-red-300">retry</button>
+            <button type="button" aria-label="Retry upload" onClick={() => onRetry(it.id)} className="text-red-300">retry</button>
...
-          <button aria-label={`Remove ${it.filename}`} onClick={() => onRemove(it.id)} className="opacity-70 hover:opacity-100">×</button>
+          <button type="button" aria-label={`Remove ${it.filename}`} onClick={() => onRemove(it.id)} className="opacity-70 hover:opacity-100">×</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/chat/AttachmentsBar.tsx` around lines 34 - 36, The buttons
in AttachmentsBar (the retry and remove buttons rendering with onRetry(it.id)
and onRemove(it.id)) lack explicit types which can cause them to act as submit
buttons inside a form; update the JSX for those <button> elements to include
type="button" so they are explicit action controls and won't trigger form
submission.
desktop/src/apps/chat/AttachmentLightbox.tsx (1)

25-43: Strengthen dialog accessibility semantics.

role="dialog" is present, but aria-modal is missing; adding it improves screen-reader behavior for modal overlays.

Proposed accessibility tweak
   return (
     <div
       role="dialog"
       aria-label="Image viewer"
+      aria-modal="true"
       className="fixed inset-0 z-50 bg-black/80 flex items-center justify-center"
       onClick={onClose}
     >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/chat/AttachmentLightbox.tsx` around lines 25 - 43, The
dialog container div with role="dialog" in AttachmentLightbox is missing
aria-modal; update the outer div (the one using role="dialog", aria-label="Image
viewer", onClose handler and rendering current/images/idx) to include
aria-modal="true" (i.e., <div role="dialog" aria-modal="true" aria-label="Image
viewer" ...>) so screen readers treat it as a modal; keep existing
onClick/onClose and stopPropagation handlers intact.
🤖 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 28-42: Both useEffect blocks (the parent loader that calls
fetch(`/api/chat/messages/${parentId}`) and the thread messages loader that
calls fetch(`/api/chat/channels/${channelId}/threads/${parentId}/messages`)) can
fail silently and leak in-flight requests; update each to use an AbortController
to cancel the fetch on cleanup, check response.ok before calling r.json(), add
.catch handlers to handle network/JSON errors, and in error cases explicitly
clear or set a safe state (e.g., setParent(null) or setMsgs([])) and optionally
set an error flag to avoid stale UI and unhandled promise rejections instead of
leaving the existing setParent/setMsgs calls as-is.
- Around line 44-55: submit() currently clears the input before onSend completes
and may surface unhandled rejections from Enter key handling; change submit() to
await onSend(content, []) first, only clear input (setInput("")) after a
successful await, catch and handle errors (e.g., show an error state and
re-populate input) to avoid losing drafts, and return the send result; update
the local thread view by appending the returned message from onSend (or calling
a provided refresh/update callback) so replies appear immediately; also make
handleKeyDown await submit() (or call submit().catch(...)) so Enter-triggered
failures are not unhandled.

In `@desktop/src/apps/MessagesApp.tsx`:
- Around line 1497-1510: The onSend callback in MessagesApp.tsx currently POSTs
to "/api/chat/messages" but never checks the fetch Response, so failed 4xx/5xx
responses are treated as success and ThreadPanel doesn't get an exception to
preserve drafts or display errors; update the onSend handler (the async function
passed to onSend) to inspect the fetch Response (response.ok) and, if not ok,
read error details (e.g., response.text() or response.json()) and throw an Error
containing that information so ThreadPanel receives a thrown error and can
handle the failure appropriately.
- Around line 556-560: The sendMessage function currently returns early when
wsRef.current.readyState !== 1, which incorrectly blocks REST-backed flows
(e.g., pendingAttachments) — remove the global readyState guard and instead only
require wsRef.current.readyState === 1 for the text-only WebSocket fallback
path. Concretely: in sendMessage, keep the checks for input/pendingAttachments
and selectedChannel, but do not return based on wsRef.readyState; later, when
choosing the send path, branch so that attachment/HTTP flows call the REST
functions regardless of wsRef state and only the WS fallback path checks
wsRef.current.readyState before sending.

In `@desktop/src/lib/chat-attachments-api.ts`:
- Around line 16-23: The uploadDiskFile function returns raw server JSON which
uses content_type and omits source, breaking the AttachmentRecord shape; update
uploadDiskFile to parse the response JSON, map response.content_type to
mime_type (or set mime_type = response.content_type if present), ensure a source
field exists (e.g., source = "disk" or "upload") and any other required
AttachmentRecord properties are present/normalized, then return that normalized
object instead of raw r.json(); keep the function name uploadDiskFile and
preserve the existing fetch/_ensureOk flow.

In `@desktop/src/shell/FilePicker.tsx`:
- Around line 56-68: The issue is that onWorkspacePick and onAgentWorkspacePick
append the incoming full selection array to the existing queued state, causing
duplicates and preventing clean deselection; instead, when multi is true replace
queued with the new selections array (do not spread prev), i.e. setQueued should
set selections directly for multi-mode so the VfsBrowser-provided full selection
becomes authoritative; keep the existing single-select behavior (when multi is
false) and continue to include the selectedAgent slug in onAgentWorkspacePick.

In `@static/desktop/assets/AgentBrowsersApp-CFn8GY-5.js`:
- Line 1: ze() currently uses y() which expects JSON, so screenshot fetch always
falls back; change ze to perform a raw fetch that reads response.blob() (or
arrayBuffer -> base64) and returns an image URL/data URL instead of JSON so
P[s.id] becomes a usable src; update any callers (b and state setter ce that
stores P) to accept the returned URL/string and consider revoking object URLs
when a screenshot is replaced or the component unmounts.
- Line 1: The J callback replaces full profile objects with the small start/stop
API response (only id and status), corrupting profile shape and breaking status
checks; update J so when it receives l from Ce/Se it merges l onto the existing
profile instead of replacing it (e.g. in the h state update use
h(prev=>prev.map(H=>H.id===l.id?{...H,...l}:H))) and when updating the selected
profile (n) also merge l with the current selected profile rather than assigning
l directly so fields like profile_name, node and agent_name are preserved.

In `@static/desktop/assets/GitHubApp-IYMAlDty.js`:
- Line 1: The unauthenticated "Connect GitHub" buttons render with empty onClick
handlers (no-op) so users can't start auth; wire the two places that render the
CTA— the small connect button in the nav (the button rendered where
R.authenticated is false, currently onClick:()=>{}) and the banner button stored
in ne (button with onClick:()=>{}) — to actually start the GitHub auth flow (for
example by opening the app's GitHub auth endpoint or calling the existing auth
start endpoint) or remove the buttons; update those handlers to call the auth
start URL (window.open or navigate) and ensure aria-labels remain correct.
- Line 1: The sidebar controls (Watched, Content type S, and status filter pe)
are wired to state (X, S, pe) but never applied to the displayed list g and X is
never populated; fix by (1) populating X when the view is "watched" (call the
appropriate fetch in I or a new fetchWatched function and set X via set state X)
so Watched shows data, (2) update the useMemo that computes g (the variable g
declared with i.useMemo) to branch on S (repos/issues/prs/releases) and pe
(status) and filter the appropriate source array (J for repos, Y for
notifications/issues, X for watched, etc.) rather than always returning J/X/J,
and (3) ensure the effect that reacts to n (the view selector set via u)
triggers the correct fetch (I for starred, fetchWatched for watched, _ for
notifications) so toggling those sidebar buttons actually updates g; refer to
symbols X, J, Y, n, S, pe, I, _, and the useMemo that defines g.

In `@static/desktop/assets/ImportApp-DBAV17Xb.js`:
- Line 1: The embed call fails because ImportApp's upload flow (function T)
discards the filenames returned by /api/import/upload and the embed action
(function P) sends only {agent} instead of the required {agent_name, files}; fix
by capturing each upload response JSON in T (await response.json()), collect the
returned filenames into a new state (e.g., [uploadedFiles, setUploadedFiles]),
and append those filenames when all uploads finish; then change P to POST
JSON.stringify({agent_name: r, files: uploadedFiles}) and guard P to require
uploadedFiles.length>0; update references to functions T and P and state names i
(queued files) to wire the new uploadedFiles state.
- Line 1: The upload loop in T currently swallows errors and always increments
the progress and final success message; update the T function to check each
fetch response.ok (for the POST to "/api/import/upload"), treat non-ok and
caught exceptions as failures (do not increment the successful-count), collect
names/ids of successfully uploaded files (use the existing i array entries), and
compute progress based on total files but report and set state o(...) and u
accordingly to reflect successes vs failures; ensure h (uploading) is still
toggled off on completion and that the final message uses the actual successful
count (or lists failed filenames) rather than assuming all files succeeded.

In `@static/desktop/assets/SettingsApp-Bjcx0zeF.js`:
- Line 1: The restart dialog crashes because W's local state variable l can
exist while l.agents is undefined; change the computation of d (currently `const
d = l ? Object.entries(l.agents) : []`) to safely handle missing agents by using
a guard like checking l?.agents or falling back to an empty object before
calling Object.entries (i.e., compute d from Object.entries(l?.agents || {})),
update references in function W where d is used so the dialog no longer throws
when agents is absent.

---

Outside diff comments:
In `@desktop/src/apps/MessagesApp.tsx`:
- Around line 603-621: The current slash-command flow posts to
/api/chat/messages but always falls through to wsRef.current.send, causing
duplicate handling when the POST succeeded; update the slash handling in
MessagesApp (the block that calls fetch for "/api/chat/messages" and then later
calls wsRef.current.send) so that when the POST returns a success status (e.g.,
200) you treat the command as handled and return early (do not call
wsRef.current.send), keep the existing catch behavior to fall through on network
errors, and continue to use setSendError for 4xx responses; ensure
selectedChannel and setSendError are preserved in the early-return path.

---

Minor comments:
In `@desktop/src/apps/chat/MessageHoverActions.tsx`:
- Around line 16-18: The three toolbar buttons rendered in MessageHoverActions
(the elements with onClick handlers onReact, onReplyInThread, and onMore) lack
an explicit type and can act as type="submit" inside forms; update each button
to include type="button" to avoid accidental form submissions while keeping
their existing aria-labels onClick handlers and classes unchanged.

In `@desktop/src/apps/chat/ThreadIndicator.tsx`:
- Around line 11-13: The conditional for building label uses a truthy check on
lastReplyAt which treats 0 as absent; update the ternary that assigns label to
test explicitly for null/undefined (e.g., lastReplyAt != null) so numeric
timestamps like 0 are treated as present while leaving replyCount and the
relative(lastReplyAt) usage unchanged; locate the label declaration in
ThreadIndicator.tsx to update the condition.

In `@desktop/src/apps/MessagesApp.tsx`:
- Around line 1335-1340: The Retry button is currently a no-op—onRetry just
replaces the error text instead of re-running the upload. Fix by ensuring each
pendingAttachments item retains the original upload payload (e.g., file/blob
under a property like file or payload) and implement onRetry to set that item to
uploading: true, clear error, and call the existing upload routine (e.g.,
uploadAttachment(item) or the same function used when initially adding
attachments) so the upload is retried; if no upload routine exists yet, remove
or conditionally hide the retry affordance in AttachmentsBar until retry
behavior is implemented. Reference: AttachmentsBar, pendingAttachments,
setPendingAttachments, and the upload function used for initial uploads.

In `@desktop/src/shell/VfsBrowser.tsx`:
- Around line 54-59: When the VfsBrowser's root prop changes the component
doesn't reset state, so currentPath and selected (and related state) can point
into the previous root; update VfsBrowser to reset state when root changes by
adding a useEffect that watches root and calls setCurrentPath(""),
setSelected(new Set()), setEntries([]) and setError(null) (and optionally
setLoading(false)) to clear stale state; reference the VfsBrowser component and
the state setters setCurrentPath, setSelected, setEntries, setError, setLoading
so you can locate and update the logic.

In `@docs/chat-guide.md`:
- Around line 317-320: Add explicit fenced code block languages for the examples
that currently use plain triple-backticks: change the attachment footer block
containing "User attached: doc.pdf (application/pdf, 200 KB)..." to use ```text
(or ```text+markdown) and change the /help command example blocks to use ```bash
(or ```text if non-shell) so markdownlint MD040 is satisfied; look for the
fenced blocks containing the attachment lines and the blocks showing the `/help`
command output and update their opening fences accordingly.

In `@docs/superpowers/plans/2026-04-19-chat-phase-2b-1-threads-attachments.md`:
- Around line 1649-1663: The VfsBrowser root is incorrect — it should use the
agent namespace so the component hits the agent-specific endpoint; change the
root prop on the VfsBrowser (where selectedAgent is used) from
`/workspaces/${selectedAgent}` to `/workspaces/agent/${selectedAgent}` so
VfsBrowser's special-case for `/workspaces/agent/<slug>` is exercised (leave
onAgentWorkspacePick and multi as-is).

In `@static/desktop/assets/CalendarApp-DSaV9uPb.js`:
- Line 1: The Today button handler y() captures the render-time Date stored in
s, so if the app stays open across midnight it may reset to the previous day;
update y() in the CalendarApp component (function T) to construct a fresh Date()
inside the handler and then call the state setters i(...) and a(...) with that
new date's getFullYear() and getMonth() instead of using the render-scoped s.

In `@static/desktop/assets/MobileSplitView-CtNEF6zb.js`:
- Line 1: The mobile selection boolean currently uses const w = l !== null
inside function b (where l is the selectedId), which treats undefined as a
selected value; change that check to a nullish check (e.g., const w = l != null)
so both null and undefined are considered "no selection" and the detail pane
won't open unexpectedly on mobile.

In `@static/desktop/assets/RedditApp-BOuG46mh.js`:
- Line 1: The detail view uses a stale v snapshot after saving because ve()
calls await b() but then still searches the old v; update b (the useCallback
that fetches library items) to return the fetched items (the t array) and change
ve() to use the returned array (e.g. const updated = await b()) and then find
the saved item from updated (updated.find(...)) before calling B(...), so the
new saved item is picked from the fresh data rather than the stale v; reference:
ve, b, v, B, and i.post.url.

---

Nitpick comments:
In `@desktop/src/apps/chat/AttachmentLightbox.tsx`:
- Around line 25-43: The dialog container div with role="dialog" in
AttachmentLightbox is missing aria-modal; update the outer div (the one using
role="dialog", aria-label="Image viewer", onClose handler and rendering
current/images/idx) to include aria-modal="true" (i.e., <div role="dialog"
aria-modal="true" aria-label="Image viewer" ...>) so screen readers treat it as
a modal; keep existing onClick/onClose and stopPropagation handlers intact.

In `@desktop/src/apps/chat/AttachmentsBar.tsx`:
- Around line 34-36: The buttons in AttachmentsBar (the retry and remove buttons
rendering with onRetry(it.id) and onRemove(it.id)) lack explicit types which can
cause them to act as submit buttons inside a form; update the JSX for those
<button> elements to include type="button" so they are explicit action controls
and won't trigger form submission.

In `@desktop/src/apps/chat/ThreadIndicator.tsx`:
- Around line 15-19: The button in ThreadIndicator.tsx (the JSX element using
onClick={onOpen} and rendering {label}) needs an explicit type to avoid
accidental form submission; update the <button> element to include type="button"
(keeping existing props like onClick={onOpen}, className, and aria-label) so it
behaves safely if moved inside a form.
🪄 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: 6167ff11-caa3-44ad-9568-dc679df95c2e

📥 Commits

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

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

Comment on lines +28 to +42
useEffect(() => {
let alive = true;
fetch(`/api/chat/messages/${parentId}`)
.then((r) => (r.ok ? r.json() : null))
.then((d) => { if (alive) setParent(d); });
return () => { alive = false; };
}, [parentId]);

useEffect(() => {
let alive = true;
fetch(`/api/chat/channels/${channelId}/threads/${parentId}/messages`)
.then((r) => r.json())
.then((d) => { if (alive) setMsgs(d.messages || []); });
return () => { alive = false; };
}, [channelId, parentId]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Handle thread-loading failures explicitly (and cancel in-flight requests).

Both loaders can fail silently right now; network/JSON failures leave stale UI and can emit unhandled async errors.

Proposed resilient fetch pattern
   useEffect(() => {
-    let alive = true;
-    fetch(`/api/chat/messages/${parentId}`)
-      .then((r) => (r.ok ? r.json() : null))
-      .then((d) => { if (alive) setParent(d); });
-    return () => { alive = false; };
+    const ac = new AbortController();
+    (async () => {
+      try {
+        const r = await fetch(`/api/chat/messages/${parentId}`, { signal: ac.signal });
+        if (!r.ok) {
+          setParent(null);
+          return;
+        }
+        const d = await r.json();
+        setParent(d);
+      } catch {
+        if (!ac.signal.aborted) setParent(null);
+      }
+    })();
+    return () => ac.abort();
   }, [parentId]);

   useEffect(() => {
-    let alive = true;
-    fetch(`/api/chat/channels/${channelId}/threads/${parentId}/messages`)
-      .then((r) => r.json())
-      .then((d) => { if (alive) setMsgs(d.messages || []); });
-    return () => { alive = false; };
+    const ac = new AbortController();
+    (async () => {
+      try {
+        const r = await fetch(
+          `/api/chat/channels/${channelId}/threads/${parentId}/messages`,
+          { signal: ac.signal },
+        );
+        if (!r.ok) {
+          setMsgs([]);
+          return;
+        }
+        const d = await r.json();
+        setMsgs(d.messages || []);
+      } catch {
+        if (!ac.signal.aborted) setMsgs([]);
+      }
+    })();
+    return () => ac.abort();
   }, [channelId, parentId]);
📝 Committable suggestion

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

Suggested change
useEffect(() => {
let alive = true;
fetch(`/api/chat/messages/${parentId}`)
.then((r) => (r.ok ? r.json() : null))
.then((d) => { if (alive) setParent(d); });
return () => { alive = false; };
}, [parentId]);
useEffect(() => {
let alive = true;
fetch(`/api/chat/channels/${channelId}/threads/${parentId}/messages`)
.then((r) => r.json())
.then((d) => { if (alive) setMsgs(d.messages || []); });
return () => { alive = false; };
}, [channelId, parentId]);
useEffect(() => {
const ac = new AbortController();
(async () => {
try {
const r = await fetch(`/api/chat/messages/${parentId}`, { signal: ac.signal });
if (!r.ok) {
setParent(null);
return;
}
const d = await r.json();
setParent(d);
} catch {
if (!ac.signal.aborted) setParent(null);
}
})();
return () => ac.abort();
}, [parentId]);
useEffect(() => {
const ac = new AbortController();
(async () => {
try {
const r = await fetch(
`/api/chat/channels/${channelId}/threads/${parentId}/messages`,
{ signal: ac.signal },
);
if (!r.ok) {
setMsgs([]);
return;
}
const d = await r.json();
setMsgs(d.messages || []);
} catch {
if (!ac.signal.aborted) setMsgs([]);
}
})();
return () => ac.abort();
}, [channelId, parentId]);
🤖 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 28 - 42, Both useEffect
blocks (the parent loader that calls fetch(`/api/chat/messages/${parentId}`) and
the thread messages loader that calls
fetch(`/api/chat/channels/${channelId}/threads/${parentId}/messages`)) can fail
silently and leak in-flight requests; update each to use an AbortController to
cancel the fetch on cleanup, check response.ok before calling r.json(), add
.catch handlers to handle network/JSON errors, and in error cases explicitly
clear or set a safe state (e.g., setParent(null) or setMsgs([])) and optionally
set an error flag to avoid stale UI and unhandled promise rejections instead of
leaving the existing setParent/setMsgs calls as-is.

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

function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
submit();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

submit() currently loses drafts on failure and doesn’t refresh the thread view.

Input is cleared before onSend resolves, and Enter-triggered async failures can surface as unhandled promise rejections. Also, replies are not reflected locally after send.

Proposed submit flow fix
+  const [sending, setSending] = useState(false);

   async function submit() {
     const content = input.trim();
-    if (!content) return;
-    setInput("");
-    await onSend(content, []);
+    if (!content || sending) return;
+    try {
+      setSending(true);
+      await onSend(content, []);
+      setInput("");
+      const r = await fetch(`/api/chat/channels/${channelId}/threads/${parentId}/messages`);
+      if (r.ok) {
+        const d = await r.json();
+        setMsgs(d.messages || []);
+      }
+    } catch {
+      // preserve draft on failure
+      setInput(content);
+    } finally {
+      setSending(false);
+    }
   }

   function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
     if (e.key === "Enter" && !e.shiftKey) {
       e.preventDefault();
-      submit();
+      void submit();
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/chat/ThreadPanel.tsx` around lines 44 - 55, submit()
currently clears the input before onSend completes and may surface unhandled
rejections from Enter key handling; change submit() to await onSend(content, [])
first, only clear input (setInput("")) after a successful await, catch and
handle errors (e.g., show an error state and re-populate input) to avoid losing
drafts, and return the send result; update the local thread view by appending
the returned message from onSend (or calling a provided refresh/update callback)
so replies appear immediately; also make handleKeyDown await submit() (or call
submit().catch(...)) so Enter-triggered failures are not unhandled.

Comment on lines 556 to +560
const sendMessage = async () => {
const text = input.trim();
if (!text || !selectedChannel || !wsRef.current || wsRef.current.readyState !== 1) return;
if (!text && pendingAttachments.length === 0) return;
if (!selectedChannel || !wsRef.current || wsRef.current.readyState !== 1) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't gate the HTTP send paths on WebSocket state.

This early return blocks attachment sends and other REST-backed flows whenever the socket drops, even though they do not need WS. Only the final text-only WS fallback should require readyState === 1.

Suggested fix
   const sendMessage = async () => {
     const text = input.trim();
     if (!text && pendingAttachments.length === 0) return;
-    if (!selectedChannel || !wsRef.current || wsRef.current.readyState !== 1) return;
+    if (!selectedChannel) return;
 
     // Block send while uploads are in-flight
     if (pendingAttachments.some((a) => a.uploading)) {
       setSendError("waiting for uploads to finish…");
       return;
@@
-    if (!text) return;
+    if (!text) return;
+    if (!wsRef.current || wsRef.current.readyState !== 1) return;
🤖 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 556 - 560, The sendMessage
function currently returns early when wsRef.current.readyState !== 1, which
incorrectly blocks REST-backed flows (e.g., pendingAttachments) — remove the
global readyState guard and instead only require wsRef.current.readyState === 1
for the text-only WebSocket fallback path. Concretely: in sendMessage, keep the
checks for input/pendingAttachments and selectedChannel, but do not return based
on wsRef.readyState; later, when choosing the send path, branch so that
attachment/HTTP flows call the REST functions regardless of wsRef state and only
the WS fallback path checks wsRef.current.readyState before sending.

Comment on lines +1497 to +1510
onSend={async (content, attachments) => {
await fetch("/api/chat/messages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
channel_id: openThread.channelId,
author_id: "user",
author_type: "user",
content,
content_type: "text",
thread_id: openThread.parentId,
attachments,
}),
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Surface failed thread sends back to ThreadPanel.

ThreadPanel relies on onSend throwing when a send fails, but this callback never checks response.ok. Right now a 4xx/5xx reply is treated as success and the panel cannot preserve the draft or show the error.

Suggested fix
         <ThreadPanel
           channelId={openThread.channelId}
           parentId={openThread.parentId}
           onClose={closeThread}
           onSend={async (content, attachments) => {
-            await fetch("/api/chat/messages", {
+            const r = await fetch("/api/chat/messages", {
               method: "POST",
               headers: { "Content-Type": "application/json" },
               body: JSON.stringify({
                 channel_id: openThread.channelId,
                 author_id: "user",
@@
                 attachments,
               }),
             });
+            if (!r.ok) {
+              const body = await r.json().catch(() => ({}));
+              throw new Error((body as { error?: string }).error || "send failed");
+            }
           }}
         />
🤖 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 1497 - 1510, The onSend
callback in MessagesApp.tsx currently POSTs to "/api/chat/messages" but never
checks the fetch Response, so failed 4xx/5xx responses are treated as success
and ThreadPanel doesn't get an exception to preserve drafts or display errors;
update the onSend handler (the async function passed to onSend) to inspect the
fetch Response (response.ok) and, if not ok, read error details (e.g.,
response.text() or response.json()) and throw an Error containing that
information so ThreadPanel receives a thrown error and can handle the failure
appropriately.

Comment on lines +16 to +23
export async function uploadDiskFile(file: File, channelId?: string): Promise<AttachmentRecord> {
const form = new FormData();
form.append("file", file);
if (channelId) form.append("channel_id", channelId);
const r = await fetch("/api/chat/upload", { method: "POST", body: form });
await _ensureOk(r);
return r.json();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Normalize /api/chat/upload response before returning AttachmentRecord.

uploadDiskFile currently returns raw server JSON, but the upload endpoint returns content_type and no source. This violates AttachmentRecord and can break image rendering logic that depends on mime_type.

Proposed response normalization
 export async function uploadDiskFile(file: File, channelId?: string): Promise<AttachmentRecord> {
   const form = new FormData();
   form.append("file", file);
   if (channelId) form.append("channel_id", channelId);
   const r = await fetch("/api/chat/upload", { method: "POST", body: form });
   await _ensureOk(r);
-  return r.json();
+  const body = await r.json() as {
+    filename: string;
+    content_type?: string;
+    mime_type?: string;
+    size: number;
+    url: string;
+    source?: "disk" | "workspace" | "agent-workspace";
+  };
+  return {
+    filename: body.filename,
+    mime_type: body.mime_type ?? body.content_type ?? "application/octet-stream",
+    size: body.size,
+    url: body.url,
+    source: body.source ?? "disk",
+  };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/lib/chat-attachments-api.ts` around lines 16 - 23, The
uploadDiskFile function returns raw server JSON which uses content_type and
omits source, breaking the AttachmentRecord shape; update uploadDiskFile to
parse the response JSON, map response.content_type to mime_type (or set
mime_type = response.content_type if present), ensure a source field exists
(e.g., source = "disk" or "upload") and any other required AttachmentRecord
properties are present/normalized, then return that normalized object instead of
raw r.json(); keep the function name uploadDiskFile and preserve the existing
fetch/_ensureOk flow.

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

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Screenshot loading is wired to JSON, but the endpoint returns PNG bytes.

On Line 1, ze() calls the screenshot endpoint via y(), which only accepts JSON. /api/agent-browsers/profiles/{profile_id}/screenshot returns image/png, so this path always falls back and P[s.id] never gets a usable image.

Suggested fix
- async function ze(a){return(await y(`/api/agent-browsers/profiles/${encodeURIComponent(a)}/screenshot`,{})).data??null}
+ async function ze(a){
+   try{
+     const res = await fetch(`/api/agent-browsers/profiles/${encodeURIComponent(a)}/screenshot`);
+     if(!res.ok) return null;
+     const blob = await res.blob();
+     return URL.createObjectURL(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, ze() currently
uses y() which expects JSON, so screenshot fetch always falls back; change ze to
perform a raw fetch that reads response.blob() (or arrayBuffer -> base64) and
returns an image URL/data URL instead of JSON so P[s.id] becomes a usable src;
update any callers (b and state setter ce that stores P) to accept the returned
URL/string and consider revoking object URLs when a screenshot is replaced or
the component unmounts.

⚠️ Potential issue | 🔴 Critical

Start/stop updates overwrite profile shape and break status logic.

On Line 1, J() replaces a full profile with start/stop API responses ({status:"started"|"stopped", id}), but UI expects full profile fields (profile_name, node, agent_name) and checks for status === "running". This causes state corruption and incorrect button behavior after toggling.

Suggested fix
- 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)))
+ const actionRes = t.status==="running" ? await Se(t.id) : await Ce(t.id);
+ if(actionRes?.id){
+   const refreshed = await fetch(`/api/agent-browsers/profiles/${encodeURIComponent(actionRes.id)}`, { headers:{Accept:"application/json"} })
+     .then(r => r.ok ? r.json() : null)
+     .catch(() => null);
+   if(refreshed){
+     h(u=>u.map(H=>H.id===refreshed.id ? refreshed : H));
+     if((s==null?void 0:s.id)===refreshed.id){
+       n(refreshed);
+       if(refreshed.status==="running") b(refreshed.id);
+     }
+   }
+ }
🤖 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 J callback
replaces full profile objects with the small start/stop API response (only id
and status), corrupting profile shape and breaking status checks; update J so
when it receives l from Ce/Se it merges l onto the existing profile instead of
replacing it (e.g. in the h state update use
h(prev=>prev.map(H=>H.id===l.id?{...H,...l}:H))) and when updating the selected
profile (n) also merge l with the current selected profile rather than assigning
l directly so fields like profile_name, node and agent_name are preserved.

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

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The unauthenticated “Connect GitHub” actions are no-ops.

Both CTAs render, but neither has a real navigation or handler, so an unauthenticated user cannot actually start the connection flow from this screen. Either wire them to the intended auth path or remove the affordance until it exists.

🤖 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 unauthenticated
"Connect GitHub" buttons render with empty onClick handlers (no-op) so users
can't start auth; wire the two places that render the CTA— the small connect
button in the nav (the button rendered where R.authenticated is false, currently
onClick:()=>{}) and the banner button stored in ne (button with onClick:()=>{})
— to actually start the GitHub auth flow (for example by opening the app's
GitHub auth endpoint or calling the existing auth start endpoint) or remove the
buttons; update those handlers to call the auth start URL (window.open or
navigate) and ensure aria-labels remain correct.

⚠️ Potential issue | 🟠 Major

Several sidebar filters are wired to state but never affect the data.

Watched reads from X, which is never populated, and the Repos / Issues / Pull Requests / Releases plus status filters never feed into g or any fetch path. Users can toggle these controls, but the list still shows the same starred repos or an always-empty watched view.

🤖 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 sidebar controls
(Watched, Content type S, and status filter pe) are wired to state (X, S, pe)
but never applied to the displayed list g and X is never populated; fix by (1)
populating X when the view is "watched" (call the appropriate fetch in I or a
new fetchWatched function and set X via set state X) so Watched shows data, (2)
update the useMemo that computes g (the variable g declared with i.useMemo) to
branch on S (repos/issues/prs/releases) and pe (status) and filter the
appropriate source array (J for repos, Y for notifications/issues, X for
watched, etc.) rather than always returning J/X/J, and (3) ensure the effect
that reacts to n (the view selector set via u) triggers the correct fetch (I for
starred, fetchWatched for watched, _ for notifications) so toggling those
sidebar buttons actually updates g; refer to symbols X, J, Y, n, S, pe, I, _,
and the useMemo that defines g.

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

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Embed requests cannot succeed with the current payload.

/api/import/embed requires agent_name and files, but this client sends only { agent }. On top of that, /api/import/upload returns the filenames that need to be passed into the embed call, and those are discarded here. As written, the Embed action will 400 every time against tinyagentos/routes/import_data.py:45-102.

Proposed fix
- await fetch("/api/import/embed", {
-   method: "POST",
-   headers: { "Content-Type": "application/json" },
-   body: JSON.stringify({ agent: selectedAgent }),
- })
+ await fetch("/api/import/embed", {
+   method: "POST",
+   headers: { "Content-Type": "application/json" },
+   body: JSON.stringify({
+     agent_name: selectedAgent,
+     files: uploadedFilenames,
+   }),
+ })
🤖 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 embed call fails
because ImportApp's upload flow (function T) discards the filenames returned by
/api/import/upload and the embed action (function P) sends only {agent} instead
of the required {agent_name, files}; fix by capturing each upload response JSON
in T (await response.json()), collect the returned filenames into a new state
(e.g., [uploadedFiles, setUploadedFiles]), and append those filenames when all
uploads finish; then change P to POST JSON.stringify({agent_name: r, files:
uploadedFiles}) and guard P to require uploadedFiles.length>0; update references
to functions T and P and state names i (queued files) to wire the new
uploadedFiles state.

⚠️ Potential issue | 🟠 Major

Upload status is reported as success even when the server rejects a file.

The loop swallows errors, never checks response.ok, and still advances progress plus the final “Uploaded N files” message. A partial or total failure will look successful to the user, and you still won’t have the authoritative filenames needed for embedding.

Proposed fix
- try {
-   await fetch("/api/import/upload", { method: "POST", body: formData })
- } catch {}
- uploadedCount += 1
+ const res = await fetch("/api/import/upload", { method: "POST", body: formData })
+ if (!res.ok) {
+   const err = await res.json().catch(() => ({}))
+   throw new Error(err.error ?? `Upload failed (${res.status})`)
+ }
+ const body = await res.json()
+ uploadedFiles.push(body.filename)
+ uploadedCount += 1
🤖 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 T
currently swallows errors and always increments the progress and final success
message; update the T function to check each fetch response.ok (for the POST to
"/api/import/upload"), treat non-ok and caught exceptions as failures (do not
increment the successful-count), collect names/ids of successfully uploaded
files (use the existing i array entries), and compute progress based on total
files but report and set state o(...) and u accordingly to reflect successes vs
failures; ensure h (uploading) is still toggled off on completion and that the
final message uses the actual successful count (or lists failed filenames)
rather than assuming all files succeeded.

@@ -0,0 +1 @@
import{r as a,j as e}from"./vendor-react-l6srOxy7.js";import{I as O,B as o}from"./toolbar-UW6q5pkx.js";import{l as Te,i as _e,f as Ae}from"./knowledge-ES9kK4zW.js";import{M as $e}from"./MobileSplitView-CtNEF6zb.js";import{u as Me}from"./use-is-mobile-v5lglusa.js";import{a3 as W,g as re,b as le,aR as ie,f as A,S as Ee,a9 as q,$ as Pe,ay as ne,y as De,k as de,D as Fe,aG as Be}from"./vendor-icons-wm645Jsx.js";import"./vendor-radix-BhM7AEEG.js";import"./vendor-layout-B-pp9n1f.js";const K={posts:[],after:null};async function M(s,r,l){try{const i=await fetch(s,{...l,headers:{Accept:"application/json",...l==null?void 0:l.headers}});return!i.ok||!(i.headers.get("content-type")??"").includes("application/json")?r:await i.json()}catch{return r}}async function Ue(s){const r=new URLSearchParams({url:s});try{const l=await fetch(`/api/reddit/thread?${r}`,{headers:{Accept:"application/json"}});return!l.ok||!(l.headers.get("content-type")??"").includes("application/json")?null:await l.json()}catch{return null}}async function Oe(s,r="hot",l){const i=new URLSearchParams({name:s,sort:r}),x=await M(`/api/reddit/subreddit?${i}`,{...K});return{posts:Array.isArray(x.posts)?x.posts:[],after:x.after??null}}async function We(s,r){const l=new URLSearchParams({q:s});r&&l.set("subreddit",r);const i=await M(`/api/reddit/search?${l}`,{...K});return{posts:Array.isArray(i.posts)?i.posts:[],after:i.after??null}}async function qe(s){const l=new URLSearchParams().toString(),i=await M(`/api/reddit/saved${l?`?${l}`:""}`,{...K});return{posts:Array.isArray(i.posts)?i.posts:[],after:i.after??null}}async function Ke(){return M("/api/reddit/auth/status",{authenticated:!1})}async function ce(s,r){try{const l=await fetch("/api/knowledge/ingest",{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify({url:s,title:r??"",text:"",categories:[],source:"reddit-client"})});return!l.ok||!(l.headers.get("content-type")??"").includes("application/json")?null:await l.json()}catch{return null}}const Ve=["LocalLLaMA","selfhosted","homelab","linux"];function V(s){const r=Date.now()/1e3-s;return r<60?"just now":r<3600?`${Math.floor(r/60)}m ago`:r<86400?`${Math.floor(r/3600)}h ago`:r<604800?`${Math.floor(r/86400)}d ago`:new Date(s*1e3).toLocaleDateString()}function $(s){return s>=1e3?`${(s/1e3).toFixed(1)}k`:String(s)}function Ge(s){return s.replace(/^https?:\/\/(www\.)?reddit\.com/,"")}function oe({comment:s,maxDepth:r=4}){const[l,i]=a.useState(!1),[x,u]=a.useState(s.depth<r),j=s.author==="[deleted]"||s.body==="[deleted]",d=()=>i(m=>!m);return e.jsxs("li",{role:"listitem",className:"text-sm",style:{marginLeft:s.depth>0?"2rem":0},children:[e.jsxs("div",{className:"flex items-center gap-2 py-0.5",children:[e.jsx("button",{"aria-label":l?"Expand comment":"Collapse comment","aria-expanded":!l,onClick:d,className:"text-shell-text-tertiary hover:text-shell-text shrink-0",children:l?e.jsx(Fe,{size:13}):e.jsx(Be,{size:13})}),j?e.jsx("span",{className:"text-shell-text-tertiary italic text-xs",children:"[deleted]"}):e.jsxs(e.Fragment,{children:[e.jsxs("span",{className:"font-semibold text-xs text-shell-text",children:["u/",s.author]}),s.distinguished==="moderator"&&e.jsx("span",{className:"text-[10px] text-green-400 font-semibold",children:"MOD"}),e.jsxs("span",{className:"text-shell-text-tertiary text-xs",children:[$(s.score)," pts"]}),e.jsx("span",{className:"text-shell-text-tertiary text-xs",children:"·"}),e.jsx("span",{className:"text-shell-text-tertiary text-xs",children:V(s.created_utc)}),s.edited&&e.jsx("span",{className:"text-shell-text-tertiary text-xs italic",children:"(edited)"})]})]}),!l&&!j&&e.jsx("p",{className:"text-shell-text-secondary text-xs whitespace-pre-wrap mt-0.5 ml-5 pb-1 leading-relaxed",children:s.body}),!l&&s.replies.length>0&&e.jsx("div",{className:"border-l border-white/5 ml-5 mt-0.5",children:x||s.depth<r?e.jsx("ul",{role:"list",className:"space-y-1",children:s.replies.map(m=>e.jsx(oe,{comment:m,maxDepth:r},m.id))}):e.jsxs("button",{className:"text-xs text-accent hover:underline ml-3 py-0.5",onClick:()=>u(!0),"aria-label":`Show ${s.replies.length} more replies`,children:["Show ",s.replies.length," more ",s.replies.length===1?"reply":"replies"]})})]})}function Je({post:s,savedItem:r,onOpen:l,onSave:i,saving:x}){const u=!!r,j=d=>{(d.key==="Enter"||d.key===" ")&&(d.preventDefault(),l(s))};return e.jsxs("div",{className:"border border-white/5 rounded-lg p-3 bg-shell-surface/30 hover:bg-shell-surface/50 transition-colors",role:"article",children:[e.jsx("div",{className:"flex items-start gap-2 mb-1",children:e.jsx("div",{className:"flex-1 min-w-0",children:e.jsx("button",{className:"text-left text-sm font-medium text-shell-text hover:text-accent transition-colors leading-snug cursor-pointer",onClick:()=>l(s),onKeyDown:j,tabIndex:0,"aria-label":`Open thread: ${s.title}`,children:s.title})})}),e.jsxs("div",{className:"flex items-center gap-2 flex-wrap mb-1.5",children:[e.jsxs("span",{className:"text-[11px] font-semibold px-1.5 py-0.5 rounded-full bg-orange-500/20 text-orange-400 border border-orange-500/30",children:["r/",s.subreddit]}),e.jsxs("span",{className:"text-xs text-shell-text-tertiary",children:["u/",s.author]}),e.jsx("span",{className:"text-shell-text-tertiary text-xs",children:"·"}),e.jsxs("span",{className:"text-xs text-shell-text-tertiary",children:[$(s.score)," pts"]}),e.jsx("span",{className:"text-shell-text-tertiary text-xs",children:"·"}),e.jsxs("span",{className:"text-xs text-shell-text-tertiary",children:[s.num_comments," comments"]}),e.jsx("span",{className:"text-shell-text-tertiary text-xs",children:"·"}),e.jsx("span",{className:"text-xs text-shell-text-tertiary",children:V(s.created_utc)}),s.flair&&e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"text-shell-text-tertiary text-xs",children:"·"}),e.jsx("span",{className:"text-[11px] px-1.5 py-0.5 rounded bg-white/5 text-shell-text-tertiary border border-white/10",children:s.flair})]})]}),s.is_self&&s.selftext&&e.jsx("p",{className:"text-xs text-shell-text-secondary line-clamp-2 mb-2 leading-relaxed",children:s.selftext}),u&&r.categories.length>0&&e.jsx("div",{className:"flex flex-wrap gap-1 mb-2",children:r.categories.map(d=>e.jsx("span",{className:"text-[10px] px-1.5 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20",children:d},d))}),e.jsx("div",{className:"flex items-center gap-2 mt-1",children:e.jsx(o,{variant:u?"secondary":"ghost",size:"sm",className:"h-6 text-xs gap-1 px-2",onClick:()=>i(s),disabled:x||u,"aria-label":u?"Saved to Library":"Save to Library",children:u?e.jsxs(e.Fragment,{children:[e.jsx(A,{size:11}),"Saved"]}):e.jsxs(e.Fragment,{children:[e.jsx(de,{size:11}),x?"Saving…":"Save to Library"]})})})]})}function rt({windowId:s}){const[r,l]=a.useState("feed"),[i,x]=a.useState(null),[u,j]=a.useState(!1),[d,m]=a.useState(null),[C,xe]=a.useState(Ve),[G,N]=a.useState(!1),[z,E]=a.useState(""),[f,y]=a.useState("subreddits"),[P,J]=a.useState({posts:[],after:null}),[L,Q]=a.useState(!1),[R,he]=a.useState("hot"),[I,D]=a.useState(""),[H,F]=a.useState(""),[g,pe]=a.useState({authenticated:!1}),[v,ue]=a.useState([]),[me,Y]=a.useState(null),[S,X]=a.useState("comments"),[h,B]=a.useState(null),[Z,ee]=a.useState(!1),[fe,w]=a.useState(!1),[Qe,ge]=a.useState(!1),k=Me(),be=a.useRef(null);a.useEffect(()=>{Ke().then(pe),b()},[]);const b=a.useCallback(async()=>{const{items:t}=await Te({source_type:"reddit",limit:200});ue(t)},[]),te=a.useCallback(async(t,n,c)=>{Q(!0);try{let p;c.trim()?p=await We(c.trim(),t??void 0):f==="saved"&&g.authenticated?p=await qe():t?p=await Oe(t,n):p={posts:[],after:null},J(p)}catch{J({posts:[],after:null})}Q(!1)},[f,g.authenticated]);a.useEffect(()=>{r==="feed"&&te(d,R,I)},[d,R,I,r,te]);const je=a.useCallback(async t=>{var p;l("thread"),X("comments"),x(null),w(!1),j(!0);const n=await Ue(t.url);x(n),j(!1);const c=v.find(ae=>ae.source_url===t.url||ae.source_url===`https://www.reddit.com${t.permalink}`);B(c??null),ge(c?(((p=c.monitor)==null?void 0:p.current_interval)??0)>0:!1)},[v]),U=a.useCallback(()=>{l("feed"),x(null),w(!1)},[]);a.useEffect(()=>{const t=n=>{n.key==="Escape"&&r==="thread"&&U()};return window.addEventListener("keydown",t),()=>window.removeEventListener("keydown",t)},[r,U]);const ye=a.useCallback(async t=>{Y(t.id),await ce(t.url,t.title),await b(),Y(null)},[b]),ve=a.useCallback(async()=>{if(!i)return;if(ee(!0),await ce(i.post.url,i.post.title)){await b();const n=v.find(c=>c.source_url===i.post.url);B(n??null)}ee(!1)},[i,v,b]),Ne=a.useCallback(async()=>{h&&(await _e(h.source_url,{title:h.title,categories:h.categories}),await b())},[h,b]),we=a.useCallback(async()=>{h&&(await Ae(h.id),B(null),w(!1),await b())},[h,b]),T=a.useCallback(()=>{const t=z.trim().replace(/^r\//,"");t&&!C.includes(t)&&(xe(n=>[...n,t]),m(t),y("subreddits")),E(""),N(!1)},[z,C]),Se=a.useCallback(t=>v.find(n=>n.source_url===t.url||n.source_url===`https://www.reddit.com${t.permalink}`),[v]),_=v.filter(t=>{var n;return t.source_type==="reddit"&&(((n=t.monitor)==null?void 0:n.current_interval)??0)>0}),ke=e.jsxs("nav",{className:"flex flex-col overflow-hidden h-full","aria-label":"Reddit navigation",children:[!k&&e.jsxs("div",{className:"flex items-center gap-2 px-3 py-3 border-b border-white/5 shrink-0",children:[e.jsx(W,{size:15,className:"text-orange-400"}),e.jsx("h1",{className:"text-sm font-semibold",children:"Reddit"})]}),e.jsx("div",{className:"flex-1 overflow-y-auto space-y-4",style:k?{padding:"8px 0 16px"}:{padding:"0.5rem"},children:k?e.jsxs(e.Fragment,{children:[e.jsxs("div",{children:[e.jsxs("div",{style:{fontSize:12,textTransform:"uppercase",letterSpacing:.5,color:"rgba(255,255,255,0.45)",padding:"8px 20px 6px",fontWeight:600,display:"flex",alignItems:"center",justifyContent:"space-between"},children:[e.jsx("span",{children:"Subreddits"}),e.jsx("button",{"aria-label":"Add subreddit",onClick:()=>N(t=>!t),style:{color:"rgba(255,255,255,0.45)",background:"none",border:"none",cursor:"pointer",padding:"0 4px"},children:e.jsx(re,{size:14})})]}),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:C.map((t,n,c)=>{const p=f==="subreddits"&&d===t;return e.jsxs("button",{type:"button","aria-pressed":p,onClick:()=>{m(t),y("subreddits"),D(""),F("")},style:{display:"flex",alignItems:"center",gap:10,width:"100%",padding:"14px 16px",background:p?"rgba(255,255,255,0.08)":"none",border:"none",borderBottom:n===c.length-1?"none":"1px solid rgba(255,255,255,0.06)",cursor:"pointer",color:"inherit",textAlign:"left"},children:[e.jsx("span",{style:{color:"#fb923c",fontSize:12,fontWeight:700,flexShrink:0},children:"r/"}),e.jsx("span",{style:{flex:1,fontSize:15,fontWeight:500,color:"rgba(255,255,255,0.9)",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:t}),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)})})]}),e.jsxs("div",{children:[e.jsxs("div",{style:{fontSize:12,textTransform:"uppercase",letterSpacing:.5,color:"rgba(255,255,255,0.45)",padding:"0 20px 6px",fontWeight:600,display:"flex",alignItems:"center",gap:6},children:[e.jsx("span",{children:"Saved Posts"}),!g.authenticated&&e.jsx(le,{size:10})]}),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:g.authenticated?e.jsxs("button",{type:"button","aria-pressed":f==="saved",onClick:()=>{y("saved"),m(null)},style:{display:"flex",alignItems:"center",gap:10,width:"100%",padding:"14px 16px",background:f==="saved"?"rgba(255,255,255,0.08)":"none",border:"none",cursor:"pointer",color:"inherit",textAlign:"left"},children:[e.jsx("span",{style:{flex:1,fontSize:15,fontWeight:500,color:"rgba(255,255,255,0.9)"},children:"Reddit Saved"}),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"})})]}):e.jsx("div",{style:{padding:"14px 16px",fontSize:14,color:"rgba(255,255,255,0.4)",fontStyle:"italic"},children:"Not connected"})})]}),_.length>0&&e.jsxs("div",{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:"Monitored"}),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:_.map((t,n,c)=>e.jsxs("button",{type:"button",onClick:()=>y("monitored"),"aria-label":`Monitored: ${t.title}`,style:{display:"flex",alignItems:"center",gap:10,width:"100%",padding:"14px 16px",background:"none",border:"none",borderBottom:n===c.length-1?"none":"1px solid rgba(255,255,255,0.06)",cursor:"pointer",color:"inherit",textAlign:"left"},children:[e.jsx(ie,{size:13,style:{flexShrink:0,color:"rgba(255,255,255,0.5)"}}),e.jsx("span",{style:{flex:1,fontSize:14,color:"rgba(255,255,255,0.85)",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:t.title}),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))})]}),e.jsx("div",{style:{padding:"4px 20px 8px"},children:g.authenticated?e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6},children:[e.jsx("span",{style:{width:8,height:8,borderRadius:"50%",background:"#22c55e",flexShrink:0}}),e.jsxs("span",{style:{fontSize:12,color:"rgba(255,255,255,0.5)"},children:["u/",g.username]})]}):e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6},children:[e.jsx("span",{style:{width:8,height:8,borderRadius:"50%",background:"rgba(255,255,255,0.3)",flexShrink:0}}),e.jsx("a",{href:"/api/reddit/auth/login",style:{fontSize:12,color:"rgb(100,180,255)"},"aria-label":"Connect Reddit account",children:"Not connected"})]})})]}):e.jsxs(e.Fragment,{children:[e.jsxs("section",{children:[e.jsxs("div",{className:"flex items-center justify-between px-2 mb-1.5",children:[e.jsx("p",{className:"text-[10px] uppercase tracking-wider text-shell-text-tertiary",children:"Subreddits"}),e.jsx("button",{"aria-label":"Add subreddit",onClick:()=>N(t=>!t),className:"text-shell-text-tertiary hover:text-accent transition-colors",children:e.jsx(re,{size:12})})]}),G&&e.jsxs("div",{className:"flex gap-1 mb-1 px-1",children:[e.jsx(O,{value:z,onChange:t=>E(t.target.value),placeholder:"r/subreddit",className:"h-6 text-xs flex-1","aria-label":"New subreddit name",onKeyDown:t=>{t.key==="Enter"&&T(),t.key==="Escape"&&N(!1)},autoFocus:!0}),e.jsx(o,{size:"sm",variant:"ghost",className:"h-6 px-1.5 text-xs",onClick:T,"aria-label":"Confirm add subreddit",children:e.jsx(A,{size:11})})]}),e.jsx("div",{className:"space-y-0.5",children:C.map(t=>{const n=f==="subreddits"&&d===t;return e.jsxs(o,{variant:n?"secondary":"ghost",size:"sm","aria-pressed":n,onClick:()=>{m(t),y("subreddits"),D(""),F("")},className:"w-full justify-start text-xs h-7 px-2 gap-1.5",children:[e.jsx("span",{className:"text-orange-400 text-[10px] font-bold",children:"r/"}),t]},t)})})]}),e.jsxs("section",{children:[e.jsxs("div",{className:"flex items-center gap-1.5 px-2 mb-1.5",children:[e.jsx("p",{className:"text-[10px] uppercase tracking-wider text-shell-text-tertiary",children:"Saved Posts"}),!g.authenticated&&e.jsx(le,{size:10,className:"text-shell-text-tertiary"})]}),g.authenticated?e.jsx(o,{variant:f==="saved"?"secondary":"ghost",size:"sm","aria-pressed":f==="saved",onClick:()=>{y("saved"),m(null)},className:"w-full justify-start text-xs h-7 px-2",children:"Reddit Saved"}):e.jsx("p",{className:"text-[11px] text-shell-text-tertiary px-2 italic",children:"Not connected"})]}),e.jsxs("section",{children:[e.jsx("p",{className:"text-[10px] uppercase tracking-wider text-shell-text-tertiary px-2 mb-1.5",children:"Monitored"}),e.jsx("div",{className:"space-y-0.5",children:_.length===0?e.jsx("p",{className:"text-[11px] text-shell-text-tertiary px-2 italic",children:"None yet"}):_.map(t=>e.jsxs(o,{variant:f==="monitored"&&d===t.id?"secondary":"ghost",size:"sm",onClick:()=>{y("monitored")},className:"w-full justify-start text-xs h-7 px-2 truncate","aria-label":`Monitored: ${t.title}`,children:[e.jsx(ie,{size:11,className:"shrink-0 mr-1"}),e.jsx("span",{className:"truncate",children:t.title})]},t.id))})]}),e.jsxs("section",{children:[e.jsx("p",{className:"text-[10px] uppercase tracking-wider text-shell-text-tertiary px-2 mb-1.5",children:"History"}),e.jsx("p",{className:"text-[11px] text-shell-text-tertiary px-2 italic",children:"Coming soon"})]}),e.jsx("div",{className:"border-t border-white/5 px-3 py-2 shrink-0 mt-auto",children:g.authenticated?e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx("span",{className:"w-2 h-2 rounded-full bg-green-500 shrink-0"}),e.jsxs("span",{className:"text-xs text-shell-text-secondary truncate",children:["u/",g.username]})]}):e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx("span",{className:"w-2 h-2 rounded-full bg-shell-text-tertiary shrink-0"}),e.jsx("a",{href:"/api/reddit/auth/login",className:"text-xs text-accent hover:underline","aria-label":"Connect Reddit account",children:"Not connected"})]})})]})})]}),Ce=t=>{t.preventDefault(),D(H)},ze=e.jsxs("main",{className:"flex-1 flex flex-col overflow-hidden",children:[e.jsxs("div",{className:"px-4 py-3 border-b border-white/5 shrink-0 space-y-2",children:[e.jsxs("form",{onSubmit:Ce,className:"flex gap-2",role:"search",children:[e.jsx(O,{ref:be,value:H,onChange:t=>F(t.target.value),placeholder:d?`Search r/${d}…`:"Search Reddit…",className:"flex-1 h-8 text-sm","aria-label":"Search Reddit"}),e.jsx(o,{type:"submit",variant:"ghost",size:"sm",className:"h-8 px-2","aria-label":"Run search",children:e.jsx(Ee,{size:14})})]}),e.jsx("div",{className:"flex items-center gap-1",role:"group","aria-label":"Sort posts",children:["hot","new","top"].map(t=>e.jsx(o,{variant:R===t?"secondary":"ghost",size:"sm",className:"h-6 text-xs px-2 capitalize","aria-pressed":R===t,onClick:()=>he(t),children:t},t))})]}),e.jsxs("div",{className:"flex-1 overflow-y-auto p-3 space-y-2",children:[L&&e.jsx("div",{className:"flex items-center justify-center py-12",children:e.jsx(q,{size:18,className:"animate-spin text-shell-text-tertiary"})}),!L&&!d&&f==="subreddits"&&!I&&e.jsxs("div",{className:"flex flex-col items-center justify-center py-16 text-shell-text-tertiary",children:[e.jsx(W,{size:36,className:"mb-3 opacity-30"}),e.jsx("p",{className:"text-sm",children:"Select a subreddit to browse"})]}),!L&&P.posts.length>0&&e.jsx("ul",{role:"list",className:"space-y-2",children:P.posts.map(t=>e.jsx("li",{role:"listitem",children:e.jsx(Je,{post:t,savedItem:Se(t),onOpen:je,onSave:ye,saving:me===t.id})},t.id))}),!L&&P.posts.length===0&&(d||I||f==="saved")&&e.jsx("div",{className:"flex flex-col items-center justify-center py-16 text-shell-text-tertiary",children:e.jsx("p",{className:"text-sm",children:"No posts found"})})]})]}),Le=(()=>{const t=(i==null?void 0:i.post)??null,n=(i==null?void 0:i.comments)??[];return e.jsxs("main",{className:"flex-1 flex flex-col overflow-hidden",children:[e.jsxs("div",{className:"px-3 py-2 border-b border-white/5 shrink-0 flex items-center justify-between gap-2 flex-wrap",children:[e.jsxs(o,{variant:"ghost",size:"sm",className:"h-7 text-xs gap-1.5",onClick:U,"aria-label":"Back to feed",children:[e.jsx(Pe,{size:13}),"Back to feed"]}),t&&e.jsxs("div",{className:"flex items-center gap-1.5 flex-wrap",children:[e.jsxs("a",{href:`https://www.reddit.com${t.permalink}`,target:"_blank",rel:"noopener noreferrer",className:"flex items-center gap-1 text-xs text-shell-text-tertiary hover:text-accent transition-colors","aria-label":"Open on Reddit",children:[e.jsx(ne,{size:12}),"Reddit"]}),h?e.jsxs(e.Fragment,{children:[e.jsxs(o,{variant:"secondary",size:"sm",className:"h-6 text-xs gap-1 px-2",disabled:!0,"aria-label":"Already saved to Library",children:[e.jsx(A,{size:11}),"Saved"]}),e.jsxs(o,{variant:"ghost",size:"sm",className:"h-6 text-xs gap-1 px-2",onClick:Ne,"aria-label":"Re-ingest this thread",children:[e.jsx(q,{size:11}),"Re-ingest"]}),fe?e.jsxs(e.Fragment,{children:[e.jsx(o,{variant:"ghost",size:"sm",className:"h-6 text-xs px-2 text-red-400 hover:text-red-300",onClick:we,"aria-label":"Confirm delete from Library",children:"Confirm Delete"}),e.jsx(o,{variant:"ghost",size:"sm",className:"h-6 text-xs px-2",onClick:()=>w(!1),"aria-label":"Cancel delete",children:"Cancel"})]}):e.jsx(o,{variant:"ghost",size:"sm",className:"h-6 text-xs gap-1 px-2 text-red-400 hover:text-red-300",onClick:()=>w(!0),"aria-label":"Delete from Library",children:e.jsx(De,{size:11})})]}):e.jsxs(o,{variant:"ghost",size:"sm",className:"h-6 text-xs gap-1 px-2",onClick:ve,disabled:Z,"aria-label":"Save to Library",children:[e.jsx(de,{size:11}),Z?"Saving…":"Save to Library"]})]})]}),e.jsxs("div",{className:"flex-1 overflow-y-auto p-4 space-y-4",children:[u&&e.jsx("div",{className:"flex items-center justify-center py-16",children:e.jsx(q,{size:20,className:"animate-spin text-shell-text-tertiary"})}),!u&&t&&e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"space-y-2",children:[e.jsx("h2",{className:"text-base font-semibold text-shell-text leading-snug",children:t.title}),e.jsxs("div",{className:"flex items-center gap-2 flex-wrap",children:[e.jsxs("span",{className:"text-[11px] font-semibold px-1.5 py-0.5 rounded-full bg-orange-500/20 text-orange-400 border border-orange-500/30",children:["r/",t.subreddit]}),e.jsxs("span",{className:"text-xs text-shell-text-tertiary",children:["by u/",t.author]}),e.jsx("span",{className:"text-shell-text-tertiary text-xs",children:"·"}),e.jsxs("span",{className:"text-xs text-shell-text-tertiary",children:[$(t.score)," pts"]}),e.jsx("span",{className:"text-shell-text-tertiary text-xs",children:"·"}),e.jsxs("span",{className:"text-xs text-shell-text-tertiary",children:[Math.round(t.upvote_ratio*100),"% upvoted"]}),e.jsx("span",{className:"text-shell-text-tertiary text-xs",children:"·"}),e.jsx("span",{className:"text-xs text-shell-text-tertiary",children:V(t.created_utc)})]}),(h==null?void 0:h.summary)&&e.jsxs("div",{className:"bg-accent/5 border border-accent/20 rounded-lg px-3 py-2",children:[e.jsx("p",{className:"text-[10px] uppercase tracking-wider text-accent mb-1 font-semibold",children:"Summary"}),e.jsx("p",{className:"text-xs text-shell-text-secondary leading-relaxed",children:h.summary})]}),t.is_self&&t.selftext&&e.jsx("div",{className:"bg-white/3 rounded-lg px-3 py-2 border border-white/5",children:e.jsx("p",{className:"text-sm text-shell-text-secondary whitespace-pre-wrap leading-relaxed",children:t.selftext})}),!t.is_self&&e.jsxs("a",{href:t.url,target:"_blank",rel:"noopener noreferrer",className:"text-xs text-accent hover:underline flex items-center gap-1","aria-label":`External link: ${t.url}`,children:[e.jsx(ne,{size:11}),Ge(t.url)||t.url]})]}),e.jsxs("div",{children:[e.jsx("div",{role:"tablist","aria-label":"Thread sections",className:"flex gap-1 border-b border-white/5 pb-0 mb-3",children:["comments","history","metadata"].map(c=>e.jsxs("button",{role:"tab","aria-selected":S===c,onClick:()=>X(c),className:["px-3 py-1.5 text-xs capitalize border-b-2 transition-colors",S===c?"border-accent text-accent":"border-transparent text-shell-text-tertiary hover:text-shell-text"].join(" "),children:[c,c==="comments"&&e.jsxs("span",{className:"ml-1 text-[10px] opacity-60",children:["(",t.num_comments,")"]})]},c))}),S==="comments"&&e.jsx("ul",{role:"list",className:"space-y-2",children:n.length===0?e.jsx("li",{className:"text-sm text-shell-text-tertiary py-4 text-center italic",children:"No comments yet"}):n.map(c=>e.jsx(oe,{comment:c},c.id))}),S==="history"&&e.jsx("div",{className:"text-sm text-shell-text-tertiary py-4",children:h?e.jsx("p",{className:"italic",children:"Monitoring snapshots will appear here when available."}):e.jsx("p",{className:"italic",children:"Save this thread to the Library to enable monitoring."})}),S==="metadata"&&t&&e.jsx("dl",{className:"grid grid-cols-2 gap-x-4 gap-y-2 text-xs",children:[["Subreddit",`r/${t.subreddit}`],["Author",`u/${t.author}`],["Score",$(t.score)],["Upvote ratio",`${Math.round(t.upvote_ratio*100)}%`],["Comments",String(t.num_comments)],["Flair",t.flair||"—"],["Type",t.is_self?"Text post":"Link post"],["Posted",new Date(t.created_utc*1e3).toLocaleString()],["Permalink",t.permalink]].map(([c,p])=>e.jsxs("div",{className:"contents",children:[e.jsx("dt",{className:"text-shell-text-tertiary font-medium truncate",children:c}),e.jsx("dd",{className:"text-shell-text truncate",title:p,children:p})]},c))})]})]}),!u&&!i&&e.jsx("div",{className:"flex flex-col items-center justify-center py-16 text-shell-text-tertiary",children:e.jsx("p",{className:"text-sm",children:"Failed to load thread"})})]})]})})(),se=d,Re=a.useCallback(()=>{m(null),l("feed"),x(null),w(!1)},[]),Ie=r==="thread"?Le:ze;return e.jsxs("div",{className:"flex flex-col h-full min-h-0 overflow-hidden bg-shell-base text-shell-text relative",children:[e.jsx($e,{selectedId:se,onBack:Re,listTitle:"Reddit",detailTitle:d?`r/${d}`:void 0,listWidth:208,list:ke,detail:se!==null?Ie:k?null:e.jsxs("div",{className:"flex flex-col items-center justify-center h-full text-shell-text-tertiary",children:[e.jsx(W,{size:36,className:"mb-3 opacity-20"}),e.jsx("p",{className:"text-sm",children:"Select a subreddit"})]})}),k&&G&&e.jsx("div",{className:"absolute inset-0 z-50 flex items-end bg-black/50 backdrop-blur-sm",onClick:()=>N(!1),role:"dialog","aria-modal":"true","aria-label":"Add subreddit",children:e.jsxs("div",{style:{borderRadius:"20px 20px 0 0",width:"100%",background:"var(--shell-surface, #1a1a2e)",padding:"20px 16px 32px"},onClick:t=>t.stopPropagation(),children:[e.jsx("p",{className:"text-sm font-semibold mb-3",children:"Add Subreddit"}),e.jsxs("div",{className:"flex gap-2",children:[e.jsx(O,{value:z,onChange:t=>E(t.target.value),placeholder:"r/subreddit",className:"flex-1","aria-label":"New subreddit name",onKeyDown:t=>{t.key==="Enter"&&T(),t.key==="Escape"&&N(!1)},autoFocus:!0}),e.jsxs(o,{onClick:T,"aria-label":"Confirm add subreddit",children:[e.jsx(A,{size:14}),"Add"]})]})]})})]})}export{rt as RedditApp};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The monitored view never loads any posts.

Clicking a monitored entry only switches f to "monitored", but te() has no monitored branch, so P.posts falls back to empty and the main pane shows “No posts found.” This makes the monitored section a dead end right now.

@@ -1,4 +1,4 @@
import{r as s,j as e}from"./vendor-react-l6srOxy7.js";import{B as b,C as w,L as N,I as E,S as T,T as Z}from"./toolbar-UW6q5pkx.js";import{u as ee}from"./main-BXOeBesV.js";import{t as te,a1 as se,ao as ae,ad as K,aa as P,at as V,au as le,av as ne,l as re,U as ce,a0 as ie,r as F,f as D,aw as Y,g as oe,y as de,c as xe,ac as me,ax as ue,X as he}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";function pe(r,l,t=a=>a,x=a=>({value:a})){const a=`taos-pref:${r}`,[d,i]=s.useState(()=>{try{const p=localStorage.getItem(a);if(p!==null)return JSON.parse(p)}catch{}return l}),[n,c]=s.useState(!1),m=s.useRef(null);s.useEffect(()=>{let p=!1;return(async()=>{try{const o=await fetch(`/api/preferences/${encodeURIComponent(r)}`);if(!o.ok){c(!0);return}const j=await o.json();if(p)return;if(j&&typeof j=="object"&&Object.keys(j).length>0){const v=t(j);i(v);try{localStorage.setItem(a,JSON.stringify(v))}catch{}}c(!0)}catch{c(!0)}})(),()=>{p=!0}},[r]);const h=s.useCallback(p=>{i(o=>{const j=typeof p=="function"?p(o):p;try{localStorage.setItem(a,JSON.stringify(j))}catch{}return m.current!==null&&clearTimeout(m.current),m.current=setTimeout(()=>{m.current=null,fetch(`/api/preferences/${encodeURIComponent(r)}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(x(j))}).catch(()=>{})},500),j})},[a,x,r]);return[d,h,{loaded:n}]}const fe=[{id:"system",label:"System Info",icon:te},{id:"storage",label:"Storage",icon:se},{id:"memory",label:"Memory",icon:ae},{id:"backup",label:"Backup & Restore",icon:K},{id:"updates",label:"Updates",icon:P},{id:"advanced",label:"Advanced",icon:V},{id:"shortcuts",label:"Keyboard Shortcuts",icon:le},{id:"accessibility",label:"Accessibility",icon:ne},{id:"desktop",label:"Desktop & Dock",icon:re},{id:"users",label:"Users",icon:ce}],je={cpu:"Detecting...",ram:"Detecting...",npu:"Detecting...",gpu:"Detecting...",disk:"Detecting...",os:"Detecting..."},ye=[{label:"Models",size:"--",bytes:0,maxBytes:1},{label:"Data",size:"--",bytes:0,maxBytes:1},{label:"App Catalog",size:"--",bytes:0,maxBytes:1}];async function G(r,l){try{const t=await fetch(r,{headers:{Accept:"application/json"}});return!t.ok||!(t.headers.get("content-type")??"").includes("application/json")?l:await t.json()}catch{return l}}function be({value:r,max:l}){const t=l>0?Math.min(100,r/l*100):0;return e.jsx("div",{className:"h-2 w-full rounded-full bg-white/5",role:"progressbar","aria-valuenow":t,"aria-valuemin":0,"aria-valuemax":100,children:e.jsx("div",{className:"h-full rounded-full bg-sky-500 transition-all",style:{width:`${t}%`}})})}function ge(){const[r,l]=s.useState(je),[t,x]=s.useState(!1),[a,d]=s.useState(!1),i=s.useCallback(async()=>{var m,h,p,o,j,v,k,S,g,R,_,I,y;x(!0);const c=await G("/api/system",null);if(c!=null&&c.hardware||c!=null&&c.resources){const u=c.hardware??{},U=c.resources??{},C=U.ram_total_mb??u.ram_mb??0,$=U.disk_total_gb??((m=u.disk)==null?void 0:m.total_gb)??0,A=((h=u.cpu)==null?void 0:h.model)??((p=u.cpu)==null?void 0:p.soc)??"Unknown",B=(o=u.cpu)!=null&&o.cores?` × ${u.cpu.cores}`:"",L=(j=u.cpu)!=null&&j.arch?` (${u.cpu.arch})`:"",f=((v=u.gpu)==null?void 0:v.model)||((k=u.gpu)==null?void 0:k.type)||"None",z=(S=u.gpu)!=null&&S.vram_mb&&u.gpu.vram_mb>0?` (${(u.gpu.vram_mb/1024).toFixed(1)} GB)`:"",M=(g=u.npu)!=null&&g.type&&u.npu.type!=="none"?u.npu.type:"None",q=(R=u.npu)!=null&&R.tops&&u.npu.tops>0?` · ${u.npu.tops} TOPS`:"",X=(_=u.disk)!=null&&_.type?` ${u.disk.type}`:"",J=[(I=u.os)==null?void 0:I.distro,(y=u.os)==null?void 0:y.version].filter(Boolean),Q=J.length>0?J.join(" "):"—";l({cpu:`${A}${B}${L}`,ram:C>=1024?`${(C/1024).toFixed(1)} GB`:C>0?`${C} MB`:"—",npu:`${M}${q}`,gpu:`${f}${z}`,disk:$>0?`${$} GB${X}`:"—",os:Q})}else l({cpu:"Unavailable",ram:"Unavailable",npu:"Unavailable",gpu:"Unavailable",disk:"Unavailable",os:"Unavailable"});x(!1)},[]);s.useEffect(()=>{i()},[i]);const n=[["CPU",r.cpu],["RAM",r.ram],["NPU",r.npu],["GPU",r.gpu],["Disk",r.disk],["OS",r.os]];return e.jsxs("section",{"aria-label":"System information",children:[e.jsx("h2",{className:"text-lg font-semibold mb-5",children:"System Information"}),e.jsx("div",{className:"rounded-2xl bg-white/[0.04] border border-white/[0.06] overflow-x-auto backdrop-blur-sm",children:e.jsx("table",{className:"w-full text-sm min-w-[360px]",children:e.jsx("tbody",{children:n.map(([c,m])=>e.jsxs("tr",{className:"border-b border-white/5 last:border-0",children:[e.jsx("td",{className:"px-5 py-3 text-shell-text-secondary font-medium w-32",children:c}),e.jsx("td",{className:"px-5 py-3",children:m})]},c))})})}),e.jsxs("div",{className:"mt-3 flex items-center gap-2 flex-wrap",children:[e.jsxs(b,{variant:"outline",size:"sm",onClick:i,disabled:t,children:[e.jsx(P,{size:14,className:t?"animate-spin":""}),"Re-detect Hardware"]}),e.jsxs(b,{variant:"outline",size:"sm",onClick:async()=>{d(!0);try{await fetch("/api/system/restart/prepare",{method:"POST"})}catch{}},"aria-label":"Restart taOS server",children:[e.jsx(P,{size:14}),"Restart Server"]})]}),e.jsx("p",{className:"mt-2 text-xs text-shell-text-tertiary",children:"Restart the server to apply settings changes that require a reload."}),a&&e.jsx(W,{onClose:()=>d(!1)})]})}function Ne(){const[r,l]=s.useState(ye);return s.useEffect(()=>{G("/api/settings/storage",null).then(t=>{t&&Array.isArray(t)?l(t):l([{label:"Models",size:"4.2 GB",bytes:4200,maxBytes:32e3},{label:"Data",size:"1.8 GB",bytes:1800,maxBytes:32e3},{label:"App Catalog",size:"320 MB",bytes:320,maxBytes:32e3}])})},[]),e.jsxs("section",{"aria-label":"Storage usage",children:[e.jsx("h2",{className:"text-lg font-semibold mb-5",children:"Storage Usage"}),e.jsx("div",{className:"space-y-3",children:r.map(t=>e.jsxs(w,{className:"p-4",children:[e.jsxs("div",{className:"flex items-center justify-between mb-2",children:[e.jsx("span",{className:"text-sm font-medium",children:t.label}),e.jsx("span",{className:"text-sm text-shell-text-secondary tabular-nums",children:t.size})]}),e.jsx(be,{value:t.bytes,max:t.maxBytes})]},t.label))})]})}const ve=[{key:"capture_conversations",label:"Conversations",desc:"Messages you send to agents in the Message Hub"},{key:"capture_notes",label:"Notes",desc:"Notes from the Text Editor app"},{key:"capture_files",label:"File activity",desc:"Files you upload or open"},{key:"capture_searches",label:"Search queries",desc:"What you search for in global search"}];function we(){const[r,l]=s.useState(null),[t,x]=s.useState(null),[a,d]=s.useState(null);s.useEffect(()=>{fetch("/api/user-memory/settings").then(n=>n.ok?n.json():null).then(n=>{l(n||{})}).catch(()=>{l({}),d("Could not load memory settings.")}),fetch("/api/user-memory/stats").then(n=>n.ok?n.json():null).then(n=>{n&&x(n)}).catch(()=>{})},[]);const i=(n,c)=>{const m={...r||{},[n]:c};l(m),fetch("/api/user-memory/settings",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({[n]:c})}).then(h=>{h.ok?d(null):d(`Failed to save setting (${h.status})`)}).catch(()=>d("Could not reach backend."))};return r?e.jsxs("section",{"aria-label":"Memory capture settings",children:[e.jsx("h2",{className:"text-lg font-semibold mb-2",children:"Memory Capture"}),e.jsx("p",{className:"text-sm text-shell-text-tertiary mb-5",children:"Choose what activity gets saved to your personal memory index. All data stays on this device."}),a&&e.jsxs("p",{className:"mb-3 text-xs text-amber-400 flex items-center gap-1.5",children:[e.jsx(F,{size:12})," ",a]}),e.jsx("div",{className:"space-y-2",children:ve.map(n=>{const c=!!r[n.key],m=`capture-${String(n.key)}`;return e.jsxs(w,{className:"p-4 flex items-center justify-between gap-3",children:[e.jsxs("div",{className:"flex-1 min-w-0",children:[e.jsx(N,{htmlFor:m,className:"text-sm font-medium text-shell-text",children:n.label}),e.jsx("p",{className:"text-xs text-shell-text-tertiary mt-0.5",children:n.desc})]}),e.jsx(T,{id:m,checked:c,onCheckedChange:h=>i(n.key,h),"aria-label":`Capture ${n.label}`})]},String(n.key))})}),t&&e.jsxs(w,{className:"mt-6 p-4",children:[e.jsx("h3",{className:"text-sm font-medium mb-3",children:"Stored chunks"}),e.jsxs("div",{className:"text-xs text-shell-text-secondary mb-2 tabular-nums",children:["Total: ",t.total]}),Object.keys(t.collections||{}).length>0?e.jsx("ul",{className:"space-y-1 text-xs text-shell-text-tertiary",children:Object.entries(t.collections).map(([n,c])=>e.jsxs("li",{className:"flex justify-between tabular-nums",children:[e.jsx("span",{children:n}),e.jsx("span",{children:c})]},n))}):e.jsx("p",{className:"text-xs text-shell-text-tertiary",children:"No memories captured yet."})]})]}):e.jsxs("section",{"aria-label":"Memory capture settings",children:[e.jsx("h2",{className:"text-lg font-semibold mb-5",children:"Memory Capture"}),e.jsx("p",{className:"text-sm text-shell-text-tertiary",children:"Loading..."})]})}function ke(){const[r,l]=s.useState(null),[t,x]=s.useState(!1),a=async()=>{x(!0),l(null);try{const d=await fetch("/api/backup",{method:"POST"});d.ok?l("Backup created successfully."):l(`Backup failed (${d.status}). API may not be available yet.`)}catch{l("Could not reach backup endpoint. API not available yet.")}x(!1)};return e.jsxs("section",{"aria-label":"Backup and restore",children:[e.jsx("h2",{className:"text-lg font-semibold mb-5",children:"Backup & Restore"}),e.jsxs(w,{className:"p-4 space-y-4",children:[e.jsxs("div",{children:[e.jsx("h3",{className:"text-sm font-medium mb-2",children:"Create Backup"}),e.jsx("p",{className:"text-xs text-shell-text-tertiary mb-3",children:"Export all agents, memory, and configuration as a backup archive."}),e.jsxs(b,{size:"sm",onClick:a,disabled:t,children:[e.jsx(K,{size:14,className:t?"animate-bounce":""}),t?"Creating...":"Create Backup"]}),r&&e.jsx("p",{className:`mt-2 text-xs ${r.includes("success")?"text-emerald-400":"text-amber-400"}`,children:r})]}),e.jsx("hr",{className:"border-white/5"}),e.jsxs("div",{children:[e.jsx("h3",{className:"text-sm font-medium mb-2",children:"Restore from Backup"}),e.jsx("p",{className:"text-xs text-shell-text-tertiary mb-3",children:"Upload a previously created backup archive to restore."}),e.jsxs("label",{className:"flex flex-col items-center gap-2 p-6 rounded-lg border-2 border-dashed border-white/10 hover:border-white/20 transition-colors cursor-pointer",children:[e.jsx(me,{size:24,className:"text-shell-text-tertiary"}),e.jsx("span",{className:"text-xs text-shell-text-tertiary",children:"Click to select a backup file"}),e.jsx("input",{type:"file",accept:".tar.gz,.zip,.bak",className:"hidden","aria-label":"Upload backup file"})]})]})]})]})}function W({onClose:r}){const[l,t]=s.useState(null),[x,a]=s.useState(!1);s.useEffect(()=>{let n=!1,c=null,m=null,h=!1;const p=()=>{h||n||(h=!0,c&&clearInterval(c),m=setInterval(async()=>{if(!n)try{(await fetch("/api/settings/update-status")).ok&&(a(!0),m&&clearInterval(m),setTimeout(()=>{n||window.location.reload()},500))}catch{}},2e3))};return c=setInterval(async()=>{if(!n)try{const o=await fetch("/api/system/restart/status");if(o.ok){const j=await o.json();t(j),j.phase==="restarting"&&p()}}catch{p()}},1e3),()=>{n=!0,c&&clearInterval(c),m&&clearInterval(m)}},[]);const d=l?Object.entries(l.agents):[];function i(n){return n==="ready"?e.jsx("span",{className:"text-[10px] px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-300",children:"ready"}):n==="timeout"?e.jsx("span",{className:"text-[10px] px-1.5 py-0.5 rounded bg-amber-500/20 text-amber-300",children:"timeout"}):n==="error"?e.jsx("span",{className:"text-[10px] px-1.5 py-0.5 rounded bg-red-500/20 text-red-300",children:"error"}):e.jsxs("span",{className:"text-[10px] px-1.5 py-0.5 rounded bg-sky-500/20 text-sky-300 flex items-center gap-1",children:[e.jsx(P,{size:10,className:"animate-spin"}),n]})}return e.jsx("div",{role:"dialog","aria-modal":"true","aria-label":"Restart progress",className:"fixed inset-0 z-50 flex items-center justify-center bg-black/60",children:e.jsxs("div",{className:"bg-shell-surface border border-white/10 rounded-xl p-6 w-full max-w-md shadow-xl space-y-4",children:[e.jsx("h3",{className:"text-base font-semibold",children:x?"Restarted — reloading…":d.length>0?"Preparing agents for restart":"Restarting server…"}),d.length>0&&e.jsx("ul",{className:"space-y-1","aria-label":"Agent preparation status",children:d.map(([n,c])=>e.jsxs("li",{className:"flex items-center justify-between text-sm",children:[e.jsx("span",{className:"text-shell-text-secondary",children:n}),i(c.status)]},n))}),(l==null?void 0:l.phase)==="restarting"&&!x&&e.jsx("p",{className:"text-xs text-shell-text-tertiary",children:"Waiting for server to come back…"}),!l&&e.jsxs("p",{className:"text-xs text-shell-text-tertiary flex items-center gap-1",children:[e.jsx(P,{size:12,className:"animate-spin"})," Connecting…"]}),e.jsx("div",{className:"flex justify-end",children:e.jsx(b,{variant:"outline",size:"sm",onClick:r,"aria-label":"Cancel restart progress dialog",children:"Cancel"})})]})})}function Se(){const[r,l]=s.useState(!1),[t,x]=s.useState(!1),[a,d]=s.useState(null),[i,n]=s.useState(null),[c,m]=s.useState({check_enabled:!0,auto_apply:!1,auto_restart:!1}),[h,p]=s.useState(null),[o,j]=s.useState(!1),[v,k]=s.useState(!1);s.useEffect(()=>{(async()=>{try{const y=await fetch("/api/preferences/auto-update");if(y.ok){const u=await y.json();u&&typeof u=="object"&&m({check_enabled:u.check_enabled??!0,auto_apply:u.auto_apply??!1,auto_restart:u.auto_restart??!1})}}catch{}try{const y=await fetch("/api/settings/update-check");y.ok&&d(await y.json())}catch{}try{const y=await fetch("/api/settings/update-status");y.ok&&p(await y.json())}catch{}})()},[]);const S=s.useCallback(async y=>{m(y);try{await fetch("/api/preferences/auto-update",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(y)})}catch{}},[]),g=async()=>{l(!0),n(null);try{const y=await fetch("/api/settings/update-check");if(y.ok){const u=await y.json();d(u),n(u.has_updates?"A new version is available.":"You are up to date.")}else n("Update check not available.")}catch{n("Could not reach update server.")}l(!1)},R=async()=>{x(!0),n(null);try{const y=await fetch("/api/settings/update",{method:"POST"});if(y.ok){const u=await y.json().catch(()=>({}));if(u.status==="restarting")j(!0);else{n(u.message??"Update applied. Restart the server to finish."),k(!0);const U=await fetch("/api/settings/update-check");U.ok&&d(await U.json());const C=await fetch("/api/settings/update-status");C.ok&&p(await C.json())}}else{const u=await y.json().catch(()=>({}));n(u.error??"Update failed.")}}catch{n("Could not apply update.")}x(!1)},_=async()=>{j(!0);try{await fetch("/api/system/restart/prepare",{method:"POST"})}catch{}},I=!!(h!=null&&h.pending_restart_sha);return e.jsxs("section",{"aria-label":"System updates",children:[e.jsx("h2",{className:"text-lg font-semibold mb-5",children:"Updates"}),I&&e.jsxs("div",{className:"mb-4 flex items-center justify-between gap-3 rounded-lg border border-amber-500/30 bg-amber-500/10 px-4 py-3",children:[e.jsxs("div",{className:"flex items-center gap-2 text-sm text-amber-200",children:[e.jsx(F,{size:16,className:"shrink-0"}),e.jsxs("span",{children:["Update pulled — restart to finish applying (",h.pending_restart_sha.slice(0,7),")"]})]}),e.jsx(b,{size:"sm",onClick:_,"aria-label":"Restart server to apply update",children:"Restart now"})]}),e.jsxs(w,{className:"p-4 space-y-4",children:[e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsx("div",{className:"p-2 rounded-lg bg-white/5 text-sky-400",children:e.jsx(xe,{size:20})}),e.jsxs("div",{className:"flex-1 min-w-0",children:[e.jsx("p",{className:"text-sm font-medium",children:"taOS"}),a!=null&&a.has_updates&&a.new_commit?e.jsxs("div",{className:"flex flex-col gap-0.5",children:[e.jsxs("p",{className:"text-xs text-shell-text-tertiary tabular-nums",children:[e.jsx("span",{className:"text-white/40",children:"installed "}),a.current_commit]}),e.jsxs("p",{className:"text-xs text-amber-300/90 tabular-nums",children:[e.jsx("span",{className:"text-amber-300/50",children:"available "}),a.new_commit]})]}):e.jsx("p",{className:"text-xs text-shell-text-tertiary tabular-nums",children:(a==null?void 0:a.current_commit)??"v0.1.0-dev"})]}),(a==null?void 0:a.has_updates)&&e.jsx("span",{className:"text-[10px] px-2 py-1 rounded-full font-semibold bg-amber-500/20 text-amber-300",children:"Update available"})]}),e.jsxs("div",{className:"flex gap-2 flex-wrap",children:[e.jsxs(b,{variant:"outline",size:"sm",onClick:g,disabled:r,children:[e.jsx(P,{size:14,className:r?"animate-spin":""}),r?"Checking...":"Check Now"]}),v?e.jsx(b,{size:"sm",onClick:_,"aria-label":"Restart server to apply update",children:"Restart Now"}):a!=null&&a.has_updates?e.jsx(b,{size:"sm",onClick:R,disabled:t,children:t?"Installing...":"Install Update"}):null]}),i&&e.jsxs("div",{className:"flex items-start gap-2 text-xs",children:[i.includes("up to date")||i.includes("applied")?e.jsx(D,{size:14,className:"text-emerald-400 shrink-0 mt-0.5"}):e.jsx(F,{size:14,className:"text-amber-400 shrink-0 mt-0.5"}),e.jsx("span",{className:"text-shell-text-secondary",children:i})]}),e.jsxs("div",{className:"border-t border-white/5 pt-4 space-y-3",children:[e.jsxs("div",{className:"flex items-center justify-between gap-3",children:[e.jsxs("div",{className:"flex-1 min-w-0",children:[e.jsx(N,{className:"text-sm",children:"Check for updates automatically"}),e.jsx("p",{className:"text-[11px] text-shell-text-tertiary mt-0.5",children:"Polls GitHub hourly and notifies when a new version is available."})]}),e.jsx(T,{checked:c.check_enabled??!0,onCheckedChange:y=>S({...c,check_enabled:y})})]}),e.jsxs("div",{className:"flex items-center justify-between gap-3",children:[e.jsxs("div",{className:"flex-1 min-w-0",children:[e.jsx(N,{className:"text-sm",children:"Install updates automatically"}),e.jsx("p",{className:"text-[11px] text-shell-text-tertiary mt-0.5",children:"Pulls + installs new versions as soon as they're detected. You'll still need to restart the server manually."})]}),e.jsx(T,{checked:c.auto_apply??!1,onCheckedChange:y=>S({...c,auto_apply:y}),disabled:!(c.check_enabled??!0)})]}),e.jsxs("div",{className:"flex items-center justify-between gap-3",children:[e.jsxs("div",{className:"flex-1 min-w-0",children:[e.jsx(N,{className:"text-sm",children:"Automatically restart after update"}),e.jsx("p",{className:"text-[11px] text-shell-text-tertiary mt-0.5",children:c.auto_restart?"Server will restart automatically once an update is pulled.":"We'll remind you every 6 hours when a restart is pending."})]}),e.jsx(T,{checked:c.auto_restart??!1,onCheckedChange:y=>S({...c,auto_restart:y}),"aria-label":"Automatically restart after update"})]})]})]}),o&&e.jsx(W,{onClose:()=>j(!1)})]})}function Ce(){const[r,l]=s.useState(`# taOS Configuration
import{r as s,j as e}from"./vendor-react-l6srOxy7.js";import{B as b,C as w,L as N,I as E,S as T,T as Z}from"./toolbar-UW6q5pkx.js";import{u as ee}from"./main-DgK4yEp2.js";import{t as te,a0 as se,an as ae,ac as K,a9 as P,as as V,at as le,au as ne,l as re,U as ce,$ as ie,r as F,f as D,av as Y,g as oe,y as de,c as xe,ab as me,aw as ue,X as he}from"./vendor-icons-wm645Jsx.js";import"./vendor-radix-BhM7AEEG.js";import"./vendor-layout-B-pp9n1f.js";import"./tokens-BWEexfPB.js";import"./vendor-codemirror-CL2HhW7v.js";function pe(r,l,t=a=>a,x=a=>({value:a})){const a=`taos-pref:${r}`,[d,i]=s.useState(()=>{try{const p=localStorage.getItem(a);if(p!==null)return JSON.parse(p)}catch{}return l}),[n,c]=s.useState(!1),m=s.useRef(null);s.useEffect(()=>{let p=!1;return(async()=>{try{const o=await fetch(`/api/preferences/${encodeURIComponent(r)}`);if(!o.ok){c(!0);return}const j=await o.json();if(p)return;if(j&&typeof j=="object"&&Object.keys(j).length>0){const v=t(j);i(v);try{localStorage.setItem(a,JSON.stringify(v))}catch{}}c(!0)}catch{c(!0)}})(),()=>{p=!0}},[r]);const h=s.useCallback(p=>{i(o=>{const j=typeof p=="function"?p(o):p;try{localStorage.setItem(a,JSON.stringify(j))}catch{}return m.current!==null&&clearTimeout(m.current),m.current=setTimeout(()=>{m.current=null,fetch(`/api/preferences/${encodeURIComponent(r)}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(x(j))}).catch(()=>{})},500),j})},[a,x,r]);return[d,h,{loaded:n}]}const fe=[{id:"system",label:"System Info",icon:te},{id:"storage",label:"Storage",icon:se},{id:"memory",label:"Memory",icon:ae},{id:"backup",label:"Backup & Restore",icon:K},{id:"updates",label:"Updates",icon:P},{id:"advanced",label:"Advanced",icon:V},{id:"shortcuts",label:"Keyboard Shortcuts",icon:le},{id:"accessibility",label:"Accessibility",icon:ne},{id:"desktop",label:"Desktop & Dock",icon:re},{id:"users",label:"Users",icon:ce}],je={cpu:"Detecting...",ram:"Detecting...",npu:"Detecting...",gpu:"Detecting...",disk:"Detecting...",os:"Detecting..."},ye=[{label:"Models",size:"--",bytes:0,maxBytes:1},{label:"Data",size:"--",bytes:0,maxBytes:1},{label:"App Catalog",size:"--",bytes:0,maxBytes:1}];async function G(r,l){try{const t=await fetch(r,{headers:{Accept:"application/json"}});return!t.ok||!(t.headers.get("content-type")??"").includes("application/json")?l:await t.json()}catch{return l}}function be({value:r,max:l}){const t=l>0?Math.min(100,r/l*100):0;return e.jsx("div",{className:"h-2 w-full rounded-full bg-white/5",role:"progressbar","aria-valuenow":t,"aria-valuemin":0,"aria-valuemax":100,children:e.jsx("div",{className:"h-full rounded-full bg-sky-500 transition-all",style:{width:`${t}%`}})})}function ge(){const[r,l]=s.useState(je),[t,x]=s.useState(!1),[a,d]=s.useState(!1),i=s.useCallback(async()=>{var m,h,p,o,j,v,k,S,g,R,_,I,y;x(!0);const c=await G("/api/system",null);if(c!=null&&c.hardware||c!=null&&c.resources){const u=c.hardware??{},U=c.resources??{},C=U.ram_total_mb??u.ram_mb??0,$=U.disk_total_gb??((m=u.disk)==null?void 0:m.total_gb)??0,A=((h=u.cpu)==null?void 0:h.model)??((p=u.cpu)==null?void 0:p.soc)??"Unknown",B=(o=u.cpu)!=null&&o.cores?` × ${u.cpu.cores}`:"",L=(j=u.cpu)!=null&&j.arch?` (${u.cpu.arch})`:"",f=((v=u.gpu)==null?void 0:v.model)||((k=u.gpu)==null?void 0:k.type)||"None",z=(S=u.gpu)!=null&&S.vram_mb&&u.gpu.vram_mb>0?` (${(u.gpu.vram_mb/1024).toFixed(1)} GB)`:"",M=(g=u.npu)!=null&&g.type&&u.npu.type!=="none"?u.npu.type:"None",q=(R=u.npu)!=null&&R.tops&&u.npu.tops>0?` · ${u.npu.tops} TOPS`:"",X=(_=u.disk)!=null&&_.type?` ${u.disk.type}`:"",J=[(I=u.os)==null?void 0:I.distro,(y=u.os)==null?void 0:y.version].filter(Boolean),Q=J.length>0?J.join(" "):"—";l({cpu:`${A}${B}${L}`,ram:C>=1024?`${(C/1024).toFixed(1)} GB`:C>0?`${C} MB`:"—",npu:`${M}${q}`,gpu:`${f}${z}`,disk:$>0?`${$} GB${X}`:"—",os:Q})}else l({cpu:"Unavailable",ram:"Unavailable",npu:"Unavailable",gpu:"Unavailable",disk:"Unavailable",os:"Unavailable"});x(!1)},[]);s.useEffect(()=>{i()},[i]);const n=[["CPU",r.cpu],["RAM",r.ram],["NPU",r.npu],["GPU",r.gpu],["Disk",r.disk],["OS",r.os]];return e.jsxs("section",{"aria-label":"System information",children:[e.jsx("h2",{className:"text-lg font-semibold mb-5",children:"System Information"}),e.jsx("div",{className:"rounded-2xl bg-white/[0.04] border border-white/[0.06] overflow-x-auto backdrop-blur-sm",children:e.jsx("table",{className:"w-full text-sm min-w-[360px]",children:e.jsx("tbody",{children:n.map(([c,m])=>e.jsxs("tr",{className:"border-b border-white/5 last:border-0",children:[e.jsx("td",{className:"px-5 py-3 text-shell-text-secondary font-medium w-32",children:c}),e.jsx("td",{className:"px-5 py-3",children:m})]},c))})})}),e.jsxs("div",{className:"mt-3 flex items-center gap-2 flex-wrap",children:[e.jsxs(b,{variant:"outline",size:"sm",onClick:i,disabled:t,children:[e.jsx(P,{size:14,className:t?"animate-spin":""}),"Re-detect Hardware"]}),e.jsxs(b,{variant:"outline",size:"sm",onClick:async()=>{d(!0);try{await fetch("/api/system/restart/prepare",{method:"POST"})}catch{}},"aria-label":"Restart taOS server",children:[e.jsx(P,{size:14}),"Restart Server"]})]}),e.jsx("p",{className:"mt-2 text-xs text-shell-text-tertiary",children:"Restart the server to apply settings changes that require a reload."}),a&&e.jsx(W,{onClose:()=>d(!1)})]})}function Ne(){const[r,l]=s.useState(ye);return s.useEffect(()=>{G("/api/settings/storage",null).then(t=>{t&&Array.isArray(t)?l(t):l([{label:"Models",size:"4.2 GB",bytes:4200,maxBytes:32e3},{label:"Data",size:"1.8 GB",bytes:1800,maxBytes:32e3},{label:"App Catalog",size:"320 MB",bytes:320,maxBytes:32e3}])})},[]),e.jsxs("section",{"aria-label":"Storage usage",children:[e.jsx("h2",{className:"text-lg font-semibold mb-5",children:"Storage Usage"}),e.jsx("div",{className:"space-y-3",children:r.map(t=>e.jsxs(w,{className:"p-4",children:[e.jsxs("div",{className:"flex items-center justify-between mb-2",children:[e.jsx("span",{className:"text-sm font-medium",children:t.label}),e.jsx("span",{className:"text-sm text-shell-text-secondary tabular-nums",children:t.size})]}),e.jsx(be,{value:t.bytes,max:t.maxBytes})]},t.label))})]})}const ve=[{key:"capture_conversations",label:"Conversations",desc:"Messages you send to agents in the Message Hub"},{key:"capture_notes",label:"Notes",desc:"Notes from the Text Editor app"},{key:"capture_files",label:"File activity",desc:"Files you upload or open"},{key:"capture_searches",label:"Search queries",desc:"What you search for in global search"}];function we(){const[r,l]=s.useState(null),[t,x]=s.useState(null),[a,d]=s.useState(null);s.useEffect(()=>{fetch("/api/user-memory/settings").then(n=>n.ok?n.json():null).then(n=>{l(n||{})}).catch(()=>{l({}),d("Could not load memory settings.")}),fetch("/api/user-memory/stats").then(n=>n.ok?n.json():null).then(n=>{n&&x(n)}).catch(()=>{})},[]);const i=(n,c)=>{const m={...r||{},[n]:c};l(m),fetch("/api/user-memory/settings",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({[n]:c})}).then(h=>{h.ok?d(null):d(`Failed to save setting (${h.status})`)}).catch(()=>d("Could not reach backend."))};return r?e.jsxs("section",{"aria-label":"Memory capture settings",children:[e.jsx("h2",{className:"text-lg font-semibold mb-2",children:"Memory Capture"}),e.jsx("p",{className:"text-sm text-shell-text-tertiary mb-5",children:"Choose what activity gets saved to your personal memory index. All data stays on this device."}),a&&e.jsxs("p",{className:"mb-3 text-xs text-amber-400 flex items-center gap-1.5",children:[e.jsx(F,{size:12})," ",a]}),e.jsx("div",{className:"space-y-2",children:ve.map(n=>{const c=!!r[n.key],m=`capture-${String(n.key)}`;return e.jsxs(w,{className:"p-4 flex items-center justify-between gap-3",children:[e.jsxs("div",{className:"flex-1 min-w-0",children:[e.jsx(N,{htmlFor:m,className:"text-sm font-medium text-shell-text",children:n.label}),e.jsx("p",{className:"text-xs text-shell-text-tertiary mt-0.5",children:n.desc})]}),e.jsx(T,{id:m,checked:c,onCheckedChange:h=>i(n.key,h),"aria-label":`Capture ${n.label}`})]},String(n.key))})}),t&&e.jsxs(w,{className:"mt-6 p-4",children:[e.jsx("h3",{className:"text-sm font-medium mb-3",children:"Stored chunks"}),e.jsxs("div",{className:"text-xs text-shell-text-secondary mb-2 tabular-nums",children:["Total: ",t.total]}),Object.keys(t.collections||{}).length>0?e.jsx("ul",{className:"space-y-1 text-xs text-shell-text-tertiary",children:Object.entries(t.collections).map(([n,c])=>e.jsxs("li",{className:"flex justify-between tabular-nums",children:[e.jsx("span",{children:n}),e.jsx("span",{children:c})]},n))}):e.jsx("p",{className:"text-xs text-shell-text-tertiary",children:"No memories captured yet."})]})]}):e.jsxs("section",{"aria-label":"Memory capture settings",children:[e.jsx("h2",{className:"text-lg font-semibold mb-5",children:"Memory Capture"}),e.jsx("p",{className:"text-sm text-shell-text-tertiary",children:"Loading..."})]})}function ke(){const[r,l]=s.useState(null),[t,x]=s.useState(!1),a=async()=>{x(!0),l(null);try{const d=await fetch("/api/backup",{method:"POST"});d.ok?l("Backup created successfully."):l(`Backup failed (${d.status}). API may not be available yet.`)}catch{l("Could not reach backup endpoint. API not available yet.")}x(!1)};return e.jsxs("section",{"aria-label":"Backup and restore",children:[e.jsx("h2",{className:"text-lg font-semibold mb-5",children:"Backup & Restore"}),e.jsxs(w,{className:"p-4 space-y-4",children:[e.jsxs("div",{children:[e.jsx("h3",{className:"text-sm font-medium mb-2",children:"Create Backup"}),e.jsx("p",{className:"text-xs text-shell-text-tertiary mb-3",children:"Export all agents, memory, and configuration as a backup archive."}),e.jsxs(b,{size:"sm",onClick:a,disabled:t,children:[e.jsx(K,{size:14,className:t?"animate-bounce":""}),t?"Creating...":"Create Backup"]}),r&&e.jsx("p",{className:`mt-2 text-xs ${r.includes("success")?"text-emerald-400":"text-amber-400"}`,children:r})]}),e.jsx("hr",{className:"border-white/5"}),e.jsxs("div",{children:[e.jsx("h3",{className:"text-sm font-medium mb-2",children:"Restore from Backup"}),e.jsx("p",{className:"text-xs text-shell-text-tertiary mb-3",children:"Upload a previously created backup archive to restore."}),e.jsxs("label",{className:"flex flex-col items-center gap-2 p-6 rounded-lg border-2 border-dashed border-white/10 hover:border-white/20 transition-colors cursor-pointer",children:[e.jsx(me,{size:24,className:"text-shell-text-tertiary"}),e.jsx("span",{className:"text-xs text-shell-text-tertiary",children:"Click to select a backup file"}),e.jsx("input",{type:"file",accept:".tar.gz,.zip,.bak",className:"hidden","aria-label":"Upload backup file"})]})]})]})]})}function W({onClose:r}){const[l,t]=s.useState(null),[x,a]=s.useState(!1);s.useEffect(()=>{let n=!1,c=null,m=null,h=!1;const p=()=>{h||n||(h=!0,c&&clearInterval(c),m=setInterval(async()=>{if(!n)try{(await fetch("/api/settings/update-status")).ok&&(a(!0),m&&clearInterval(m),setTimeout(()=>{n||window.location.reload()},500))}catch{}},2e3))};return c=setInterval(async()=>{if(!n)try{const o=await fetch("/api/system/restart/status");if(o.ok){const j=await o.json();t(j),j.phase==="restarting"&&p()}}catch{p()}},1e3),()=>{n=!0,c&&clearInterval(c),m&&clearInterval(m)}},[]);const d=l?Object.entries(l.agents):[];function i(n){return n==="ready"?e.jsx("span",{className:"text-[10px] px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-300",children:"ready"}):n==="timeout"?e.jsx("span",{className:"text-[10px] px-1.5 py-0.5 rounded bg-amber-500/20 text-amber-300",children:"timeout"}):n==="error"?e.jsx("span",{className:"text-[10px] px-1.5 py-0.5 rounded bg-red-500/20 text-red-300",children:"error"}):e.jsxs("span",{className:"text-[10px] px-1.5 py-0.5 rounded bg-sky-500/20 text-sky-300 flex items-center gap-1",children:[e.jsx(P,{size:10,className:"animate-spin"}),n]})}return e.jsx("div",{role:"dialog","aria-modal":"true","aria-label":"Restart progress",className:"fixed inset-0 z-50 flex items-center justify-center bg-black/60",children:e.jsxs("div",{className:"bg-shell-surface border border-white/10 rounded-xl p-6 w-full max-w-md shadow-xl space-y-4",children:[e.jsx("h3",{className:"text-base font-semibold",children:x?"Restarted — reloading…":d.length>0?"Preparing agents for restart":"Restarting server…"}),d.length>0&&e.jsx("ul",{className:"space-y-1","aria-label":"Agent preparation status",children:d.map(([n,c])=>e.jsxs("li",{className:"flex items-center justify-between text-sm",children:[e.jsx("span",{className:"text-shell-text-secondary",children:n}),i(c.status)]},n))}),(l==null?void 0:l.phase)==="restarting"&&!x&&e.jsx("p",{className:"text-xs text-shell-text-tertiary",children:"Waiting for server to come back…"}),!l&&e.jsxs("p",{className:"text-xs text-shell-text-tertiary flex items-center gap-1",children:[e.jsx(P,{size:12,className:"animate-spin"})," Connecting…"]}),e.jsx("div",{className:"flex justify-end",children:e.jsx(b,{variant:"outline",size:"sm",onClick:r,"aria-label":"Cancel restart progress dialog",children:"Cancel"})})]})})}function Se(){const[r,l]=s.useState(!1),[t,x]=s.useState(!1),[a,d]=s.useState(null),[i,n]=s.useState(null),[c,m]=s.useState({check_enabled:!0,auto_apply:!1,auto_restart:!1}),[h,p]=s.useState(null),[o,j]=s.useState(!1),[v,k]=s.useState(!1);s.useEffect(()=>{(async()=>{try{const y=await fetch("/api/preferences/auto-update");if(y.ok){const u=await y.json();u&&typeof u=="object"&&m({check_enabled:u.check_enabled??!0,auto_apply:u.auto_apply??!1,auto_restart:u.auto_restart??!1})}}catch{}try{const y=await fetch("/api/settings/update-check");y.ok&&d(await y.json())}catch{}try{const y=await fetch("/api/settings/update-status");y.ok&&p(await y.json())}catch{}})()},[]);const S=s.useCallback(async y=>{m(y);try{await fetch("/api/preferences/auto-update",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(y)})}catch{}},[]),g=async()=>{l(!0),n(null);try{const y=await fetch("/api/settings/update-check");if(y.ok){const u=await y.json();d(u),n(u.has_updates?"A new version is available.":"You are up to date.")}else n("Update check not available.")}catch{n("Could not reach update server.")}l(!1)},R=async()=>{x(!0),n(null);try{const y=await fetch("/api/settings/update",{method:"POST"});if(y.ok){const u=await y.json().catch(()=>({}));if(u.status==="restarting")j(!0);else{n(u.message??"Update applied. Restart the server to finish."),k(!0);const U=await fetch("/api/settings/update-check");U.ok&&d(await U.json());const C=await fetch("/api/settings/update-status");C.ok&&p(await C.json())}}else{const u=await y.json().catch(()=>({}));n(u.error??"Update failed.")}}catch{n("Could not apply update.")}x(!1)},_=async()=>{j(!0);try{await fetch("/api/system/restart/prepare",{method:"POST"})}catch{}},I=!!(h!=null&&h.pending_restart_sha);return e.jsxs("section",{"aria-label":"System updates",children:[e.jsx("h2",{className:"text-lg font-semibold mb-5",children:"Updates"}),I&&e.jsxs("div",{className:"mb-4 flex items-center justify-between gap-3 rounded-lg border border-amber-500/30 bg-amber-500/10 px-4 py-3",children:[e.jsxs("div",{className:"flex items-center gap-2 text-sm text-amber-200",children:[e.jsx(F,{size:16,className:"shrink-0"}),e.jsxs("span",{children:["Update pulled — restart to finish applying (",h.pending_restart_sha.slice(0,7),")"]})]}),e.jsx(b,{size:"sm",onClick:_,"aria-label":"Restart server to apply update",children:"Restart now"})]}),e.jsxs(w,{className:"p-4 space-y-4",children:[e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsx("div",{className:"p-2 rounded-lg bg-white/5 text-sky-400",children:e.jsx(xe,{size:20})}),e.jsxs("div",{className:"flex-1 min-w-0",children:[e.jsx("p",{className:"text-sm font-medium",children:"taOS"}),a!=null&&a.has_updates&&a.new_commit?e.jsxs("div",{className:"flex flex-col gap-0.5",children:[e.jsxs("p",{className:"text-xs text-shell-text-tertiary tabular-nums",children:[e.jsx("span",{className:"text-white/40",children:"installed "}),a.current_commit]}),e.jsxs("p",{className:"text-xs text-amber-300/90 tabular-nums",children:[e.jsx("span",{className:"text-amber-300/50",children:"available "}),a.new_commit]})]}):e.jsx("p",{className:"text-xs text-shell-text-tertiary tabular-nums",children:(a==null?void 0:a.current_commit)??"v0.1.0-dev"})]}),(a==null?void 0:a.has_updates)&&e.jsx("span",{className:"text-[10px] px-2 py-1 rounded-full font-semibold bg-amber-500/20 text-amber-300",children:"Update available"})]}),e.jsxs("div",{className:"flex gap-2 flex-wrap",children:[e.jsxs(b,{variant:"outline",size:"sm",onClick:g,disabled:r,children:[e.jsx(P,{size:14,className:r?"animate-spin":""}),r?"Checking...":"Check Now"]}),v?e.jsx(b,{size:"sm",onClick:_,"aria-label":"Restart server to apply update",children:"Restart Now"}):a!=null&&a.has_updates?e.jsx(b,{size:"sm",onClick:R,disabled:t,children:t?"Installing...":"Install Update"}):null]}),i&&e.jsxs("div",{className:"flex items-start gap-2 text-xs",children:[i.includes("up to date")||i.includes("applied")?e.jsx(D,{size:14,className:"text-emerald-400 shrink-0 mt-0.5"}):e.jsx(F,{size:14,className:"text-amber-400 shrink-0 mt-0.5"}),e.jsx("span",{className:"text-shell-text-secondary",children:i})]}),e.jsxs("div",{className:"border-t border-white/5 pt-4 space-y-3",children:[e.jsxs("div",{className:"flex items-center justify-between gap-3",children:[e.jsxs("div",{className:"flex-1 min-w-0",children:[e.jsx(N,{className:"text-sm",children:"Check for updates automatically"}),e.jsx("p",{className:"text-[11px] text-shell-text-tertiary mt-0.5",children:"Polls GitHub hourly and notifies when a new version is available."})]}),e.jsx(T,{checked:c.check_enabled??!0,onCheckedChange:y=>S({...c,check_enabled:y})})]}),e.jsxs("div",{className:"flex items-center justify-between gap-3",children:[e.jsxs("div",{className:"flex-1 min-w-0",children:[e.jsx(N,{className:"text-sm",children:"Install updates automatically"}),e.jsx("p",{className:"text-[11px] text-shell-text-tertiary mt-0.5",children:"Pulls + installs new versions as soon as they're detected. You'll still need to restart the server manually."})]}),e.jsx(T,{checked:c.auto_apply??!1,onCheckedChange:y=>S({...c,auto_apply:y}),disabled:!(c.check_enabled??!0)})]}),e.jsxs("div",{className:"flex items-center justify-between gap-3",children:[e.jsxs("div",{className:"flex-1 min-w-0",children:[e.jsx(N,{className:"text-sm",children:"Automatically restart after update"}),e.jsx("p",{className:"text-[11px] text-shell-text-tertiary mt-0.5",children:c.auto_restart?"Server will restart automatically once an update is pulled.":"We'll remind you every 6 hours when a restart is pending."})]}),e.jsx(T,{checked:c.auto_restart??!1,onCheckedChange:y=>S({...c,auto_restart:y}),"aria-label":"Automatically restart after update"})]})]})]}),o&&e.jsx(W,{onClose:()=>j(!1)})]})}function Ce(){const[r,l]=s.useState(`# taOS Configuration
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard Object.entries against missing agents in restart status.

const d = l ? Object.entries(l.agents) : [] can throw when l exists but l.agents is undefined (valid during partial status responses), crashing the restart dialog.

💡 Proposed fix
-const d=l?Object.entries(l.agents):[];
+const d=l&&l.agents&&typeof l.agents==="object"?Object.entries(l.agents):[];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@static/desktop/assets/SettingsApp-Bjcx0zeF.js` at line 1, The restart dialog
crashes because W's local state variable l can exist while l.agents is
undefined; change the computation of d (currently `const d = l ?
Object.entries(l.agents) : []`) to safely handle missing agents by using a guard
like checking l?.agents or falling back to an empty object before calling
Object.entries (i.e., compute d from Object.entries(l?.agents || {})), update
references in function W where d is used so the dialog no longer throws when
agents is absent.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant