Skip to content

feat(assistant): taOS Assistant slide-over panel v1#293

Merged
jaylfc merged 1 commit into
masterfrom
feat/taos-assistant
May 1, 2026
Merged

feat(assistant): taOS Assistant slide-over panel v1#293
jaylfc merged 1 commit into
masterfrom
feat/taos-assistant

Conversation

@jaylfc
Copy link
Copy Markdown
Owner

@jaylfc jaylfc commented May 1, 2026

Summary

  • Adds a ✨ Sparkles button to the top bar (left of Search) and Ctrl+/ keyboard shortcut that opens a 400px slide-over chat panel
  • Q&A only — no tool calls in v1. Model picker reuses ModelPickerFlow; settings persisted via desktop_settings preference namespace taos_agent
  • Streams token-by-token responses from the LiteLLM proxy as NDJSON; first-use empty state prompts model selection when none configured
  • System prompt loaded at startup from docs/taos-agent-manual.md (covers taOS apps, chat primitives, beads verbs, architecture)
  • Chat history is session-only in zustand (no DB persistence — v2 item)

New files

  • tinyagentos/routes/taos_agent.pyGET/PATCH /api/taos-agent/settings, POST /api/taos-agent/chat
  • desktop/src/stores/taos-agent-store.ts — zustand store: isOpen, messages, model, streaming
  • desktop/src/components/TaosAssistantPanel.tsx — slide-over panel
  • desktop/src/components/TaosAssistantSettings.tsx — model picker modal
  • docs/taos-agent-manual.md — system-prompt manual (~1 400 tokens)

Design decisions

  • NDJSON over SSE: the existing canvas uses EventSource (SSE) but that requires a GET. Chat needs POST (messages body). Used StreamingResponse with application/x-ndjson and a ReadableStream + TextDecoder fetch on the frontend — same pattern as the LiteLLM proxy's own SSE, just consumed differently.
  • Settings modal: reused ModelPickerFlow directly (same 3-screen source → provider → list flow agents use). The modal is a thin wrapper matching ModelPickerModal's pattern.
  • No default model: per spec — the first-use state blocks input until the user picks a model.
  • CSS animation: inline @keyframes in the panel component rather than adding to global CSS, keeping the component self-contained.

Test plan

  • python3 -m pytest tests/test_taos_agent_route.py -v — 5 tests, all pass
  • cd desktop && npm test -- --run — 269 tests, all pass (8 store tests + 7 panel tests + 3 TopBar tests included)
  • cd desktop && npm run build — clean TypeScript build, no errors
  • Open /desktop, click ✨ button — panel slides in from right
  • Panel shows "Pick a model to get started" before a model is selected
  • Click "Choose a model", pick from local/worker/cloud, model saves
  • Type a question, Cmd+Enter sends — tokens stream in
  • Close by clicking ✨ again, pressing Ctrl+/, or clicking outside the panel
  • History persists while panel is toggled; clears on page reload
  • Settings cog opens model picker, changing model updates displayed model

Summary by CodeRabbit

Release Notes

  • New Features

    • Added AI assistant panel with real-time chat capabilities and model selection settings.
    • Assistant toggles via Ctrl+/ keyboard shortcut or dedicated top bar button.
    • Streaming support for real-time message responses.
  • Documentation

    • Added operator manual for taOS Assistant covering usage, configuration, and architecture.
  • Tests

    • Added comprehensive test coverage for new assistant components and state management.

Adds a system-wide taOS Assistant accessible from the top bar:

- ✨ Sparkles button in TopBar (left of Search), Ctrl+/ shortcut
- 400px slide-over panel with 300ms ease-out animation, transparent
  backdrop closes on click
- NDJSON streaming chat completion via LiteLLM proxy
- Model picker reuses ModelPickerFlow; settings persisted in
  desktop_settings under the 'taos_agent' preference namespace
- First-use empty state prompts model selection when none configured
- Session-only history (zustand store, resets on page reload)
- System prompt loaded from docs/taos-agent-manual.md at startup
- Backend: GET/PATCH /api/taos-agent/settings, POST /api/taos-agent/chat
- 5 backend tests, 8 store unit tests, 7 panel render tests, 3 TopBar tests
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 1, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a new taOS Assistant feature with a Zustand-powered state store, chat UI panel, model selection modal, and integration into the main application through system shortcuts and UI controls. The implementation includes comprehensive tests and documentation.

Changes

Cohort / File(s) Summary
taOS Agent Store
desktop/src/stores/taos-agent-store.ts, desktop/src/stores/taos-agent-store.test.ts
New Zustand store managing assistant panel state (open/closed, messages, selected model, streaming status) with actions for toggling/opening/closing panel, appending/clearing messages, and updating streaming state.
TaosAssistantPanel
desktop/src/components/TaosAssistantPanel.tsx, desktop/src/components/TaosAssistantPanel.test.tsx
New component rendering slide-over assistant UI with message history, model-aware input validation, Cmd/Ctrl+Enter send support, and streaming response handling via JSON-line parsing and delta accumulation.
TaosAssistantSettings
desktop/src/components/TaosAssistantSettings.tsx
New modal component fetching available AI models from multiple sources (/api/models, worker/cloud providers), aggregating them into a unified selection UI, and persisting selection via PATCH request.
App Integration
desktop/src/App.tsx, desktop/src/components/TopBar.tsx, desktop/src/components/TopBar.test.tsx
Wires assistant panel into main app with toggleAssistant handler bound to Ctrl+/ shortcut; adds assistant-open button to TopBar with accompanying test coverage.
Documentation
docs/taos-agent-manual.md
New system manual documenting taOS Assistant role, v1 Q&A limitations, architecture, configuration, and operational workflows.
Build Artifacts
desktop/tsconfig.tsbuildinfo, static/desktop/assets/...
TypeScript build info updated for new components; multiple JS bundles regenerated with updated vendor-icon and dependency module paths; ImagesApp and ModelsApp bundles replaced with new implementations.

Sequence Diagram

sequenceDiagram
    actor User
    participant Panel as TaosAssistantPanel
    participant Store as useTaosAgentStore
    participant Server as Backend API
    participant Settings as TaosAssistantSettings

    User->>Panel: Click assistant button / Ctrl+/
    Panel->>Store: togglePanel()
    Store->>Panel: isOpen = true
    Panel->>Server: GET /api/taos-agent/settings
    Server-->>Panel: current model
    Panel->>Store: setModel(modelId)

    User->>Panel: Type message & send (Ctrl+Enter)
    Panel->>Store: appendMessage({role: user, content})
    Store->>Panel: messages updated
    Panel->>Store: appendMessage({role: assistant, content: ""})
    Panel->>Server: POST /api/taos-agent/chat {messages}
    activate Server
    Server-->>Panel: streaming response (JSON-lines)
    deactivate Server
    
    loop For each delta line
        Panel->>Store: appendDelta(delta)
        Store->>Panel: assistant message updated
        Panel->>Panel: scroll to latest
    end

    User->>Panel: Click settings icon
    Panel->>Settings: open settings modal
    Settings->>Server: GET /api/models
    Server-->>Settings: available models
    User->>Settings: Select model
    Settings->>Server: PATCH /api/taos-agent/settings
    Settings->>Store: setModel(modelId)
    Settings->>Panel: onClose()
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 hop hop, chat we'll pop!
A sparkly assistant takes the top,
From models chosen to streams that flow,
Messages dance in a sliding show,
Wisdom whispered, delta by delta! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(assistant): taOS Assistant slide-over panel v1' accurately describes the main change—introducing a new taOS Assistant slide-over panel feature. It is specific, relates directly to the primary purpose of the changeset, and follows conventional commit conventions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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/taos-assistant

