feat(assistant): taOS Assistant slide-over panel v1#293
Conversation
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
📝 WalkthroughWalkthroughThis 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
Sequence DiagramsequenceDiagram
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()
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Review rate limit: 8/10 reviews remaining, refill in 8 minutes and 54 seconds. Comment |
Code Review SummaryStatus: 7 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (11 files)
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(() => {}); |
There was a problem hiding this comment.
WARNING: Silent catch - errors fetching model settings are ignored, potentially leaving user unaware of sync failures.
| return; | ||
| } | ||
|
|
||
| const reader = resp.body.getReader(); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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([ |
There was a problem hiding this comment.
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") { |
There was a problem hiding this comment.
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} |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
SUGGESTION: Implement a maximum message history limit in the store to prevent unbounded memory growth in long sessions.
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (2)
static/desktop/assets/RedditApp-DGneRgew.js (1)
1-1: ⚡ Quick winHarden external links sourced from API (
href: t.url).In the thread rendering, the code sets an anchor
hrefdirectly 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 (typicallyhttp:/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.urlfrom 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 winAdd a first-delta edge-case test for the streaming path.
Right now
appendDeltais 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
📒 Files selected for processing (73)
desktop/src/App.tsxdesktop/src/components/TaosAssistantPanel.test.tsxdesktop/src/components/TaosAssistantPanel.tsxdesktop/src/components/TaosAssistantSettings.tsxdesktop/src/components/TopBar.test.tsxdesktop/src/components/TopBar.tsxdesktop/src/stores/taos-agent-store.test.tsdesktop/src/stores/taos-agent-store.tsdesktop/tsconfig.tsbuildinfodocs/taos-agent-manual.mdstatic/desktop/assets/ActivityApp-Z5UbYkZK.jsstatic/desktop/assets/AgentBrowsersApp-CS8kkDZb.jsstatic/desktop/assets/AgentsApp-oeyIx2CM.jsstatic/desktop/assets/BrowserApp-XO3njq3j.jsstatic/desktop/assets/CalendarApp-Brs-2T1n.jsstatic/desktop/assets/ChannelsApp-CaF4mq21.jsstatic/desktop/assets/ClusterApp-D1ErkLX6.jsstatic/desktop/assets/ContactsApp-CdNipJUp.jsstatic/desktop/assets/FilesApp-DKYL9cJH.jsstatic/desktop/assets/GitHubApp-s-aeJRVW.jsstatic/desktop/assets/ImageViewerApp-DKngQM4v.jsstatic/desktop/assets/ImagesApp-BpvTm1-d.jsstatic/desktop/assets/ImagesApp-GchtiFDl.jsstatic/desktop/assets/ImportApp-B2Uev60p.jsstatic/desktop/assets/LibraryApp-vS0lU0aO.jsstatic/desktop/assets/MCPApp-DxOd0p9S.jsstatic/desktop/assets/MemoryApp-DK6Ur5V_.jsstatic/desktop/assets/MessagesApp-1uw297YJ.jsstatic/desktop/assets/MobileSplitView-C_LKRlo9.jsstatic/desktop/assets/ModelsApp-BLFdOh5e.jsstatic/desktop/assets/ModelsApp-D7A7YnrI.jsstatic/desktop/assets/ProvidersApp-CH7lQnZr.jsstatic/desktop/assets/RedditApp-DGneRgew.jsstatic/desktop/assets/SecretsApp-DW7hwBWV.jsstatic/desktop/assets/ServiceAppWindow-Cg0YyM4J.jsstatic/desktop/assets/SettingsApp-eGVc_wA9.jsstatic/desktop/assets/StoreApp-C74OKdwl.jsstatic/desktop/assets/TasksApp-CGa7SA4I.jsstatic/desktop/assets/TextEditorApp-BcIOpi8q.jsstatic/desktop/assets/XApp-b2aBeEJR.jsstatic/desktop/assets/YouTubeApp-Dlq5vbij.jsstatic/desktop/assets/chat-B2ixX2ij.jsstatic/desktop/assets/index-BA1Mw07m.jsstatic/desktop/assets/index-BARB8L84.jsstatic/desktop/assets/index-BYnEaLHZ.jsstatic/desktop/assets/index-BbgSA7W6.jsstatic/desktop/assets/index-CPk1r2iP.jsstatic/desktop/assets/index-CeJ92Ly3.jsstatic/desktop/assets/index-Cj48u5dq.jsstatic/desktop/assets/index-CmjrHFAo.jsstatic/desktop/assets/index-CmqwvdUs.jsstatic/desktop/assets/index-D0LAc8hu.jsstatic/desktop/assets/index-DCmdfusE.jsstatic/desktop/assets/index-DKEJwALV.jsstatic/desktop/assets/index-De7g4bJY.jsstatic/desktop/assets/index-DjcsFILK.jsstatic/desktop/assets/index-DpfDARsO.jsstatic/desktop/assets/index-EOSc79Qx.jsstatic/desktop/assets/index-OZk-IoAC.jsstatic/desktop/assets/index-QLOn-36w.jsstatic/desktop/assets/main-BXTXU4kK.jsstatic/desktop/assets/main-BcmIGaB-.jsstatic/desktop/assets/models-D8xGfVt0.jsstatic/desktop/assets/tokens-B3iCBgSC.cssstatic/desktop/assets/tokens-BWV4-NFa.jsstatic/desktop/assets/tokens-CB8qn41V.cssstatic/desktop/assets/vendor-codemirror-D5oyQsmH.jsstatic/desktop/assets/vendor-icons-D4KyVt4P.jsstatic/desktop/chat.htmlstatic/desktop/index.htmltests/test_taos_agent_route.pytinyagentos/app.pytinyagentos/routes/taos_agent.py
💤 Files with no reviewable changes (2)
- static/desktop/assets/ModelsApp-D7A7YnrI.js
- static/desktop/assets/ImagesApp-BpvTm1-d.js
|
|
||
| const toggleLaunchpad = useCallback(() => setLaunchpadOpen((v) => !v), []); | ||
| const toggleSearch = useCallback(() => setSearchOpen((v) => !v), []); | ||
| const toggleAssistant = useCallback(() => useTaosAgentStore.getState().togglePanel(), []); |
There was a problem hiding this comment.
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.
| <ShortcutProvider> | ||
| <SystemShortcuts toggleSearch={toggleSearch} toggleLaunchpad={toggleLaunchpad} /> | ||
| <SystemShortcuts toggleSearch={toggleSearch} toggleLaunchpad={toggleLaunchpad} toggleAssistant={toggleAssistant} /> |
There was a problem hiding this comment.
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.
| const [settingsOpen, setSettingsOpen] = useState(false); | ||
| const messagesEndRef = useRef<HTMLDivElement>(null); |
There was a problem hiding this comment.
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 openAlso 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.
| // 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 }); | ||
|
|
There was a problem hiding this comment.
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.
| // 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.
| 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) { |
There was a problem hiding this comment.
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.
| 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).
| 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 { |
There was a problem hiding this comment.
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.
| 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.
| ## Chat system | ||
|
|
||
| For deep detail, refer to `docs/chat-guide.md`. Here is a quick reference. | ||
|
|
There was a problem hiding this comment.
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).
| ``` | ||
| @don can you summarise this file? | ||
| ``` | ||
|
|
||
| Address all agents in the channel: | ||
| ``` | ||
| @all let's brainstorm ideas for the landing page | ||
| ``` |
There was a problem hiding this comment.
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.
| ``` | |
| @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}; | |||
There was a problem hiding this comment.
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.
Summary
Ctrl+/keyboard shortcut that opens a 400px slide-over chat panelModelPickerFlow; settings persisted viadesktop_settingspreference namespacetaos_agentdocs/taos-agent-manual.md(covers taOS apps, chat primitives, beads verbs, architecture)New files
tinyagentos/routes/taos_agent.py—GET/PATCH /api/taos-agent/settings,POST /api/taos-agent/chatdesktop/src/stores/taos-agent-store.ts— zustand store:isOpen,messages,model,streamingdesktop/src/components/TaosAssistantPanel.tsx— slide-over paneldesktop/src/components/TaosAssistantSettings.tsx— model picker modaldocs/taos-agent-manual.md— system-prompt manual (~1 400 tokens)Design decisions
EventSource(SSE) but that requires a GET. Chat needs POST (messages body). UsedStreamingResponsewithapplication/x-ndjsonand aReadableStream+TextDecoderfetch on the frontend — same pattern as the LiteLLM proxy's own SSE, just consumed differently.ModelPickerFlowdirectly (same 3-screen source → provider → list flow agents use). The modal is a thin wrapper matchingModelPickerModal's pattern.@keyframesin 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 passcd 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/desktop, click ✨ button — panel slides in from rightSummary by CodeRabbit
Release Notes
New Features
Documentation
Tests