Review rate limit: 8/10 reviews remaining, refill in 8 minutes and 54 seconds.

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 May 1, 2026

Code Review Summary

Status: 7 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 4
Issue Details (click to expand)

WARNING

File Line Issue
desktop/src/components/TaosAssistantPanel.tsx 32 Silent catch - errors fetching model settings are ignored
desktop/src/components/TaosAssistantSettings.tsx 71 If PATCH request fails, local state updated causing inconsistency
desktop/src/stores/taos-agent-store.ts 49 appendDelta assumes last message is assistant, deltas discarded if not

SUGGESTION

File Line Issue
desktop/src/components/TaosAssistantPanel.tsx 72 Add AbortController for chat fetch to prevent leaks
desktop/src/components/TaosAssistantSettings.tsx 29 Use AbortController for fetches to cancel on unmount
desktop/src/components/TaosAssistantPanel.tsx 237 Add maxLength to textarea to limit input length
desktop/src/stores/taos-agent-store.ts 31 Add message history limit to prevent memory growth
Files Reviewed (11 files)
  • desktop/src/App.tsx - 0 issues
  • desktop/src/components/TaosAssistantPanel.tsx - 3 issues
  • desktop/src/components/TaosAssistantPanel.test.tsx - 0 issues
  • desktop/src/components/TaosAssistantSettings.tsx - 2 issues
  • desktop/src/components/TopBar.tsx - 0 issues
  • desktop/src/components/TopBar.test.tsx - 0 issues
  • desktop/src/stores/taos-agent-store.ts - 2 issues
  • desktop/src/stores/taos-agent-store.test.ts - 0 issues
  • desktop/tsconfig.tsbuildinfo - 0 issues
  • docs/taos-agent-manual.md - 0 issues

Fix these issues in Kilo Cloud


Reviewed by grok-code-fast-1:optimized:free · 111,474 tokens

.then((data) => {
if (data?.model !== undefined) setModel(data.model);
})
.catch(() => {});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Silent catch - errors fetching model settings are ignored, potentially leaving user unaware of sync failures.

return;
}

const reader = resp.body.getReader();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Add AbortController to the chat fetch to allow cancellation if the component unmounts during streaming, preventing potential resource leaks.

setModel(modelId);
onClose();
} catch {
// ignore
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: If PATCH request to save model fails, the local state is still updated, which could lead to inconsistency between UI and backend.


async function load() {
try {
const [localRes, workers, providers] = await Promise.all([
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Consider using AbortController for the fetches in the load function to cancel requests if the component unmounts, avoiding unnecessary network activity.

set((s) => {
const msgs = [...s.messages];
const last = msgs[msgs.length - 1];
if (last && last.role === "assistant") {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: appendDelta assumes the last message is from the assistant; if this invariant is broken, deltas will be silently discarded.


{/* Settings modal — rendered above the panel */}
<TaosAssistantSettings
open={settingsOpen}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Add maxLength attribute to the textarea to prevent excessively long inputs that could impact performance or API limits.

export type TaosAgentStore = TaosAgentState & TaosAgentActions;

export const useTaosAgentStore = create<TaosAgentStore>((set) => ({
isOpen: false,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Implement a maximum message history limit in the store to prevent unbounded memory growth in long sessions.

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: 9

🧹 Nitpick comments (2)
static/desktop/assets/RedditApp-DGneRgew.js (1)

1-1: ⚡ Quick win

Harden external links sourced from API (href: t.url).

In the thread rendering, the code sets an anchor href directly from server/API data (t.url). If that value can ever be attacker-controlled or contain unexpected protocols, this is a common security footgun (e.g., javascript: URLs). Even with React-level protections, it’s safer to enforce an allowlist (typically http:/https:) or strip/normalize non-http(s) schemes before rendering.

Please verify at runtime:

  • What React/ReactDOM version is used here (and whether it blocks javascript: hrefs in your setup)?
  • Whether t.url from your Reddit API proxy is sanitized/validated.

If needed, implement protocol allowlisting/sanitization at the API boundary or in the UI helper before rendering.

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

In `@static/desktop/assets/RedditApp-DGneRgew.js` at line 1, The external anchor
in the thread detail (inside the Le render where the anchor uses href: t.url and
displays Ge(t.url)||t.url) can be attacker-controlled; update the rendering to
sanitize/allowlist the URL scheme before using t.url (e.g., only allow
http/https, fall back to a safe placeholder or omit href if invalid). Locate the
anchor in function Le (the a element with href:t.url and aria-label `External
link: ${t.url}`) and add a runtime check that validates/normalizes t.url (strip
unsafe schemes like javascript:, data:, etc. or ensure protocol is http/https)
and use the sanitized value or disable the link when validation fails.
desktop/src/stores/taos-agent-store.test.ts (1)

44-52: ⚡ Quick win

Add a first-delta edge-case test for the streaming path.

Right now appendDelta is only covered when an empty assistant message already exists. Since chat output is streamed token-by-token, it's worth locking down what should happen when the first delta arrives before that placeholder is present.

Suggested test shape
+  it("appendDelta handles the first streamed chunk without a placeholder", () => {
+    const { appendMessage, appendDelta } = useTaosAgentStore.getState();
+    appendMessage({ role: "user", content: "Hi", ts: 1 });
+    appendDelta("Hello");
+    const msgs = useTaosAgentStore.getState().messages;
+    expect(msgs.at(-1)?.role).toBe("assistant");
+    expect(msgs.at(-1)?.content).toBe("Hello");
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/stores/taos-agent-store.test.ts` around lines 44 - 52, Add a test
covering the edge case where appendDelta is called before an assistant
placeholder message exists: call appendMessage with a user message only, then
call appendDelta("Hello") and appendDelta(" world"), and assert that
useTaosAgentStore.getState().messages contains an assistant message whose
content is "Hello world". Reference the existing helpers appendDelta,
appendMessage and the messages array on useTaosAgentStore to locate where to add
the new test.
🤖 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/App.tsx`:
- Line 142: The top-bar button is wired to the toggle action (toggleAssistant ->
useTaosAgentStore.getState().togglePanel()) which will close the panel if it's
already open; change it to call an explicit "open" action on the store instead
so TopBar's onAssistantOpen always opens the assistant. Replace the
toggleAssistant callback to call the store's open method (e.g.,
useTaosAgentStore.getState().openPanel() or setPanelOpen(true) /
openAssistant()) rather than togglePanel(), and update the same pattern where
used at the other occurrence (line ~235) so onAssistantOpen consistently invokes
the open action.
- Around line 252-253: SystemShortcuts is registering the assistant hotkey even
on mobile/tablet where TaosAssistantPanel never renders, so update the usage or
registration so the assistant shortcut is only bound on layouts that actually
show the assistant: add a layout-aware condition (e.g. isDesktop or
showAssistant) in App and either (A) only pass toggleAssistant into
<SystemShortcuts> when that condition is true, or (B) add a prop like
enableAssistantShortcut to SystemShortcuts and inside SystemShortcuts only
register the Ctrl+/ handler when that prop is true; reference SystemShortcuts,
toggleAssistant, and TaosAssistantPanel to locate the relevant code.

In `@desktop/src/components/TaosAssistantPanel.tsx`:
- Around line 20-21: The settingsOpen state can persist across panel
close/reopen; update TaosAssistantPanel to reset settingsOpen when the panel is
closed by adding an effect that watches the panel's visibility prop (e.g.,
open/visible/isOpen) and calls setSettingsOpen(false) whenever the panel closes
(and/or on unmount) so the settings modal won't reopen unexpectedly; reference
the settingsOpen and setSettingsOpen state variables in the new useEffect inside
TaosAssistantPanel.
- Around line 50-57: The user message is being duplicated because
useTaosAgentStore.getState().messages already contains the new message and you
then push it again; update the payload construction in TaosAssistantPanel
(reference useTaosAgentStore.getState().messages, the .slice(0, -1) call and the
payload.push({ role: "user", content" }) line) so you either remove the explicit
payload.push of the user message or change the slice/filter logic to exclude the
placeholder assistant but not drop the newly added user message; ensure the
final payload contains a single instance of the user's message.
- Around line 76-97: The stream handling can drop the final JSON chunk because
the TextDecoder stream buffer and leftover `buf` aren’t processed after the read
loop; after the while loop finishes, flush the decoder (call decoder.decode()
without stream) append that to `buf`, then if `buf.trim()` parse it as JSON and
handle `delta`, `error`, or `done` exactly like inside the loop (use the same
JSON.parse handling and calls to `appendDelta`) so the final chunk isn’t lost;
this touches the reader/decoder loop in TaosAssistantPanel (variables: reader,
decoder, buf, appendDelta).

In `@desktop/src/components/TaosAssistantSettings.tsx`:
- Around line 61-70: The PATCH request in handleSelect currently calls
setModel(modelId) and onClose() even when the HTTP response is an error; update
handleSelect to check the fetch response (e.g., const res = await fetch(...); if
(!res.ok) throw new Error(await res.text() || res.statusText)) and only call
setModel(modelId) and onClose() when the response is successful (res.ok); ensure
the catch block handles/report the error (do not update local state on failure)
so UI stays consistent with backend.

In `@docs/taos-agent-manual.md`:
- Around line 42-45: In the "Chat system" section of docs/taos-agent-manual.md
remove the unreachable reference to `docs/chat-guide.md` (the literal string or
link) and instead inline the essential guidance the assistant needs or delete
the sentence entirely; update the "Chat system" paragraph so it contains only
self-contained, actionable instructions the assistant can follow (refer to the
"Chat system" heading to locate the text to change).
- Around line 49-56: The two unlabeled fenced code blocks containing the example
lines "@don can you summarise this file?" and "@all let's brainstorm ideas for
the landing page" should be updated to include a language label (e.g., "text")
to satisfy markdownlint MD040; locate the two triple-backtick blocks in
docs/taos-agent-manual.md that wrap those lines and change them from plain ```
to ```text so the fences become ```text `@don` can you summarise this file? ```
and ```text `@all` let's brainstorm ideas for the landing page ```.

In `@static/desktop/assets/BrowserApp-XO3njq3j.js`:
- Line 1: The copy feedback timeout in the ee callback uses
setTimeout(()=>T(!1),1500) without cleanup which can call setState after
unmount; fix by keeping the timeout id in a ref (e.g., timerRef via useRef),
clear any existing timeout before creating a new one, assign the new id to
timerRef inside ee, and add an effect cleanup (useEffect with empty deps) that
calls clearTimeout(timerRef.current) to ensure the timer is cleared on unmount;
update references to the T setter and ee callback to use this timerRef.

---

Nitpick comments:
In `@desktop/src/stores/taos-agent-store.test.ts`:
- Around line 44-52: Add a test covering the edge case where appendDelta is
called before an assistant placeholder message exists: call appendMessage with a
user message only, then call appendDelta("Hello") and appendDelta(" world"), and
assert that useTaosAgentStore.getState().messages contains an assistant message
whose content is "Hello world". Reference the existing helpers appendDelta,
appendMessage and the messages array on useTaosAgentStore to locate where to add
the new test.

In `@static/desktop/assets/RedditApp-DGneRgew.js`:
- Line 1: The external anchor in the thread detail (inside the Le render where
the anchor uses href: t.url and displays Ge(t.url)||t.url) can be
attacker-controlled; update the rendering to sanitize/allowlist the URL scheme
before using t.url (e.g., only allow http/https, fall back to a safe placeholder
or omit href if invalid). Locate the anchor in function Le (the a element with
href:t.url and aria-label `External link: ${t.url}`) and add a runtime check
that validates/normalizes t.url (strip unsafe schemes like javascript:, data:,
etc. or ensure protocol is http/https) and use the sanitized value or disable
the link when validation fails.
🪄 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: 86e33ffa-3246-46e8-8668-74dd064051b1

📥 Commits

Reviewing files that changed from the base of the PR and between 9e03305 and 2478490.

📒 Files selected for processing (73)
  • desktop/src/App.tsx
  • desktop/src/components/TaosAssistantPanel.test.tsx
  • desktop/src/components/TaosAssistantPanel.tsx
  • desktop/src/components/TaosAssistantSettings.tsx
  • desktop/src/components/TopBar.test.tsx
  • desktop/src/components/TopBar.tsx
  • desktop/src/stores/taos-agent-store.test.ts
  • desktop/src/stores/taos-agent-store.ts
  • desktop/tsconfig.tsbuildinfo
  • docs/taos-agent-manual.md
  • static/desktop/assets/ActivityApp-Z5UbYkZK.js
  • static/desktop/assets/AgentBrowsersApp-CS8kkDZb.js
  • static/desktop/assets/AgentsApp-oeyIx2CM.js
  • static/desktop/assets/BrowserApp-XO3njq3j.js
  • static/desktop/assets/CalendarApp-Brs-2T1n.js
  • static/desktop/assets/ChannelsApp-CaF4mq21.js
  • static/desktop/assets/ClusterApp-D1ErkLX6.js
  • static/desktop/assets/ContactsApp-CdNipJUp.js
  • static/desktop/assets/FilesApp-DKYL9cJH.js
  • static/desktop/assets/GitHubApp-s-aeJRVW.js
  • static/desktop/assets/ImageViewerApp-DKngQM4v.js
  • static/desktop/assets/ImagesApp-BpvTm1-d.js
  • static/desktop/assets/ImagesApp-GchtiFDl.js
  • static/desktop/assets/ImportApp-B2Uev60p.js
  • static/desktop/assets/LibraryApp-vS0lU0aO.js
  • static/desktop/assets/MCPApp-DxOd0p9S.js
  • static/desktop/assets/MemoryApp-DK6Ur5V_.js
  • static/desktop/assets/MessagesApp-1uw297YJ.js
  • static/desktop/assets/MobileSplitView-C_LKRlo9.js
  • static/desktop/assets/ModelsApp-BLFdOh5e.js
  • static/desktop/assets/ModelsApp-D7A7YnrI.js
  • static/desktop/assets/ProvidersApp-CH7lQnZr.js
  • static/desktop/assets/RedditApp-DGneRgew.js
  • static/desktop/assets/SecretsApp-DW7hwBWV.js
  • static/desktop/assets/ServiceAppWindow-Cg0YyM4J.js
  • static/desktop/assets/SettingsApp-eGVc_wA9.js
  • static/desktop/assets/StoreApp-C74OKdwl.js
  • static/desktop/assets/TasksApp-CGa7SA4I.js
  • static/desktop/assets/TextEditorApp-BcIOpi8q.js
  • static/desktop/assets/XApp-b2aBeEJR.js
  • static/desktop/assets/YouTubeApp-Dlq5vbij.js
  • static/desktop/assets/chat-B2ixX2ij.js
  • static/desktop/assets/index-BA1Mw07m.js
  • static/desktop/assets/index-BARB8L84.js
  • static/desktop/assets/index-BYnEaLHZ.js
  • static/desktop/assets/index-BbgSA7W6.js
  • static/desktop/assets/index-CPk1r2iP.js
  • static/desktop/assets/index-CeJ92Ly3.js
  • static/desktop/assets/index-Cj48u5dq.js
  • static/desktop/assets/index-CmjrHFAo.js
  • static/desktop/assets/index-CmqwvdUs.js
  • static/desktop/assets/index-D0LAc8hu.js
  • static/desktop/assets/index-DCmdfusE.js
  • static/desktop/assets/index-DKEJwALV.js
  • static/desktop/assets/index-De7g4bJY.js
  • static/desktop/assets/index-DjcsFILK.js
  • static/desktop/assets/index-DpfDARsO.js
  • static/desktop/assets/index-EOSc79Qx.js
  • static/desktop/assets/index-OZk-IoAC.js
  • static/desktop/assets/index-QLOn-36w.js
  • static/desktop/assets/main-BXTXU4kK.js
  • static/desktop/assets/main-BcmIGaB-.js
  • static/desktop/assets/models-D8xGfVt0.js
  • static/desktop/assets/tokens-B3iCBgSC.css
  • static/desktop/assets/tokens-BWV4-NFa.js
  • static/desktop/assets/tokens-CB8qn41V.css
  • static/desktop/assets/vendor-codemirror-D5oyQsmH.js
  • static/desktop/assets/vendor-icons-D4KyVt4P.js
  • static/desktop/chat.html
  • static/desktop/index.html
  • tests/test_taos_agent_route.py
  • tinyagentos/app.py
  • tinyagentos/routes/taos_agent.py
💤 Files with no reviewable changes (2)
  • static/desktop/assets/ModelsApp-D7A7YnrI.js
  • static/desktop/assets/ImagesApp-BpvTm1-d.js

Comment thread desktop/src/App.tsx

const toggleLaunchpad = useCallback(() => setLaunchpadOpen((v) => !v), []);
const toggleSearch = useCallback(() => setSearchOpen((v) => !v), []);
const toggleAssistant = useCallback(() => useTaosAgentStore.getState().togglePanel(), []);
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 | 🟡 Minor | ⚡ Quick win

Pass an open action to the top-bar button, not a toggle.

TopBar advertises onAssistantOpen, but this wiring closes the panel when it is already open. That makes the "Open taOS Assistant" button behave like a hidden toggle and diverges from the documented close affordances.

Suggested change
-  const toggleAssistant = useCallback(() => useTaosAgentStore.getState().togglePanel(), []);
+  const toggleAssistant = useCallback(() => useTaosAgentStore.getState().togglePanel(), []);
+  const openAssistant = useCallback(() => useTaosAgentStore.getState().openPanel(), []);
...
-              <TopBar onSearchOpen={toggleSearch} onAssistantOpen={toggleAssistant} />
+              <TopBar onSearchOpen={toggleSearch} onAssistantOpen={openAssistant} />

Also applies to: 235-235

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

In `@desktop/src/App.tsx` at line 142, The top-bar button is wired to the toggle
action (toggleAssistant -> useTaosAgentStore.getState().togglePanel()) which
will close the panel if it's already open; change it to call an explicit "open"
action on the store instead so TopBar's onAssistantOpen always opens the
assistant. Replace the toggleAssistant callback to call the store's open method
(e.g., useTaosAgentStore.getState().openPanel() or setPanelOpen(true) /
openAssistant()) rather than togglePanel(), and update the same pattern where
used at the other occurrence (line ~235) so onAssistantOpen consistently invokes
the open action.

Comment thread desktop/src/App.tsx
Comment on lines 252 to +253
<ShortcutProvider>
<SystemShortcuts toggleSearch={toggleSearch} toggleLaunchpad={toggleLaunchpad} />
<SystemShortcuts toggleSearch={toggleSearch} toggleLaunchpad={toggleLaunchpad} toggleAssistant={toggleAssistant} />
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 | 🟡 Minor | ⚡ Quick win

Don't register the assistant shortcut on layouts that never render the assistant.

In the mobile/tablet branch, SystemShortcuts still binds Ctrl+/, but <TaosAssistantPanel /> is desktop-only. On devices with a hardware keyboard this becomes a silent no-op UI-wise while still mutating store state.

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

In `@desktop/src/App.tsx` around lines 252 - 253, SystemShortcuts is registering
the assistant hotkey even on mobile/tablet where TaosAssistantPanel never
renders, so update the usage or registration so the assistant shortcut is only
bound on layouts that actually show the assistant: add a layout-aware condition
(e.g. isDesktop or showAssistant) in App and either (A) only pass
toggleAssistant into <SystemShortcuts> when that condition is true, or (B) add a
prop like enableAssistantShortcut to SystemShortcuts and inside SystemShortcuts
only register the Ctrl+/ handler when that prop is true; reference
SystemShortcuts, toggleAssistant, and TaosAssistantPanel to locate the relevant
code.

Comment on lines +20 to +21
const [settingsOpen, setSettingsOpen] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
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 | 🟡 Minor | ⚡ Quick win

settingsOpen can persist across panel close/reopen.

If the panel is closed while settings is open, reopening can auto-show the modal unexpectedly because local state is retained.

Suggested fix
   const [settingsOpen, setSettingsOpen] = useState(false);

+  useEffect(() => {
+    if (!isOpen) setSettingsOpen(false);
+  }, [isOpen]);
+
   // Sync model from backend on first open

Also applies to: 117-117

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

In `@desktop/src/components/TaosAssistantPanel.tsx` around lines 20 - 21, The
settingsOpen state can persist across panel close/reopen; update
TaosAssistantPanel to reset settingsOpen when the panel is closed by adding an
effect that watches the panel's visibility prop (e.g., open/visible/isOpen) and
calls setSettingsOpen(false) whenever the panel closes (and/or on unmount) so
the settings modal won't reopen unexpectedly; reference the settingsOpen and
setSettingsOpen state variables in the new useEffect inside TaosAssistantPanel.

Comment on lines +50 to +57
// Build messages payload — only user/assistant (no system; backend injects system)
const history = useTaosAgentStore.getState().messages;
const payload = history
.filter((m) => m.role !== "system")
.slice(0, -1) // exclude the placeholder assistant we just added
.map((m) => ({ role: m.role, content: m.content }));
payload.push({ role: "user", content });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Current user message is sent twice in the chat payload.

After appending messages, history.slice(0, -1) already includes the new user message. payload.push({ role: "user", content }) duplicates it.

Suggested fix
     const payload = history
       .filter((m) => m.role !== "system")
       .slice(0, -1) // exclude the placeholder assistant we just added
       .map((m) => ({ role: m.role, content: m.content }));
-    payload.push({ role: "user", content });
📝 Committable suggestion

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

Suggested change
// Build messages payload — only user/assistant (no system; backend injects system)
const history = useTaosAgentStore.getState().messages;
const payload = history
.filter((m) => m.role !== "system")
.slice(0, -1) // exclude the placeholder assistant we just added
.map((m) => ({ role: m.role, content: m.content }));
payload.push({ role: "user", content });
// Build messages payload — only user/assistant (no system; backend injects system)
const history = useTaosAgentStore.getState().messages;
const payload = history
.filter((m) => m.role !== "system")
.slice(0, -1) // exclude the placeholder assistant we just added
.map((m) => ({ role: m.role, content: m.content }));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/components/TaosAssistantPanel.tsx` around lines 50 - 57, The user
message is being duplicated because useTaosAgentStore.getState().messages
already contains the new message and you then push it again; update the payload
construction in TaosAssistantPanel (reference
useTaosAgentStore.getState().messages, the .slice(0, -1) call and the
payload.push({ role: "user", content" }) line) so you either remove the explicit
payload.push of the user message or change the slice/filter logic to exclude the
placeholder assistant but not drop the newly added user message; ensure the
final payload contains a single instance of the user's message.

Comment on lines +76 to +97
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop() ?? "";
for (const line of lines) {
if (!line.trim()) continue;
try {
const obj = JSON.parse(line) as { delta?: string; done?: boolean; error?: string };
if (obj.error) {
appendDelta(`\n\n_Error: ${obj.error}_`);
} else if (obj.delta) {
appendDelta(obj.delta);
}
// obj.done == true means stream is complete — loop ends naturally
} catch {
// skip malformed lines
}
}
}
} catch (e) {
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 | ⚡ Quick win

Final streamed JSON chunk can be lost if it lacks trailing newline.

The parser processes only lines and discards buf after loop exit, so last delta/error may be dropped.

Suggested fix
       while (true) {
         const { done, value } = await reader.read();
         if (done) break;
         buf += decoder.decode(value, { stream: true });
         const lines = buf.split("\n");
         buf = lines.pop() ?? "";
         for (const line of lines) {
           if (!line.trim()) continue;
           try {
             const obj = JSON.parse(line) as { delta?: string; done?: boolean; error?: string };
             if (obj.error) {
               appendDelta(`\n\n_Error: ${obj.error}_`);
             } else if (obj.delta) {
               appendDelta(obj.delta);
             }
             // obj.done == true means stream is complete — loop ends naturally
           } catch {
             // skip malformed lines
           }
         }
       }
+      if (buf.trim()) {
+        try {
+          const obj = JSON.parse(buf) as { delta?: string; done?: boolean; error?: string };
+          if (obj.error) appendDelta(`\n\n_Error: ${obj.error}_`);
+          else if (obj.delta) appendDelta(obj.delta);
+        } catch {
+          // skip malformed trailing chunk
+        }
+      }
📝 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
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop() ?? "";
for (const line of lines) {
if (!line.trim()) continue;
try {
const obj = JSON.parse(line) as { delta?: string; done?: boolean; error?: string };
if (obj.error) {
appendDelta(`\n\n_Error: ${obj.error}_`);
} else if (obj.delta) {
appendDelta(obj.delta);
}
// obj.done == true means stream is complete — loop ends naturally
} catch {
// skip malformed lines
}
}
}
} catch (e) {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop() ?? "";
for (const line of lines) {
if (!line.trim()) continue;
try {
const obj = JSON.parse(line) as { delta?: string; done?: boolean; error?: string };
if (obj.error) {
appendDelta(`\n\n_Error: ${obj.error}_`);
} else if (obj.delta) {
appendDelta(obj.delta);
}
// obj.done == true means stream is complete — loop ends naturally
} catch {
// skip malformed lines
}
}
}
if (buf.trim()) {
try {
const obj = JSON.parse(buf) as { delta?: string; done?: boolean; error?: string };
if (obj.error) appendDelta(`\n\n_Error: ${obj.error}_`);
else if (obj.delta) appendDelta(obj.delta);
} catch {
// skip malformed trailing chunk
}
}
} catch (e) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/components/TaosAssistantPanel.tsx` around lines 76 - 97, The
stream handling can drop the final JSON chunk because the TextDecoder stream
buffer and leftover `buf` aren’t processed after the read loop; after the while
loop finishes, flush the decoder (call decoder.decode() without stream) append
that to `buf`, then if `buf.trim()` parse it as JSON and handle `delta`,
`error`, or `done` exactly like inside the loop (use the same JSON.parse
handling and calls to `appendDelta`) so the final chunk isn’t lost; this touches
the reader/decoder loop in TaosAssistantPanel (variables: reader, decoder, buf,
appendDelta).

Comment on lines +61 to +70
const handleSelect = async (modelId: string) => {
try {
await fetch("/api/taos-agent/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: modelId }),
});
setModel(modelId);
onClose();
} catch {
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 | ⚡ Quick win

Don’t update local model state when settings PATCH fails.

Right now setModel(modelId) and onClose() run even on HTTP error statuses, which can desync UI state from persisted backend settings.

Suggested fix
   const handleSelect = async (modelId: string) => {
     try {
-      await fetch("/api/taos-agent/settings", {
+      const res = await fetch("/api/taos-agent/settings", {
         method: "PATCH",
         headers: { "Content-Type": "application/json" },
         body: JSON.stringify({ model: modelId }),
       });
+      if (!res.ok) return;
       setModel(modelId);
       onClose();
     } catch {
       // ignore
     }
   };
📝 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
const handleSelect = async (modelId: string) => {
try {
await fetch("/api/taos-agent/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: modelId }),
});
setModel(modelId);
onClose();
} catch {
const handleSelect = async (modelId: string) => {
try {
const res = await fetch("/api/taos-agent/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: modelId }),
});
if (!res.ok) return;
setModel(modelId);
onClose();
} catch {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/components/TaosAssistantSettings.tsx` around lines 61 - 70, The
PATCH request in handleSelect currently calls setModel(modelId) and onClose()
even when the HTTP response is an error; update handleSelect to check the fetch
response (e.g., const res = await fetch(...); if (!res.ok) throw new Error(await
res.text() || res.statusText)) and only call setModel(modelId) and onClose()
when the response is successful (res.ok); ensure the catch block handles/report
the error (do not update local state on failure) so UI stays consistent with
backend.

Comment thread docs/taos-agent-manual.md
Comment on lines +42 to +45
## Chat system

For deep detail, refer to `docs/chat-guide.md`. Here is a quick reference.

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 | ⚡ Quick win

Remove the unreachable docs/chat-guide.md reference from the system prompt.

This file is the prompt the assistant actually gets in v1, so telling it to "refer to" another doc it cannot read encourages invented details instead of grounded answers. Inline the necessary guidance here or drop the reference.

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

In `@docs/taos-agent-manual.md` around lines 42 - 45, In the "Chat system" section
of docs/taos-agent-manual.md remove the unreachable reference to
`docs/chat-guide.md` (the literal string or link) and instead inline the
essential guidance the assistant needs or delete the sentence entirely; update
the "Chat system" paragraph so it contains only self-contained, actionable
instructions the assistant can follow (refer to the "Chat system" heading to
locate the text to change).

Comment thread docs/taos-agent-manual.md
Comment on lines +49 to +56
```
@don can you summarise this file?
```

Address all agents in the channel:
```
@all let's brainstorm ideas for the landing page
```
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 | 🟡 Minor | ⚡ Quick win

Add languages to the fenced examples.

These unlabeled code fences trip markdownlint MD040, so the doc will keep generating avoidable warnings until they're tagged.

Suggested change
-```
+```text
 `@don` can you summarise this file?

...
- +text
@all let's brainstorm ideas for the landing page

📝 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
```
@don can you summarise this file?
```
Address all agents in the channel:
```
@all let's brainstorm ideas for the landing page
```
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 49-49: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 54-54: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

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

In `@docs/taos-agent-manual.md` around lines 49 - 56, The two unlabeled fenced
code blocks containing the example lines "@don can you summarise this file?" and
"@all let's brainstorm ideas for the landing page" should be updated to include
a language label (e.g., "text") to satisfy markdownlint MD040; locate the two
triple-backtick blocks in docs/taos-agent-manual.md that wrap those lines and
change them from plain ``` to ```text so the fences become ```text `@don` can you
summarise this file? ``` and ```text `@all` let's brainstorm ideas for the landing
page ```.

@@ -1 +1 @@
import{r as s,j as e}from"./vendor-react-DqNigfVP.js";import{h as ne,j as F,B as n,C as N,c as z,L as re,T as le}from"./toolbar-DnudFjaC.js";import{ad as oe,b9 as ie,ba as ce,as as S,a1 as A,aB as de,v as M,bb as xe,b0 as me,T as ue,V as he}from"./vendor-icons-CkMcUED-.js";import"./vendor-milkdown-yCoRvi8b.js";import"./vendor-radix-C7_z_uQC.js";import"./vendor-layout-tICHznDU.js";const m="https://duckduckgo.com",pe=[{name:"DuckDuckGo",url:"https://duckduckgo.com",icon:"🦆"},{name:"Wikipedia",url:"https://en.wikipedia.org",icon:"📚"},{name:"GitHub",url:"https://github.com",icon:"🐙"},{name:"Hacker News",url:"https://news.ycombinator.com",icon:"🟧"}];function H(b){return`/api/desktop/proxy?url=${encodeURIComponent(b)}`}function be(b){const l=b.trim();return l?/^https?:\/\//i.test(l)?l:`https://${l}`:m}function V(){return/iPad|iPhone|iPod/.test(navigator.userAgent)||navigator.platform==="MacIntel"&&navigator.maxTouchPoints>1}function Ce({windowId:b,initialUrl:l}){const d=s.useRef(null),E=s.useRef(!1),[r,f]=s.useState(m),[L,u]=s.useState(m),[h,R]=s.useState([m]),[o,g]=s.useState(0),[W,i]=s.useState(!1),[_,c]=s.useState(!1),[K,T]=s.useState(!1),[J,y]=s.useState(!1),[p,q]=s.useState(""),[B,j]=s.useState(null),[I,O]=s.useState(!1),[w,U]=s.useState(V()?"external":"embedded"),k=o>0,C=o<h.length-1,D=s.useCallback(t=>{const a=be(t);f(a),u(a),i(!1),c(!0),R(x=>[...x.slice(0,o+1),a]),g(x=>x+1)},[o]),Q=s.useCallback(()=>{if(!k)return;const t=o-1;g(t);const a=h[t]??m;f(a),u(a),i(!1),c(!0)},[k,h,o]),X=s.useCallback(()=>{if(!C)return;const t=o+1;g(t);const a=h[t]??m;f(a),u(a),i(!1),c(!0)},[C,h,o]),Y=s.useCallback(()=>{i(!1),c(!0),d.current&&(d.current.src=H(r))},[r]),Z=t=>{t.preventDefault(),D(L)},v=s.useCallback(()=>{window.open(r,"_blank","noopener,noreferrer")},[r]),ee=s.useCallback(()=>{navigator.clipboard.writeText(r).then(()=>{T(!0),setTimeout(()=>T(!1),1500)})},[r]),te=s.useCallback(()=>{var t,a;c(!1);try{const x=d.current;if(x)try{const P=x.contentDocument;if(P){const $=((a=(t=P.body)==null?void 0:t.textContent)==null?void 0:a.trim())??"";if($.startsWith("{")&&$.includes('"error"')){i(!0);return}}}catch{}}catch{}},[]),se=s.useCallback(()=>{c(!1),i(!0)},[]);s.useEffect(()=>{V()&&U("external")},[]),s.useEffect(()=>{if(!(!l||E.current))try{const t=new URL(l,window.location.href);if(t.protocol!=="http:"&&t.protocol!=="https:"){console.warn("[BrowserApp] rejected initialUrl with non-http(s) scheme:",t.protocol);return}E.current=!0;const a=t.toString();f(a),u(a),R([a]),g(0),d.current&&(d.current.src=a)}catch(t){console.warn("[BrowserApp] invalid initialUrl:",l,t)}},[]);const G=s.useCallback(()=>{U(t=>t==="embedded"?"external":"embedded"),i(!1)},[]),ae=s.useCallback(async()=>{if(p.trim()){O(!0),j(null);try{const a=await(await fetch("/api/desktop/browser/agent-command",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url:r,command:p})})).json();a.error?j(`Error: ${a.error}${a.install?` (Install: ${a.install})`:""}`):j(a.message||"Command sent")}catch(t){j(`Error: ${t}`)}O(!1)}},[p,r]);return e.jsxs("div",{className:"flex flex-col h-full",children:[e.jsx("form",{onSubmit:Z,children:e.jsxs(ne,{children:[e.jsxs(F,{children:[e.jsx(n,{type:"button",variant:"ghost",size:"icon",onClick:Q,disabled:!k,"aria-label":"Back",children:e.jsx(oe,{size:16})}),e.jsx(n,{type:"button",variant:"ghost",size:"icon",onClick:X,disabled:!C,"aria-label":"Forward",children:e.jsx(ie,{size:16})}),e.jsx(n,{type:"button",variant:"ghost",size:"icon",onClick:Y,"aria-label":"Refresh",children:e.jsx(ce,{size:16,className:_?"animate-spin":""})})]}),e.jsxs("div",{className:"flex-1 min-w-0 flex items-center gap-2 px-3 py-1 rounded-lg bg-shell-bg-deep border border-white/10",children:[e.jsx(S,{size:14,className:"text-shell-text-tertiary shrink-0"}),e.jsx("input",{type:"text",value:L,onChange:t=>u(t.target.value),className:"flex-1 bg-transparent text-sm text-shell-text outline-none placeholder:text-shell-text-tertiary min-w-0",placeholder:"Enter URL","aria-label":"URL"})]}),e.jsxs(F,{children:[e.jsxs(n,{type:"button",variant:"default",size:"sm",onClick:v,"aria-label":"Open in new tab",title:"Open in new tab",children:[e.jsx(A,{size:12}),e.jsx("span",{children:"Open in Tab"})]}),e.jsxs(n,{type:"button",variant:"outline",size:"sm",onClick:G,"aria-label":w==="embedded"?"Switch to external mode":"Switch to embedded mode",title:w==="embedded"?"Embedded mode":"External mode",children:[e.jsx(S,{size:12}),e.jsx("span",{children:w==="embedded"?"Embed":"Ext"})]}),e.jsxs("div",{className:"relative",children:[e.jsx(n,{type:"button",variant:"ghost",size:"icon",onClick:ee,"aria-label":"Copy URL",title:"Copy URL",children:e.jsx(de,{size:14})}),K&&e.jsx("span",{className:"absolute -bottom-6 right-0 text-[10px] bg-shell-surface border border-shell-border rounded px-1 py-0.5 text-shell-text-secondary whitespace-nowrap z-10",children:"Copied!"})]}),e.jsxs(n,{type:"button",variant:"outline",size:"sm",onClick:()=>y(!0),"aria-label":"Agent Browse",title:"Ask an agent to browse",children:[e.jsx(M,{size:12}),e.jsx("span",{children:"Agent"})]})]})]})}),e.jsxs("div",{className:"flex items-center gap-1 px-2 py-1 bg-shell-surface/50 border-b border-shell-border",children:[e.jsx(xe,{size:12,className:"text-shell-text-tertiary shrink-0 mr-1"}),pe.map(t=>e.jsxs("button",{type:"button",onClick:()=>D(t.url),className:"flex items-center gap-1 px-2 py-0.5 rounded text-xs text-shell-text-secondary hover:bg-shell-surface-hover hover:text-shell-text transition-colors","aria-label":`Go to ${t.name}`,children:[e.jsx(me,{size:10,className:"text-shell-text-tertiary"}),e.jsx("span",{children:t.name})]},t.url))]}),w==="external"?e.jsx("div",{className:"flex-1 flex items-center justify-center p-8",children:e.jsx(N,{className:"max-w-md w-full",children:e.jsxs(z,{className:"flex flex-col items-center gap-4 pt-6 text-center",children:[e.jsx(S,{size:48,className:"text-shell-text-tertiary"}),e.jsxs("div",{children:[e.jsx("h3",{className:"text-lg font-medium text-shell-text mb-1",children:"External Browser Mode"}),e.jsx("p",{className:"text-sm text-shell-text-secondary",children:"Pages open in a new browser tab for full compatibility. This is the default on iOS and recommended for sites that don't render well embedded."})]}),e.jsxs(n,{type:"button",onClick:v,children:[e.jsx(A,{size:16}),"Open ",new URL(r).hostname," in Tab"]}),e.jsx(n,{type:"button",variant:"link",size:"sm",onClick:G,children:"Switch to embedded mode"})]})})}):W?e.jsx("div",{className:"flex-1 flex items-center justify-center p-8",children:e.jsx(N,{className:"max-w-md w-full",children:e.jsxs(z,{className:"flex flex-col items-center gap-4 pt-6 text-center",children:[e.jsx(ue,{size:48,className:"text-amber-500"}),e.jsxs("div",{children:[e.jsx("h3",{className:"text-lg font-medium text-shell-text mb-1",children:"Could not load this page"}),e.jsx("p",{className:"text-sm text-shell-text-secondary",children:"This site could not be loaded through the proxy. Open it directly in a new tab instead."})]}),e.jsxs(n,{type:"button",onClick:v,children:[e.jsx(A,{size:16}),"Open in Tab"]}),e.jsx(n,{type:"button",variant:"link",size:"sm",onClick:()=>{i(!1),c(!0)},children:"Try again in embedded mode"})]})})}):e.jsx("iframe",{ref:d,src:H(r),className:"flex-1 w-full border-none bg-white",sandbox:"allow-downloads allow-forms allow-modals allow-pointer-lock allow-popups allow-presentation allow-same-origin allow-scripts",title:"Browser",onLoad:te,onError:se}),J&&e.jsx("div",{className:"fixed inset-0 z-[10002] flex items-center justify-center bg-black/50 backdrop-blur-sm",onClick:()=>y(!1),role:"dialog","aria-modal":"true","aria-label":"Agent Browse command",children:e.jsx(N,{className:"w-[90vw] max-w-md p-4",onClick:t=>t.stopPropagation(),children:e.jsxs(z,{className:"p-0 space-y-3",children:[e.jsxs("div",{children:[e.jsx(re,{children:"Ask an agent to help on this page"}),e.jsx("p",{className:"text-xs text-shell-text-tertiary mt-1",children:'Example: "Find the pricing", "Fill out the contact form", "Extract all product names"'})]}),e.jsx(le,{value:p,onChange:t=>q(t.target.value),placeholder:"What do you want the agent to do?",rows:3}),B&&e.jsx("div",{className:"p-2 rounded bg-white/5 text-xs text-shell-text-secondary border border-white/10",children:B}),e.jsxs("div",{className:"flex gap-2 justify-end",children:[e.jsx(n,{variant:"outline",size:"sm",onClick:()=>y(!1),children:"Cancel"}),e.jsxs(n,{size:"sm",onClick:ae,disabled:I||!p.trim(),children:[I?e.jsx(he,{size:14,className:"animate-spin"}):e.jsx(M,{size:14}),"Run"]})]})]})})})]})}export{Ce as BrowserApp,Ce as default};
import{r as s,j as e}from"./vendor-react-DqNigfVP.js";import{h as ne,j as F,B as n,C as N,c as z,L as re,T as le}from"./toolbar-DnudFjaC.js";import{ad as oe,b9 as ie,ba as ce,as as S,a1 as A,aB as de,v as M,bb as xe,b0 as me,T as ue,Y as he}from"./vendor-icons-D4KyVt4P.js";import"./vendor-milkdown-yCoRvi8b.js";import"./vendor-radix-C7_z_uQC.js";import"./vendor-layout-tICHznDU.js";const m="https://duckduckgo.com",pe=[{name:"DuckDuckGo",url:"https://duckduckgo.com",icon:"🦆"},{name:"Wikipedia",url:"https://en.wikipedia.org",icon:"📚"},{name:"GitHub",url:"https://github.com",icon:"🐙"},{name:"Hacker News",url:"https://news.ycombinator.com",icon:"🟧"}];function H(b){return`/api/desktop/proxy?url=${encodeURIComponent(b)}`}function be(b){const l=b.trim();return l?/^https?:\/\//i.test(l)?l:`https://${l}`:m}function W(){return/iPad|iPhone|iPod/.test(navigator.userAgent)||navigator.platform==="MacIntel"&&navigator.maxTouchPoints>1}function Ce({windowId:b,initialUrl:l}){const d=s.useRef(null),E=s.useRef(!1),[r,f]=s.useState(m),[L,u]=s.useState(m),[h,R]=s.useState([m]),[o,g]=s.useState(0),[_,i]=s.useState(!1),[K,c]=s.useState(!1),[V,T]=s.useState(!1),[J,y]=s.useState(!1),[p,Y]=s.useState(""),[B,j]=s.useState(null),[I,O]=s.useState(!1),[w,U]=s.useState(W()?"external":"embedded"),k=o>0,C=o<h.length-1,D=s.useCallback(t=>{const a=be(t);f(a),u(a),i(!1),c(!0),R(x=>[...x.slice(0,o+1),a]),g(x=>x+1)},[o]),q=s.useCallback(()=>{if(!k)return;const t=o-1;g(t);const a=h[t]??m;f(a),u(a),i(!1),c(!0)},[k,h,o]),Q=s.useCallback(()=>{if(!C)return;const t=o+1;g(t);const a=h[t]??m;f(a),u(a),i(!1),c(!0)},[C,h,o]),X=s.useCallback(()=>{i(!1),c(!0),d.current&&(d.current.src=H(r))},[r]),Z=t=>{t.preventDefault(),D(L)},v=s.useCallback(()=>{window.open(r,"_blank","noopener,noreferrer")},[r]),ee=s.useCallback(()=>{navigator.clipboard.writeText(r).then(()=>{T(!0),setTimeout(()=>T(!1),1500)})},[r]),te=s.useCallback(()=>{var t,a;c(!1);try{const x=d.current;if(x)try{const P=x.contentDocument;if(P){const $=((a=(t=P.body)==null?void 0:t.textContent)==null?void 0:a.trim())??"";if($.startsWith("{")&&$.includes('"error"')){i(!0);return}}}catch{}}catch{}},[]),se=s.useCallback(()=>{c(!1),i(!0)},[]);s.useEffect(()=>{W()&&U("external")},[]),s.useEffect(()=>{if(!(!l||E.current))try{const t=new URL(l,window.location.href);if(t.protocol!=="http:"&&t.protocol!=="https:"){console.warn("[BrowserApp] rejected initialUrl with non-http(s) scheme:",t.protocol);return}E.current=!0;const a=t.toString();f(a),u(a),R([a]),g(0),d.current&&(d.current.src=a)}catch(t){console.warn("[BrowserApp] invalid initialUrl:",l,t)}},[]);const G=s.useCallback(()=>{U(t=>t==="embedded"?"external":"embedded"),i(!1)},[]),ae=s.useCallback(async()=>{if(p.trim()){O(!0),j(null);try{const a=await(await fetch("/api/desktop/browser/agent-command",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url:r,command:p})})).json();a.error?j(`Error: ${a.error}${a.install?` (Install: ${a.install})`:""}`):j(a.message||"Command sent")}catch(t){j(`Error: ${t}`)}O(!1)}},[p,r]);return e.jsxs("div",{className:"flex flex-col h-full",children:[e.jsx("form",{onSubmit:Z,children:e.jsxs(ne,{children:[e.jsxs(F,{children:[e.jsx(n,{type:"button",variant:"ghost",size:"icon",onClick:q,disabled:!k,"aria-label":"Back",children:e.jsx(oe,{size:16})}),e.jsx(n,{type:"button",variant:"ghost",size:"icon",onClick:Q,disabled:!C,"aria-label":"Forward",children:e.jsx(ie,{size:16})}),e.jsx(n,{type:"button",variant:"ghost",size:"icon",onClick:X,"aria-label":"Refresh",children:e.jsx(ce,{size:16,className:K?"animate-spin":""})})]}),e.jsxs("div",{className:"flex-1 min-w-0 flex items-center gap-2 px-3 py-1 rounded-lg bg-shell-bg-deep border border-white/10",children:[e.jsx(S,{size:14,className:"text-shell-text-tertiary shrink-0"}),e.jsx("input",{type:"text",value:L,onChange:t=>u(t.target.value),className:"flex-1 bg-transparent text-sm text-shell-text outline-none placeholder:text-shell-text-tertiary min-w-0",placeholder:"Enter URL","aria-label":"URL"})]}),e.jsxs(F,{children:[e.jsxs(n,{type:"button",variant:"default",size:"sm",onClick:v,"aria-label":"Open in new tab",title:"Open in new tab",children:[e.jsx(A,{size:12}),e.jsx("span",{children:"Open in Tab"})]}),e.jsxs(n,{type:"button",variant:"outline",size:"sm",onClick:G,"aria-label":w==="embedded"?"Switch to external mode":"Switch to embedded mode",title:w==="embedded"?"Embedded mode":"External mode",children:[e.jsx(S,{size:12}),e.jsx("span",{children:w==="embedded"?"Embed":"Ext"})]}),e.jsxs("div",{className:"relative",children:[e.jsx(n,{type:"button",variant:"ghost",size:"icon",onClick:ee,"aria-label":"Copy URL",title:"Copy URL",children:e.jsx(de,{size:14})}),V&&e.jsx("span",{className:"absolute -bottom-6 right-0 text-[10px] bg-shell-surface border border-shell-border rounded px-1 py-0.5 text-shell-text-secondary whitespace-nowrap z-10",children:"Copied!"})]}),e.jsxs(n,{type:"button",variant:"outline",size:"sm",onClick:()=>y(!0),"aria-label":"Agent Browse",title:"Ask an agent to browse",children:[e.jsx(M,{size:12}),e.jsx("span",{children:"Agent"})]})]})]})}),e.jsxs("div",{className:"flex items-center gap-1 px-2 py-1 bg-shell-surface/50 border-b border-shell-border",children:[e.jsx(xe,{size:12,className:"text-shell-text-tertiary shrink-0 mr-1"}),pe.map(t=>e.jsxs("button",{type:"button",onClick:()=>D(t.url),className:"flex items-center gap-1 px-2 py-0.5 rounded text-xs text-shell-text-secondary hover:bg-shell-surface-hover hover:text-shell-text transition-colors","aria-label":`Go to ${t.name}`,children:[e.jsx(me,{size:10,className:"text-shell-text-tertiary"}),e.jsx("span",{children:t.name})]},t.url))]}),w==="external"?e.jsx("div",{className:"flex-1 flex items-center justify-center p-8",children:e.jsx(N,{className:"max-w-md w-full",children:e.jsxs(z,{className:"flex flex-col items-center gap-4 pt-6 text-center",children:[e.jsx(S,{size:48,className:"text-shell-text-tertiary"}),e.jsxs("div",{children:[e.jsx("h3",{className:"text-lg font-medium text-shell-text mb-1",children:"External Browser Mode"}),e.jsx("p",{className:"text-sm text-shell-text-secondary",children:"Pages open in a new browser tab for full compatibility. This is the default on iOS and recommended for sites that don't render well embedded."})]}),e.jsxs(n,{type:"button",onClick:v,children:[e.jsx(A,{size:16}),"Open ",new URL(r).hostname," in Tab"]}),e.jsx(n,{type:"button",variant:"link",size:"sm",onClick:G,children:"Switch to embedded mode"})]})})}):_?e.jsx("div",{className:"flex-1 flex items-center justify-center p-8",children:e.jsx(N,{className:"max-w-md w-full",children:e.jsxs(z,{className:"flex flex-col items-center gap-4 pt-6 text-center",children:[e.jsx(ue,{size:48,className:"text-amber-500"}),e.jsxs("div",{children:[e.jsx("h3",{className:"text-lg font-medium text-shell-text mb-1",children:"Could not load this page"}),e.jsx("p",{className:"text-sm text-shell-text-secondary",children:"This site could not be loaded through the proxy. Open it directly in a new tab instead."})]}),e.jsxs(n,{type:"button",onClick:v,children:[e.jsx(A,{size:16}),"Open in Tab"]}),e.jsx(n,{type:"button",variant:"link",size:"sm",onClick:()=>{i(!1),c(!0)},children:"Try again in embedded mode"})]})})}):e.jsx("iframe",{ref:d,src:H(r),className:"flex-1 w-full border-none bg-white",sandbox:"allow-downloads allow-forms allow-modals allow-pointer-lock allow-popups allow-presentation allow-same-origin allow-scripts",title:"Browser",onLoad:te,onError:se}),J&&e.jsx("div",{className:"fixed inset-0 z-[10002] flex items-center justify-center bg-black/50 backdrop-blur-sm",onClick:()=>y(!1),role:"dialog","aria-modal":"true","aria-label":"Agent Browse command",children:e.jsx(N,{className:"w-[90vw] max-w-md p-4",onClick:t=>t.stopPropagation(),children:e.jsxs(z,{className:"p-0 space-y-3",children:[e.jsxs("div",{children:[e.jsx(re,{children:"Ask an agent to help on this page"}),e.jsx("p",{className:"text-xs text-shell-text-tertiary mt-1",children:'Example: "Find the pricing", "Fill out the contact form", "Extract all product names"'})]}),e.jsx(le,{value:p,onChange:t=>Y(t.target.value),placeholder:"What do you want the agent to do?",rows:3}),B&&e.jsx("div",{className:"p-2 rounded bg-white/5 text-xs text-shell-text-secondary border border-white/10",children:B}),e.jsxs("div",{className:"flex gap-2 justify-end",children:[e.jsx(n,{variant:"outline",size:"sm",onClick:()=>y(!1),children:"Cancel"}),e.jsxs(n,{size:"sm",onClick:ae,disabled:I||!p.trim(),children:[I?e.jsx(he,{size:14,className:"animate-spin"}):e.jsx(M,{size:14}),"Run"]})]})]})})})]})}export{Ce as BrowserApp,Ce as default};
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 | 🟡 Minor | ⚡ Quick win

Clear the “Copied!” timeout on unmount (avoid setState after unmount).

The “Copied!” feedback is cleared via setTimeout(() => T(!1), 1500) but there’s no visible cleanup on component unmount. If the panel/browser component unmounts quickly after copying, this can trigger state updates after unmount (React may warn). Store the timer id and clear it in an effect cleanup.

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

In `@static/desktop/assets/BrowserApp-XO3njq3j.js` at line 1, The copy feedback
timeout in the ee callback uses setTimeout(()=>T(!1),1500) without cleanup which
can call setState after unmount; fix by keeping the timeout id in a ref (e.g.,
timerRef via useRef), clear any existing timeout before creating a new one,
assign the new id to timerRef inside ee, and add an effect cleanup (useEffect
with empty deps) that calls clearTimeout(timerRef.current) to ensure the timer
is cleared on unmount; update references to the T setter and ee callback to use
this timerRef.

@jaylfc jaylfc merged commit 4ee939e into master May 1, 2026
8 checks passed
@jaylfc jaylfc deleted the feat/taos-assistant branch May 1, 2026 15:23
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