Skip to content

superagent#1413

Merged
Kitenite merged 32 commits into
mainfrom
m-agent-two
Feb 15, 2026
Merged

superagent#1413
Kitenite merged 32 commits into
mainfrom
m-agent-two

Conversation

@abhiaiyer91
Copy link
Copy Markdown
Collaborator

@abhiaiyer91 abhiaiyer91 commented Feb 12, 2026

Description

Related Issues

Type of Change

  • Bug fix
  • New feature
  • Documentation
  • Refactor
  • Other (please describe):

Testing

Screenshots (if applicable)

Additional Notes

Summary by CodeRabbit

  • New Features

    • AI chat streaming with interactive tool‑approval flows, resumable streams, and per‑session streaming controls
    • Dynamic multi‑provider model picker with provider grouping and logos
    • New agent/tool UI blocks (expandable agent calls, tool‑specific displays) and interactive user‑question tool
    • Web search and fetch tools plus a Mastra‑based superagent orchestration
  • Improvements

    • Local client streaming hook, faster client chat flow, history hydration, and token usage tracking (per‑turn/session)

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Reworks desktop AI chat to a client-driven streaming model: adds a Mastra-based superagent runtime, per-session streaming/orchestration and approval flows, OAuth credential sourcing, new tRPC streaming/approval endpoints, message hydration/tool-part modeling, many UI components/hooks, and supporting agent tools and packages.

Changes

Cohort / File(s) Summary
Build Configuration
apps/desktop/electron.vite.config.ts
Added "pg-native" to externals for the Electron main build.
AI Chat Router & Auth
apps/desktop/src/lib/trpc/routers/ai-chat/index.ts, .../utils/auth/auth.ts, .../utils/models.ts
Large router expansion: credential sourcing (config/keychain/OAuth), per-session event buses/AbortControllers/runId maps, many new procedures (getModels, getMessages, superagent, superagentStream, abortSuperagent, approveToolCall, approveToolUse, etc.), and exported auth helpers + model listing util.
Agent runtime / packages/agent
packages/agent/package.json, packages/agent/src/index.ts, packages/agent/src/superagent.ts
Introduces Mastra-based superagent: new deps, Postgres-backed storage + Memory, planningAgent and composed superagent, OAuth token handling (setAnthropicAuthToken), and multiple new exports (storage, memory, mastra, superagent, etc.).
ChatInterface core & types
apps/desktop/src/renderer/.../ChatInterface/ChatInterface.tsx, .../types.ts, .../constants.ts
Rewrote to client-side stateful streaming: new props, token usage tracking, DEFAULT_MODEL/READ_ONLY_TOOLS, revised message/type model surface, and changed send/stop and approval flows.
Streaming hook
apps/desktop/src/renderer/.../hooks/useSuperagentStream/useSuperagentStream.ts, .../index.ts
New useSuperagentStream hook: subscribes to aiChat.superagentStream, parses chunk types (text/tool/agent/usage/approval), updates nested message parts, tracks runIds/activeAgentCallId, and surfaces pending approvals; index re-export added.
Message hydration & tool helpers
apps/desktop/src/renderer/.../utils/hydrate-messages.ts, .../utils/tool-helpers.ts
Added hydrateMessages to convert external/history messages into internal ChatMessage/MessagePart model; added tool helper utilities (toToolDisplayState, getArgs, getResult, toWsToolState).
UI components (new)
apps/desktop/src/renderer/.../components/AgentCallBlock/*, .../MastraToolCallBlock/*, .../MessagePartsRenderer/*, .../ReadOnlyToolCall/*, .../ToolApprovalBar/*
New UI components for nested agent/tool rendering and approval UI; index barrels added for re-exports.
Model picker & UI wiring
apps/desktop/src/renderer/.../ModelPicker/ModelPicker.tsx
ModelPicker now queries aiChat.getModels, groups by provider, shows provider logos, and selects DEFAULT_MODEL by default.
Tools
packages/agent/src/tools/*
New agent tools: web_fetch, web_search, ask_user_question (with schemas and execution logic).
Docs / plan
plan.md
Added OAuth/PKCE plan, credential storage approach, tRPC OAuth router design, and UI/migration notes for provider integration.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/UI
    participant ChatIF as ChatInterface
    participant Hook as useSuperagentStream
    participant tRPC as aiChat tRPC
    participant Agent as Superagent/Agent
    participant Store as Storage/Context

    User->>ChatIF: send message (handleSend)
    ChatIF->>tRPC: superagent mutation (session, input, context)
    tRPC->>Store: fetch/inject credentials & context
    tRPC->>Agent: start agent run
    Agent->>tRPC: emit streaming chunks
    tRPC->>Hook: forward chunks via superagentStream subscription

    loop chunk processing
      Hook->>Hook: parse chunk (text/tool/agent/usage/approval)
      Hook->>ChatIF: update messages, parts, usage, pendingApproval
      ChatIF->>User: render message parts / approval UI
    end

    alt tool requires approval
      Agent->>tRPC: emit tool-call-approval
      tRPC->>Hook: mark suspended/runId
      Hook->>ChatIF: show ToolApprovalBar
      User->>ChatIF: approve/decline
      ChatIF->>tRPC: call approveToolCall
      tRPC->>Agent: resume run
      Agent->>tRPC: continue streaming
    end

    Agent->>tRPC: done
    Hook->>ChatIF: finalize message, clear streaming
    ChatIF->>User: display completed response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐇 I nibble logs and stitch the streams,

Parts nest and spin in clever schemes,
Tokens counted, approvals sought,
Agents race to fetch what's taught,
A rabbit cheers this streaming dream.

🚥 Pre-merge checks | ✅ 1 | ❌ 3
❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description contains only unfilled template sections with no actual content describing changes, objectives, or testing performed. Complete the description by filling in all sections: summarize changes, link related issues, select the change type, describe testing, and add context about the implementation.
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'superagent' is vague and generic, providing no meaningful information about the changeset beyond a single term. Expand the title to clearly describe the main change, such as 'Implement superagent-based AI chat streaming' or 'Add AI provider OAuth and superagent orchestration'.
✅ Passed checks (1 passed)
Check name Status Explanation
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch m-agent-two

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

abhiaiyer91 and others added 4 commits February 11, 2026 18:01
# Conflicts:
#	apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx
#	bun.lock
Extract types, constants, utilities, components, and hooks from the
1,537-line ChatInterface.tsx into 10 co-located files for maintainability.
No behavioral changes.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 13, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ✅ Electric Fly.io app
  • ✅ Streams Fly.io app

Thank you for your contribution! 🎉

@Kitenite Kitenite marked this pull request as ready for review February 14, 2026 19:05
Copy link
Copy Markdown
Contributor

@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: 19

🤖 Fix all issues with AI agents
In `@apps/desktop/src/lib/trpc/routers/ai-chat/index.ts`:
- Around line 497-514: The sendMessage mutation currently does nothing (just
logs and returns { success: true }); either restore the original agent dispatch
by uncommenting and re-enabling the chatSessionManager.startAgent(...)
fire-and-forget call (including its .catch(...) error logging) and remove the
stray console.log, or delete the sendMessage procedure entirely if the frontend
now uses a different endpoint (e.g., superagent). Locate the sendMessage
publicProcedure and either re-enable the chatSessionManager.startAgent
invocation with proper error handling or remove the procedure to avoid a no-op
API surface.
- Around line 536-545: Concurrent requests for the same sessionId overwrite the
AbortController in sessionAbortControllers, leaving the previous stream
uncancellable; before creating and storing a new AbortController in the async
block, check sessionAbortControllers.get(input.sessionId) and if one exists call
.abort() on it and remove it, then create the new AbortController and set it
(sessionAbortControllers.set(input.sessionId, abortController)); ensure any
cleanup also updates sessionAbortControllers (and optionally sessionContext)
when streams finish or error to avoid stale controllers.
- Around line 663-679: The catch block for the superagent stream currently emits
an error but doesn't clean up sessionRunIds or sessionContext, causing stale
state; update the catch (error) handler so that if the error is not an
intentional abort (i.e., abortController.signal.aborted is false) you also
delete sessionRunIds.delete(input.sessionId) and
sessionContext.delete(input.sessionId) (the same keys you remove on successful
completion) before emitting the error event, ensuring sessionRunIds,
sessionContext, and sessionAbortControllers are all cleared when a stream fails.
- Around line 198-221: The parseFileMentions function currently uses join(cwd,
relPath) and can read files outside the project (path traversal); after
computing the candidate path (use path.resolve or normalize on join(cwd,
relPath)) verify that the resolved path remains inside cwd (for example by
ensuring path.relative(cwd, resolvedPath) does not start with '..' and is not
absolute outside cwd) and skip adding/reading any mention that escapes cwd;
update parseFileMentions to perform this check before calling safeReadFile so
only files under cwd are read.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx`:
- Around line 173-191: handleApprove/handleDecline fire the mutation and clear
pendingApproval but never update the corresponding tool-call part in messages,
so the UI doesn't reflect the approval/denial; update the messages state
optimistically inside both handlers (before/alongside
approveToolCallMutation.mutate) by finding the message/part that matches
pendingApproval.runId and replacing its tool-call part status or metadata to
indicate approved=true or approved=false, using setMessages to return a new
messages array (immutably) so the UI shows the decision immediately; keep the
existing approveToolCallMutation.mutate call and sessionId/runId usage but
ensure the update targets the same runId and only modifies the tool-call part
(preserving other parts and message fields).
- Around line 79-84: The effect that sets local messages from historyMessages
(useEffect that calls setMessages(hydrateMessages(...))) overwrites in-progress
streaming content when history refetches; modify that effect to guard against
overwriting during an active stream (check an isStreaming flag or similar state
before calling setMessages) or alternatively disable unwanted refetching at the
source (set the tRPC query option refetchOnWindowFocus: false / disable polling)
so hydrateMessages is only applied when not streaming.
- Around line 219-223: The linter flags use of array index as key in the
msg.parts.map within ChatInterface; change the key to a stable composite key
(e.g., combine the parent message identifier and part index/type) instead of
key={i} so keys remain unique and stable across renders — locate the mapping
over msg.parts in the ChatInterface component (the span rendering of part.text)
and replace the index key with something like `${msg.id}-${part.type}-${i}` or
another unique identifier from the message.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/AgentCallBlock/AgentCallBlock.tsx`:
- Line 48: The ChevronDownIcon rotation isn't working because its utility
depends on a parent with the "group" class; update the CollapsibleTrigger
component (the JSX element named CollapsibleTrigger where className is set) to
include "group" in its className (e.g., add the token "group" alongside existing
classes like "flex w-full items-center justify-between gap-4 p-3") so the
group-data-[state=open]:rotate-180 on ChevronDownIcon takes effect.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessagePartsRenderer/MessagePartsRenderer.tsx`:
- Around line 91-137: The switch-to-display mapping and filename-extraction
logic in MessagePartsRenderer (inside the exploringItems map using getArgs(p)
and p.toolName) is duplicated in ReadOnlyToolCall; extract a shared utility
function named getToolDisplayInfo(toolName, args, status) that returns {title,
subtitle, icon, isPending, isError} and centralize the filename-shortening logic
(e.g., take last path segment if subtitle includes "/") inside it, then replace
the switch and post-processing in MessagePartsRenderer and ReadOnlyToolCall to
call getToolDisplayInfo(getArgs(p).toolName? no — call
getToolDisplayInfo(p.toolName, args, p.status) and use its returned fields;
ensure produced titles for mastra_workspace_list_files are consistent
("Listing"/"Listed") and preserve existing icon choices (FileIcon,
FolderTreeIcon, FileSearchIcon, SearchIcon) and error/pending semantics.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/ModelPicker.tsx`:
- Line 42: The ModelPicker is calling electronTrpc.aiChat.getModels.useQuery but
ignores isLoading and error, leaving the selector blank on load/failure; update
the hook call to destructure isLoading and error alongside data (e.g., const {
data: models, isLoading, error } = electronTrpc.aiChat.getModels.useQuery()),
then render a loading option or spinner and disable the select when isLoading,
and render a clear error message or fallback option when error is present
(inside the ModelPicker component) so users see progress or failure instead of
an empty selector.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/constants.ts`:
- Around line 17-25: The READ_ONLY_TOOLS Set in constants.ts incorrectly
includes mutating entries mastra_workspace_mkdir and mastra_workspace_delete;
either remove those two entries so mkdir and delete go through the existing
approval flow (ToolApprovalBar) or, if the intent is to auto-approve them,
rename READ_ONLY_TOOLS to a clearer name like INLINE_TOOLS or
AUTO_APPROVED_TOOLS and update usages (e.g., ReadOnlyToolCall consumers) so
semantics match; update all references to READ_ONLY_TOOLS/ReadOnlyToolCall or
ToolApprovalBar accordingly to maintain correct UI behavior.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSuperagentStream/useSuperagentStream.ts`:
- Around line 616-644: The debug console.log inside the "tool-call-approval"
branch is leaking approval payloads; remove or guard the line that logs
JSON.stringify(chunk). Locate the switch case for "tool-call-approval" in
useSuperagentStream (the block that references runIdRef.current and calls
setPendingApproval) and either delete the console.log(...) call or wrap it
behind a development-only flag (e.g., process.env.NODE_ENV === "development") so
only non-production builds emit the chunk contents; keep the rest of the
approval handling (approvalData construction, setPendingApproval, and the
runIdRef.current check) unchanged.
- Around line 433-464: handleTopLevelChunk contains several comparisons between
existing tool-call parts and chunkToolCallId using raw equality which can fail
if chunkToolCallId is non-string; update every comparison in handleTopLevelChunk
(e.g., the parts.find and parts.map checks inside updateLastAssistant and any
other checks that compare pt.toolCallId to chunkToolCallId) to coerce
chunkToolCallId with String(chunkToolCallId) (or coerce pt.toolCallId
consistently) before comparing so the equality checks match the string IDs
(ensure you update all instances referenced in this diff such as the find at the
top and the map branch that builds or updates the "tool-call" part).
- Around line 193-208: The non-null assertions p.text! should be removed: after
the guard if (!p?.text) break; extract the value into a local const (e.g., const
text = p.text) and use that variable when creating/updating parts (inside the
updateActiveAgentParts callback where you currently reference p.text!). Apply
the same change in the other "text-delta" handler (the top-level handler around
the second occurrence) so both uses avoid the non-null assertion; reference the
parameter p and the updateActiveAgentParts call to locate the spots.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/utils/hydrate-messages.ts`:
- Around line 10-32: The debug console.log in hydrate-messages.ts is leaking raw
message content; remove the console.log call that prints historyMessages (the
JSON.stringify block referencing historyMessages.slice(...)) or wrap it behind a
proper debug flag/logger check (e.g., only log when DEBUG or a dev-only logger
is enabled). Locate the console.log in the hydrate messages utility and either
delete it or replace it with a conditional debug-level log using your app's
logger (so production builds never print raw message content).

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/utils/tool-helpers.ts`:
- Around line 4-10: Handle the edge case where a ToolCallPart has status ===
"done" but no result and no isError: update toToolDisplayState to check for
part.status === "done" (before the final fallthrough) and return
"output-available" for a completed-but-empty-result call; mirror the same logic
in toWsToolState so both mappings treat a finished-but-empty tool call as an
output-available state rather than "input-available".

In `@packages/agent/src/index.ts`:
- Line 15: index.ts re-exporting "./superagent" causes PostgresStore to be
instantiated at module import, forcing DATABASE_URL/DB connection for all
consumers; remove the direct re-export from packages/agent/src/index.ts and
either (A) expose superagent via a separate entrypoint (e.g., create a dedicated
export path "@superset/agent/superagent" that re-exports the module) or (B)
change superagent.ts to lazy-initialize PostgresStore (provide a
getSuperagent/getStore factory function that constructs PostgresStore on first
use instead of at top-level). Update any callers to import the new entrypoint or
call the factory (referencing ./superagent, PostgresStore, and the current
top-level exports like createPermissionRequest/getActiveSessionCount to ensure
they no longer trigger DB initialization).

In `@packages/agent/src/superagent.ts`:
- Around line 11-14: The module currently uses a non-null assertion on
process.env.DATABASE_URL when constructing storage with new PostgresStore, which
the linter flags and which can produce unclear runtime failures at module load;
update the module to explicitly validate that process.env.DATABASE_URL is set
before creating storage (e.g., read process.env.DATABASE_URL into a local
variable, if falsy throw a clear Error like "DATABASE_URL is required" or return
a guarded factory), and then pass that validated string into new PostgresStore
so storage is only constructed with a guaranteed connection string.

In `@plan.md`:
- Around line 46-51: The current approach mutates process.env (e.g., setting
process.env.ANTHROPIC_API_KEY or process.env.OPENAI_API_KEY) around calls in the
superagent mutation (before superagent.stream() at the code referenced around
line 522) and the approveToolCall resumption path, which creates a race;
instead, refactor so you do NOT write to process.env: retrieve the provider
token from the credential store, then pass that token directly into the Mastra
agent/model constructor or into superagent.stream()/request options (or whatever
per-request auth API the Mastra/agent client exposes) for the specific call (use
the parsed provider from modelId such as "anthropic" to pick token), and remove
the set/restore of the env var; update both the superagent mutation and the
approveToolCall resumption code paths to use this per-request credential
injection.
🧹 Nitpick comments (12)
packages/agent/src/superagent.ts (2)

11-21: Module-level side effects: all infrastructure is instantiated at import time.

PostgresStore, Memory, Agent, and Mastra are all created as top-level constants. This means any module that imports from packages/agent (directly or transitively) will:

  • Attempt a PostgreSQL connection immediately
  • Fail hard if DATABASE_URL is missing
  • Make isolated unit testing difficult without a live database

If this is intentional (e.g., this module is only ever imported in a controlled server context), consider adding a brief comment documenting that assumption. Otherwise, consider lazy initialization via a factory function.

Also applies to: 66-92, 160-196


70-74: requestContext.get("modelId") — consider validating the returned value.

Both agents use a dynamic model resolver that trusts whatever string requestContext.get("modelId") returns. If a consumer sets modelId to a non-empty but invalid provider string, the agent will fail at generation time with an opaque error. A lightweight guard (e.g., checking the string matches a known pattern like provider/model) would improve debuggability.

Also applies to: 164-168

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/types.ts (1)

69-82: MastraChunk.type is broadly typed as string.

If the set of chunk types is known and finite, a string literal union would provide better type safety and autocomplete for consumers. If the types are open-ended or come from an external library, string is acceptable.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/constants.ts (1)

3-8: Hardcoded default model.

Consider whether DEFAULT_MODEL should be configurable (e.g., via user settings or environment) rather than hardcoded to a specific Anthropic model. This is fine for now but may become a maintenance burden as models evolve.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ToolApprovalBar/ToolApprovalBar.tsx (1)

44-49: Unnecessary as string cast.

JSON.stringify() already returns string; the cast is redundant.

-							: (JSON.stringify(pendingApproval.args, null, 2) as string)}
+							: JSON.stringify(pendingApproval.args, null, 2)}
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/ModelPicker.tsx (1)

69-93: providerToLogo called redundantly inside the render loop.

providerToLogo(model.provider) on line 72 is called for every model on every render. Since the provider is already the group key, you can compute it once per group.

♻️ Suggested optimization
-				{Object.entries(groupedModels).map(([provider, providerModels]) => (
-					<ModelSelectorGroup key={provider} heading={provider}>
-						{providerModels.map((model) => {
-							const logo = providerToLogo(model.provider);
+				{Object.entries(groupedModels).map(([provider, providerModels]) => {
+					const logo = providerToLogo(provider);
+					return (
+					<ModelSelectorGroup key={provider} heading={provider}>
+						{providerModels.map((model) => {
							return (
								<ModelSelectorItem
									key={model.id}
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/utils/hydrate-messages.ts (1)

82-112: Agent vs. tool-call detection relies solely on name prefix convention.

The tName.startsWith("agent-") check (line 82) determines whether a tool part is rendered as an AgentCallPart or a ToolCallPart. If a regular tool name happens to start with "agent-", it will be misclassified. This is likely fine if the naming convention is enforced upstream, but worth noting as a coupling assumption.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessagePartsRenderer/MessagePartsRenderer.tsx (1)

28-34: renderParts is recreated every render and passed to AgentCallBlock.

The inner renderParts function captures isStreaming from the closure and is passed as a prop to AgentCallBlock. Since it's not memoized (e.g., via useCallback), AgentCallBlock will receive a new function reference on every render, potentially causing unnecessary re-renders of the collapsible tree. For now this is fine given the streaming context, but consider memoizing if performance becomes an issue.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSuperagentStream/useSuperagentStream.ts (1)

367-383: handleTopLevelChunk takes 12 parameters — consider a params object.

This standalone function receives 12 positional arguments, making call sites hard to read and error-prone. Wrapping them in a single options object would improve clarity and reduce positional mistakes.

apps/desktop/src/lib/trpc/routers/ai-chat/index.ts (3)

69-75: execSync blocks the event loop — consider execFile (async).

safeExec is called up to 3 times in gatherProjectContext (git branch, status, log), each with a 3-second timeout. Since execSync is synchronous, this can block the Node.js event loop for up to 9 seconds in the worst case, stalling all other IPC handlers and subscriptions during that window.

Suggested async alternative
-import { execSync } from "node:child_process";
+import { execFile } from "node:child_process";
+import { promisify } from "node:util";
+
+const execFileAsync = promisify(execFile);

-function safeExec(cmd: string, cwd: string, timeoutMs = 3_000): string {
+async function safeExec(cmd: string, cwd: string, timeoutMs = 3_000): Promise<string> {
 	try {
-		return execSync(cmd, { cwd, timeout: timeoutMs, encoding: "utf-8" }).trim();
+		const { stdout } = await execFileAsync("git", cmd.replace(/^git\s+/, "").split(/\s+/), { cwd, timeout: timeoutMs });
+		return stdout.trim();
 	} catch {
 		return "";
 	}
 }

This would also require making gatherProjectContext async.


314-334: Remove or gate debug logging behind a flag.

The [getMessages] debug block iterates messages and logs tool-part JSON to the console on every query call. This is verbose for production use and may inadvertently log sensitive tool arguments or results.


556-557: Misleading operator precedence — use explicit grouping or a ternary.

const contextInstructions = projectContext + fileMentionContext || undefined;

This evaluates as (projectContext + fileMentionContext) || undefined due to + binding tighter than ||. It happens to work (empty string concatenation is falsy), but reads as though || applies only to fileMentionContext.

Clearer alternative
-const contextInstructions =
-    projectContext + fileMentionContext || undefined;
+const combined = projectContext + fileMentionContext;
+const contextInstructions = combined || undefined;

Comment on lines +198 to +221
function parseFileMentions(text: string, cwd: string): FileMention[] {
// Match @some/path/to/file.ext patterns (must have at least one / or . to avoid false positives)
const mentionRegex = /@([\w./-]+(?:\/[\w./-]+|\.[\w]+))/g;
const mentions: FileMention[] = [];
const seen = new Set<string>();

let match: RegExpExecArray | null;
while ((match = mentionRegex.exec(text)) !== null) {
const relPath = match[1];
if (seen.has(relPath)) continue;
seen.add(relPath);

const absPath = join(cwd, relPath);
const content = safeReadFile(absPath, 50_000); // allow larger files for explicit mentions
mentions.push({
raw: match[0],
absPath,
relPath,
content,
});
}

return mentions;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Path traversal vulnerability in file mention parsing.

join(cwd, relPath) does not confine reads to the project directory. A user message containing @../../etc/passwd will resolve to a path outside cwd and safeReadFile will happily read it (up to 50 KB). The file contents are then injected into the AI prompt context.

Validate that the resolved absolute path is still within cwd after normalization:

Proposed fix
+import { resolve } from "node:path";
+
 function parseFileMentions(text: string, cwd: string): FileMention[] {
 	const mentionRegex = /@([\w./-]+(?:\/[\w./-]+|\.[\w]+))/g;
 	const mentions: FileMention[] = [];
 	const seen = new Set<string>();
+	const normalizedCwd = resolve(cwd);

 	let match: RegExpExecArray | null;
 	while ((match = mentionRegex.exec(text)) !== null) {
 		const relPath = match[1];
 		if (seen.has(relPath)) continue;
 		seen.add(relPath);

-		const absPath = join(cwd, relPath);
+		const absPath = resolve(cwd, relPath);
+		if (!absPath.startsWith(normalizedCwd + "/") && absPath !== normalizedCwd) continue;
 		const content = safeReadFile(absPath, 50_000);
📝 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
function parseFileMentions(text: string, cwd: string): FileMention[] {
// Match @some/path/to/file.ext patterns (must have at least one / or . to avoid false positives)
const mentionRegex = /@([\w./-]+(?:\/[\w./-]+|\.[\w]+))/g;
const mentions: FileMention[] = [];
const seen = new Set<string>();
let match: RegExpExecArray | null;
while ((match = mentionRegex.exec(text)) !== null) {
const relPath = match[1];
if (seen.has(relPath)) continue;
seen.add(relPath);
const absPath = join(cwd, relPath);
const content = safeReadFile(absPath, 50_000); // allow larger files for explicit mentions
mentions.push({
raw: match[0],
absPath,
relPath,
content,
});
}
return mentions;
}
function parseFileMentions(text: string, cwd: string): FileMention[] {
// Match `@some/path/to/file.ext` patterns (must have at least one / or . to avoid false positives)
const mentionRegex = /@([\w./-]+(?:\/[\w./-]+|\.[\w]+))/g;
const mentions: FileMention[] = [];
const seen = new Set<string>();
const normalizedCwd = resolve(cwd);
let match: RegExpExecArray | null;
while ((match = mentionRegex.exec(text)) !== null) {
const relPath = match[1];
if (seen.has(relPath)) continue;
seen.add(relPath);
const absPath = resolve(cwd, relPath);
if (!absPath.startsWith(normalizedCwd + "/") && absPath !== normalizedCwd) continue;
const content = safeReadFile(absPath, 50_000); // allow larger files for explicit mentions
mentions.push({
raw: match[0],
absPath,
relPath,
content,
});
}
return mentions;
}
🤖 Prompt for AI Agents
In `@apps/desktop/src/lib/trpc/routers/ai-chat/index.ts` around lines 198 - 221,
The parseFileMentions function currently uses join(cwd, relPath) and can read
files outside the project (path traversal); after computing the candidate path
(use path.resolve or normalize on join(cwd, relPath)) verify that the resolved
path remains inside cwd (for example by ensuring path.relative(cwd,
resolvedPath) does not start with '..' and is not absolute outside cwd) and skip
adding/reading any mention that escapes cwd; update parseFileMentions to perform
this check before calling safeReadFile so only files under cwd are read.

Comment on lines +497 to +514
sendMessage: publicProcedure
.input(z.object({ sessionId: z.string(), text: z.string() }))
.mutation(({ input }) => {
// Fire-and-forget: agent runs in background, errors surface via streamEvents
void chatSessionManager
.startAgent({
sessionId: input.sessionId,
prompt: input.text,
})
.catch((error: unknown) => {
console.error(
"[ai-chat/sendMessage] Failed to start agent:",
error,
);
console.log("sendMessage", input);
// // Fire-and-forget: agent runs in background, errors surface via streamEvents
// void chatSessionManager
// .startAgent({
// sessionId: input.sessionId,
// prompt: input.text,
// })
// .catch((error: unknown) => {
// console.error(
// "[ai-chat/sendMessage] Failed to start agent:",
// error,
// );
// });
return { success: true };
}),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

sendMessage is a no-op — remove or restore the implementation.

The entire agent-dispatch logic is commented out, leaving sendMessage as a mutation that only logs and returns { success: true } without doing anything. If this endpoint is still called by the frontend, users will see no response. If it's replaced by superagent, consider removing it to avoid confusion.

🤖 Prompt for AI Agents
In `@apps/desktop/src/lib/trpc/routers/ai-chat/index.ts` around lines 497 - 514,
The sendMessage mutation currently does nothing (just logs and returns {
success: true }); either restore the original agent dispatch by uncommenting and
re-enabling the chatSessionManager.startAgent(...) fire-and-forget call
(including its .catch(...) error logging) and remove the stray console.log, or
delete the sendMessage procedure entirely if the frontend now uses a different
endpoint (e.g., superagent). Locate the sendMessage publicProcedure and either
re-enable the chatSessionManager.startAgent invocation with proper error
handling or remove the procedure to avoid a no-op API surface.

Comment on lines 536 to 545
void (async () => {
// --- Abort controller ---
const abortController = new AbortController();
sessionAbortControllers.set(input.sessionId, abortController);

// Store context for approval resumption
sessionContext.set(input.sessionId, {
cwd: input.cwd,
modelId: input.modelId,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Concurrent calls for the same session overwrite the abort controller without cancelling the previous stream.

If superagent is called twice for the same sessionId before the first stream completes, line 539 overwrites the AbortController in the map. The first stream becomes uncancellable but keeps emitting chunks to the same session event, interleaving with the second stream's output.

Consider aborting any existing controller before storing the new one:

Proposed fix
 				void (async () => {
+					// Cancel any in-flight stream for this session
+					sessionAbortControllers.get(input.sessionId)?.abort();
+
 					const abortController = new AbortController();
 					sessionAbortControllers.set(input.sessionId, abortController);
🤖 Prompt for AI Agents
In `@apps/desktop/src/lib/trpc/routers/ai-chat/index.ts` around lines 536 - 545,
Concurrent requests for the same sessionId overwrite the AbortController in
sessionAbortControllers, leaving the previous stream uncancellable; before
creating and storing a new AbortController in the async block, check
sessionAbortControllers.get(input.sessionId) and if one exists call .abort() on
it and remove it, then create the new AbortController and set it
(sessionAbortControllers.set(input.sessionId, abortController)); ensure any
cleanup also updates sessionAbortControllers (and optionally sessionContext)
when streams finish or error to avoid stale controllers.

Comment on lines +663 to +679
} catch (error) {
// Don't emit error for intentional aborts
if (abortController.signal.aborted) {
console.log("[ai-chat/superagent] Stream aborted by user.");
superagentEmitter.emit(input.sessionId, {
type: "done",
});
return;
}
console.error("[ai-chat/superagent] Stream failed:", error);
superagentEmitter.emit(input.sessionId, {
type: "error",
error: error instanceof Error ? error.message : String(error),
});
} finally {
sessionAbortControllers.delete(input.sessionId);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

sessionRunIds and sessionContext leak on stream error.

On a non-abort error, the catch block emits an error event but does not clean up sessionRunIds or sessionContext. The finally block only removes the abort controller. This leaves stale entries that could cause a later approveToolCall to use outdated context or attempt to resume a dead stream.

Proposed fix
 					} catch (error) {
 						if (abortController.signal.aborted) {
 							console.log("[ai-chat/superagent] Stream aborted by user.");
 							superagentEmitter.emit(input.sessionId, { type: "done" });
 							return;
 						}
 						console.error("[ai-chat/superagent] Stream failed:", error);
 						superagentEmitter.emit(input.sessionId, {
 							type: "error",
 							error: error instanceof Error ? error.message : String(error),
 						});
 					} finally {
 						sessionAbortControllers.delete(input.sessionId);
+						// Clean up on any terminal path (error or abort)
+						// Success + suspension paths handle their own cleanup
+						if (!sessionSuspended.has(input.sessionId)) {
+							sessionRunIds.delete(input.sessionId);
+							sessionContext.delete(input.sessionId);
+						}
 					}
🤖 Prompt for AI Agents
In `@apps/desktop/src/lib/trpc/routers/ai-chat/index.ts` around lines 663 - 679,
The catch block for the superagent stream currently emits an error but doesn't
clean up sessionRunIds or sessionContext, causing stale state; update the catch
(error) handler so that if the error is not an intentional abort (i.e.,
abortController.signal.aborted is false) you also delete
sessionRunIds.delete(input.sessionId) and sessionContext.delete(input.sessionId)
(the same keys you remove on successful completion) before emitting the error
event, ensuring sessionRunIds, sessionContext, and sessionAbortControllers are
all cleared when a stream fails.

Comment on lines 79 to +84
useEffect(() => {
if (!sessionId || !cwd) return;
if (existingSession === undefined) return;

hasConnected.current = false;
setSessionReady(false);

if (existingSession) {
restoreSessionRef.current.mutate({
sessionId,
cwd,
paneId,
tabId,
model: selectedModelRef.current.id,
permissionMode: permissionModeRef.current,
});
} else {
startSessionRef.current.mutate({
sessionId,
workspaceId,
cwd,
paneId,
tabId,
model: selectedModelRef.current.id,
permissionMode: permissionModeRef.current,
});
}

return () => {
stopSessionRef.current.mutate({ sessionId });
};
}, [sessionId, cwd, workspaceId, existingSession, paneId, tabId]);
if (!historyMessages || historyMessages.length === 0) return;
setMessages(
hydrateMessages(historyMessages as Array<Record<string, unknown>>),
);
}, [historyMessages]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

History re-fetch during streaming will overwrite local message state.

The useEffect at line 79 calls setMessages(hydrateMessages(...)) whenever historyMessages changes. If the tRPC query refetches (e.g., due to window focus or polling) while the user is mid-stream, it will replace the live-streaming messages with the stale server history, causing the UI to jump or lose in-progress content.

Consider adding a guard (e.g., skip if isStreaming is true) or using { refetchOnWindowFocus: false } on the query.

Proposed fix
 	useEffect(() => {
 		if (!historyMessages || historyMessages.length === 0) return;
+		if (isStreaming) return; // Don't overwrite live-streaming state
 		setMessages(
 			hydrateMessages(historyMessages as Array<Record<string, unknown>>),
 		);
-	}, [historyMessages]);
+	}, [historyMessages, isStreaming]);
📝 Committable suggestion

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

Suggested change
useEffect(() => {
if (!sessionId || !cwd) return;
if (existingSession === undefined) return;
hasConnected.current = false;
setSessionReady(false);
if (existingSession) {
restoreSessionRef.current.mutate({
sessionId,
cwd,
paneId,
tabId,
model: selectedModelRef.current.id,
permissionMode: permissionModeRef.current,
});
} else {
startSessionRef.current.mutate({
sessionId,
workspaceId,
cwd,
paneId,
tabId,
model: selectedModelRef.current.id,
permissionMode: permissionModeRef.current,
});
}
return () => {
stopSessionRef.current.mutate({ sessionId });
};
}, [sessionId, cwd, workspaceId, existingSession, paneId, tabId]);
if (!historyMessages || historyMessages.length === 0) return;
setMessages(
hydrateMessages(historyMessages as Array<Record<string, unknown>>),
);
}, [historyMessages]);
useEffect(() => {
if (!historyMessages || historyMessages.length === 0) return;
if (isStreaming) return; // Don't overwrite live-streaming state
setMessages(
hydrateMessages(historyMessages as Array<Record<string, unknown>>),
);
}, [historyMessages, isStreaming]);
🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx`
around lines 79 - 84, The effect that sets local messages from historyMessages
(useEffect that calls setMessages(hydrateMessages(...))) overwrites in-progress
streaming content when history refetches; modify that effect to guard against
overwriting during an active stream (check an isStreaming flag or similar state
before calling setMessages) or alternatively disable unwanted refetching at the
source (set the tRPC query option refetchOnWindowFocus: false / disable polling)
so hydrateMessages is only applied when not streaming.

Comment on lines +10 to +32
// Debug: log the raw shape from the server to verify toAISdkV5Messages output
console.log(
"[hydrate] raw historyMessages sample:",
JSON.stringify(
historyMessages.slice(0, 2).map((m) => ({
id: m.id,
role: m.role,
hasTopLevelParts: Array.isArray(m.parts),
hasContentParts:
!!m.content &&
Array.isArray((m.content as Record<string, unknown>)?.parts),
partTypes: (
(m.parts as Array<Record<string, unknown>>) ??
((m.content as Record<string, unknown>)?.parts as Array<
Record<string, unknown>
>) ??
[]
).map((p) => ({ type: p.type, toolName: p.toolName })),
})),
null,
2,
),
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove debug console.log — leaks message content in production.

This logs the first two raw messages (including user text, tool arguments, and potentially sensitive data) every time history is hydrated. This should be removed or gated behind a debug flag before merging.

Proposed fix
-	// Debug: log the raw shape from the server to verify toAISdkV5Messages output
-	console.log(
-		"[hydrate] raw historyMessages sample:",
-		JSON.stringify(
-			historyMessages.slice(0, 2).map((m) => ({
-				id: m.id,
-				role: m.role,
-				hasTopLevelParts: Array.isArray(m.parts),
-				hasContentParts:
-					!!m.content &&
-					Array.isArray((m.content as Record<string, unknown>)?.parts),
-				partTypes: (
-					(m.parts as Array<Record<string, unknown>>) ??
-					((m.content as Record<string, unknown>)?.parts as Array<
-						Record<string, unknown>
-					>) ??
-					[]
-				).map((p) => ({ type: p.type, toolName: p.toolName })),
-			})),
-			null,
-			2,
-		),
-	);
📝 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
// Debug: log the raw shape from the server to verify toAISdkV5Messages output
console.log(
"[hydrate] raw historyMessages sample:",
JSON.stringify(
historyMessages.slice(0, 2).map((m) => ({
id: m.id,
role: m.role,
hasTopLevelParts: Array.isArray(m.parts),
hasContentParts:
!!m.content &&
Array.isArray((m.content as Record<string, unknown>)?.parts),
partTypes: (
(m.parts as Array<Record<string, unknown>>) ??
((m.content as Record<string, unknown>)?.parts as Array<
Record<string, unknown>
>) ??
[]
).map((p) => ({ type: p.type, toolName: p.toolName })),
})),
null,
2,
),
);
🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/utils/hydrate-messages.ts`
around lines 10 - 32, The debug console.log in hydrate-messages.ts is leaking
raw message content; remove the console.log call that prints historyMessages
(the JSON.stringify block referencing historyMessages.slice(...)) or wrap it
behind a proper debug flag/logger check (e.g., only log when DEBUG or a dev-only
logger is enabled). Locate the console.log in the hydrate messages utility and
either delete it or replace it with a conditional debug-level log using your
app's logger (so production builds never print raw message content).

Comment on lines +4 to +10
export function toToolDisplayState(part: ToolCallPart): ToolDisplayState {
if (part.status === "streaming") return "input-streaming";
if (part.status === "calling") return "input-complete";
if (part.isError) return "output-error";
if (part.result != null) return "output-available";
return "input-available";
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Edge case: status === "done" with no result and no error falls through to "input-available".

If a tool call completes (status: "done") but has neither result nor isError set, both toToolDisplayState and toWsToolState will return "input-available", which semantically suggests the tool hasn't started yet. This is likely a rare edge case, but worth considering whether a completed-but-empty-result state should map to something else (e.g., "output-available").

🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/utils/tool-helpers.ts`
around lines 4 - 10, Handle the edge case where a ToolCallPart has status ===
"done" but no result and no isError: update toToolDisplayState to check for
part.status === "done" (before the final fallthrough) and return
"output-available" for a completed-but-empty-result call; mirror the same logic
in toWsToolState so both mappings treat a finished-but-empty tool call as an
output-available state rather than "input-available".

Comment thread packages/agent/src/index.ts Outdated
initSessionStore,
setClaudeSessionId,
} from "./session-store";
export { memory, superagent } from "./superagent";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Re-exporting ./superagent triggers module-level side effects for all consumers of @superset/agent.

Because superagent.ts instantiates PostgresStore at the top level, every consumer that imports anything from @superset/agent — even unrelated exports like createPermissionRequest or getActiveSessionCount — will now require DATABASE_URL to be set and will attempt a PostgreSQL connection at import time.

If not all consumers need the superagent, consider isolating it behind a separate export path (e.g., @superset/agent/superagent) or using lazy initialization in superagent.ts.

🤖 Prompt for AI Agents
In `@packages/agent/src/index.ts` at line 15, index.ts re-exporting "./superagent"
causes PostgresStore to be instantiated at module import, forcing
DATABASE_URL/DB connection for all consumers; remove the direct re-export from
packages/agent/src/index.ts and either (A) expose superagent via a separate
entrypoint (e.g., create a dedicated export path "@superset/agent/superagent"
that re-exports the module) or (B) change superagent.ts to lazy-initialize
PostgresStore (provide a getSuperagent/getStore factory function that constructs
PostgresStore on first use instead of at top-level). Update any callers to
import the new entrypoint or call the factory (referencing ./superagent,
PostgresStore, and the current top-level exports like
createPermissionRequest/getActiveSessionCount to ensure they no longer trigger
DB initialization).

Comment on lines +11 to +14
export const storage = new PostgresStore({
connectionString: process.env.DATABASE_URL!,
id: "superagent-db",
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

process.env.DATABASE_URL! — non-null assertion flagged by pipeline lint.

The linter warns against the non-null assertion. If DATABASE_URL is unset, undefined is silently passed to PostgresStore, likely causing a confusing runtime error (connection failure rather than a clear "missing config" message). Since this runs at module-load time, the failure surface is wide.

Proposed fix
+const connectionString = process.env.DATABASE_URL;
+if (!connectionString) {
+	throw new Error("DATABASE_URL environment variable is required for superagent storage");
+}
+
 export const storage = new PostgresStore({
-	connectionString: process.env.DATABASE_URL!,
+	connectionString,
 	id: "superagent-db",
 });
📝 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
export const storage = new PostgresStore({
connectionString: process.env.DATABASE_URL!,
id: "superagent-db",
});
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
throw new Error("DATABASE_URL environment variable is required for superagent storage");
}
export const storage = new PostgresStore({
connectionString,
id: "superagent-db",
});
🧰 Tools
🪛 GitHub Actions: CI

[warning] 12-12: lint/style/noNonNullAssertion: Forbidden non-null assertion. connectionString: process.env.DATABASE_URL!

🤖 Prompt for AI Agents
In `@packages/agent/src/superagent.ts` around lines 11 - 14, The module currently
uses a non-null assertion on process.env.DATABASE_URL when constructing storage
with new PostgresStore, which the linter flags and which can produce unclear
runtime failures at module load; update the module to explicitly validate that
process.env.DATABASE_URL is set before creating storage (e.g., read
process.env.DATABASE_URL into a local variable, if falsy throw a clear Error
like "DATABASE_URL is required" or return a guarded factory), and then pass that
validated string into new PostgresStore so storage is only constructed with a
guaranteed connection string.

Comment thread plan.md
Comment on lines +46 to +51
- In the `superagent` mutation (line 522), before calling `superagent.stream()`:
1. Parse the provider from `modelId` (e.g., `"anthropic/..."` → `"anthropic"`)
2. Call the credential store to get a valid access token for that provider
3. If found, set `process.env.ANTHROPIC_API_KEY` (or `OPENAI_API_KEY`) to the token
4. After the stream completes, restore the original env var value
- Same for `approveToolCall` resumption path
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Race condition risk in process.env token injection strategy.

The plan injects OAuth tokens into process.env before each superagent.stream() call and restores afterwards. Since Node.js is single-threaded but async, if two concurrent sessions trigger overlapping agent calls for different providers/tokens, one session's env var write can be read by another session's Mastra call between the set and restore. Consider passing credentials directly to the Mastra agent/model constructor (if supported) rather than mutating a shared global.

🤖 Prompt for AI Agents
In `@plan.md` around lines 46 - 51, The current approach mutates process.env
(e.g., setting process.env.ANTHROPIC_API_KEY or process.env.OPENAI_API_KEY)
around calls in the superagent mutation (before superagent.stream() at the code
referenced around line 522) and the approveToolCall resumption path, which
creates a race; instead, refactor so you do NOT write to process.env: retrieve
the provider token from the credential store, then pass that token directly into
the Mastra agent/model constructor or into superagent.stream()/request options
(or whatever per-request auth API the Mastra/agent client exposes) for the
specific call (use the parsed provider from modelId such as "anthropic" to pick
token), and remove the set/restore of the env var; update both the superagent
mutation and the approveToolCall resumption code paths to use this per-request
credential injection.

The heredoc was writing literal \t strings instead of tab characters,
causing Caddy to fail with "unrecognized directive: \treverse_proxy".
Switch to <<- heredoc with actual tab indentation.
Copy link
Copy Markdown
Contributor

@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: 4

🤖 Fix all issues with AI agents
In `@apps/desktop/src/lib/trpc/routers/ai-chat/index.ts`:
- Around line 46-60: The per-session collections sessionAbortControllers,
sessionRunIds, sessionContext, and sessionSuspended are never cleared, causing a
leak; update the session teardown paths (deleteSession and stopSession which
call chatSessionManager) to remove any entries for the given sessionId by
calling sessionAbortControllers.delete(sessionId),
sessionRunIds.delete(sessionId), sessionContext.delete(sessionId), and
sessionSuspended.delete(sessionId) (also remove any listeners on
superagentEmitter for that session if applicable) so state is fully cleaned when
a session is stopped or deleted.
- Around line 77-83: safeExec currently uses execSync which blocks the Electron
main process; replace it with an async implementation (e.g., promisified
child_process.execFile or using child_process.spawn wrapped in a Promise) and
make safeExec return a Promise<string>; update any callers such as
gatherProjectContext (make it async) and the superagent handler that invokes it
to await the new safeExec calls (for git branch/status/log) so the commands run
asynchronously and do not block the event loop.
- Around line 763-827: The resumed stream started in the approveToolCall block
doesn't register an AbortController so subsequent abortSuperagent calls can't
cancel it; create a new AbortController before calling
superagent.approveToolCall / declineToolCall, add its signal to approvalOpts
(e.g., approvalOpts.signal = controller.signal), store the controller in
sessionAbortControllers keyed by input.sessionId, and ensure you clean it up
(delete from sessionAbortControllers) when the resumed stream completes or
errors (similar to the original stream's finally) so abortSuperagent can cancel
this resumed stream.

In `@packages/agent/src/superagent.ts`:
- Around line 125-136: The Workspace creation is using unvalidated cwd from user
input when constructing LocalFilesystem and LocalSandbox (seen in Workspace,
LocalFilesystem, LocalSandbox usage), which can allow path traversal or access
to arbitrary system paths; before creating the Workspace (or before calling
executeAgent in the ai-chat router), call the existing
assertRegisteredWorktree(cwd) validation from path-validation.ts (or otherwise
map/normalize cwd to a canonical registered worktree root) and reject or
normalize any cwd that fails validation; apply the same guard where similar
Workspace construction occurs (also around the block at lines ~214-219) so only
registered workspace roots are used as basePath/workingDirectory.
🧹 Nitpick comments (8)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessagePartsRenderer/MessagePartsRenderer.tsx (1)

30-36: renderParts inconsistently mixes explicit parameters with closure captures.

parts and isLastAssistant are explicit parameters (allowing AgentCallBlock to supply its own values), but isStreaming and onAnswer are captured from the parent closure. If a nested agent call needs a different isStreaming or onAnswer, there's no way to pass it. Consider either making all four values explicit parameters, or documenting that the closure capture is intentional.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/MastraToolCallBlock.tsx (2)

126-141: Unsafe type assertion on result.answers.

result.answers as Record<string, string> | undefined bypasses runtime validation. If the server ever returns non-string values in answers, this will silently propagate bad data. Consider a lightweight runtime check or at minimum narrowing with typeof.

Proposed safer narrowing
-		const answers = result.answers as
-			| Record<string, string>
-			| undefined;
+		const answers =
+			result.answers != null &&
+			typeof result.answers === "object" &&
+			!Array.isArray(result.answers)
+				? (result.answers as Record<string, string>)
+				: undefined;

As per coding guidelines, "Avoid any type annotation unless necessary - prioritize type safety".


158-178: Fallback uses raw part.args/part.result while other branches use getArgs/getResult helpers.

This is likely intentional for generic display, but worth noting: if getArgs/getResult do meaningful normalization (e.g., JSON parsing), the fallback ToolInput/ToolOutput may receive unparsed data. If the generic Tool components can handle both parsed and raw forms, this is fine.

packages/agent/src/superagent.ts (2)

62-72: Eager module-level side effects: PostgresStore and Memory instantiate on import.

Both storage and memory are created at module load time, meaning any file that imports anything from this module will trigger a Postgres connection attempt — even if the consumer only needs, say, setAnthropicAuthToken. If this module is ever imported in a context where DATABASE_URL is unavailable (tests, CLI tooling, client bundles), it will fail immediately.

Consider lazy initialization (factory function or getter) so the connection is only established when actually needed.


122-124: Redundant type assertion: as string | undefined.

requestContext.get() already returns string | undefined per the type declared on line 39. The as cast is unnecessary and can mask future type changes.

-		const cwd = requestContext.get("cwd") as string | undefined;
+		const cwd = requestContext.get("cwd");

Also applies to: 211-213

apps/desktop/src/lib/trpc/routers/ai-chat/index.ts (3)

333-353: Remove debug logging from getMessages.

This block logs tool-part keys and up to 500 chars of serialized message content to the console on every getMessages call. It appears to be development scaffolding — it adds per-request overhead and may leak conversation content into logs.

Proposed fix
 			const uiMessages = toAISdkV5Messages(result.messages);
-			// Debug: log tool parts to verify shape
-			for (const msg of uiMessages.slice(0, 3)) {
-				const parts = (msg as Record<string, unknown>).parts as
-					| Array<Record<string, unknown>>
-					| undefined;
-				if (parts) {
-					for (const p of parts) {
-						if (String(p.type ?? "").startsWith("tool-")) {
-							console.log(
-								"[getMessages] V5 tool part keys:",
-								Object.keys(p),
-								"type:",
-								p.type,
-							);
-							console.log(
-								"[getMessages] V5 tool part:",
-								JSON.stringify(p, null, 2).slice(0, 500),
-							);
-						}
-					}
-				}
-			}
 			return uiMessages;

574-576: Operator-precedence gotcha — consider explicit clarity.

const contextInstructions = projectContext + fileMentionContext || undefined;

This reads as (projectContext + fileMentionContext) || undefined due to + binding tighter than ||, which happens to produce the desired result. However, it's easy to misread on a quick scan. A small clarity improvement:

Proposed fix
-				const contextInstructions =
-					projectContext + fileMentionContext || undefined;
+				const combinedContext = projectContext + fileMentionContext;
+				const contextInstructions = combinedContext || undefined;

629-672: Reduce verbose per-chunk logging before shipping.

The streaming loop logs on every chunk that matches a broad set of types (tool-*, text-delta, finish, start, step-start, step-finish) and on the first 5 chunks unconditionally. For active sessions, this produces a very high volume of console output. Consider limiting to lifecycle events (start/finish/error/suspension) only, or gating behind a DEBUG flag.

Comment on lines +46 to +60
/** Per-session event bus for streaming superagent chunks to the renderer */
const superagentEmitter = new EventEmitter();
superagentEmitter.setMaxListeners(50);

/** Per-session AbortController for cancelling running streams */
const sessionAbortControllers = new Map<string, AbortController>();

/** Per-session runId for tool approval (maps sessionId → runId) */
const sessionRunIds = new Map<string, string>();

/** Per-session context needed for approval resumption (maps sessionId → { cwd, modelId }) */
const sessionContext = new Map<string, { cwd: string; modelId: string }>();

/** Track whether a session stream is suspended (waiting for tool approval) */
const sessionSuspended = new Set<string>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Per-session maps are never cleaned up by deleteSession / stopSession.

The module-level maps (sessionAbortControllers, sessionRunIds, sessionContext) and sessionSuspended set grow over the application lifetime. The deleteSession (line 429) and stopSession (line 420) procedures delegate to chatSessionManager but never clear entries from these maps. In a long-running Electron process, this is a slow memory leak and can also cause stale state if a session ID is reused.

Consider adding cleanup when sessions are explicitly stopped or deleted:

Proposed fix (in deleteSession / stopSession)
 		deleteSession: publicProcedure
 			.input(z.object({ sessionId: z.string() }))
 			.mutation(async ({ input }) => {
+				sessionAbortControllers.get(input.sessionId)?.abort();
+				sessionAbortControllers.delete(input.sessionId);
+				sessionRunIds.delete(input.sessionId);
+				sessionContext.delete(input.sessionId);
+				sessionSuspended.delete(input.sessionId);
 				await chatSessionManager.deleteSession({
 					sessionId: input.sessionId,
 				});
 				return { success: true };
 			}),

Apply similarly to stopSession.

🤖 Prompt for AI Agents
In `@apps/desktop/src/lib/trpc/routers/ai-chat/index.ts` around lines 46 - 60, The
per-session collections sessionAbortControllers, sessionRunIds, sessionContext,
and sessionSuspended are never cleared, causing a leak; update the session
teardown paths (deleteSession and stopSession which call chatSessionManager) to
remove any entries for the given sessionId by calling
sessionAbortControllers.delete(sessionId), sessionRunIds.delete(sessionId),
sessionContext.delete(sessionId), and sessionSuspended.delete(sessionId) (also
remove any listeners on superagentEmitter for that session if applicable) so
state is fully cleaned when a session is stopped or deleted.

Comment on lines +77 to +83
function safeExec(cmd: string, cwd: string, timeoutMs = 3_000): string {
try {
return execSync(cmd, { cwd, timeout: timeoutMs, encoding: "utf-8" }).trim();
} catch {
return "";
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

execSync blocks the Electron main-process event loop.

safeExec uses execSync, which is called up to 3 times from gatherProjectContext (git branch, git status, git log) on every superagent invocation. Each call can block for up to 3 seconds (the configured timeout), stalling all IPC, tRPC handlers, and window responsiveness for up to ~9 seconds in the worst case.

Switch to an async alternative (child_process.execFile with util.promisify, or child_process.spawn) and await it inside the already-async superagent handler.

Proposed fix sketch
+import { execFile } from "node:child_process";
+import { promisify } from "node:util";
+const execFileAsync = promisify(execFile);
+
-function safeExec(cmd: string, cwd: string, timeoutMs = 3_000): string {
+async function safeExec(cmd: string, cwd: string, timeoutMs = 3_000): Promise<string> {
 	try {
-		return execSync(cmd, { cwd, timeout: timeoutMs, encoding: "utf-8" }).trim();
+		const { stdout } = await execFileAsync("sh", ["-c", cmd], {
+			cwd,
+			timeout: timeoutMs,
+			encoding: "utf-8",
+		});
+		return stdout.trim();
 	} catch {
 		return "";
 	}
 }

gatherProjectContext would then become async as well.

🤖 Prompt for AI Agents
In `@apps/desktop/src/lib/trpc/routers/ai-chat/index.ts` around lines 77 - 83,
safeExec currently uses execSync which blocks the Electron main process; replace
it with an async implementation (e.g., promisified child_process.execFile or
using child_process.spawn wrapped in a Promise) and make safeExec return a
Promise<string>; update any callers such as gatherProjectContext (make it async)
and the superagent handler that invokes it to await the new safeExec calls (for
git branch/status/log) so the commands run asynchronously and do not block the
event loop.

Comment on lines +763 to +827
void (async () => {
try {
// Restore the requestContext so dynamic workspace tools are available
const ctx = sessionContext.get(input.sessionId);
const reqCtx = ctx
? new RequestContext([
["modelId", ctx.modelId],
["cwd", ctx.cwd],
])
: undefined;

console.log(
`[ai-chat/approveToolCall] Calling ${input.approved ? "approveToolCall" : "declineToolCall"} with runId: ${input.runId}, cwd: ${ctx?.cwd}`,
);
const approvalOpts = {
runId: input.runId,
...(reqCtx ? { requestContext: reqCtx } : {}),
};
const stream = input.approved
? await superagent.approveToolCall(approvalOpts)
: await superagent.declineToolCall(approvalOpts);

let chunkCount = 0;
console.log(
`[ai-chat/approveToolCall] Got stream, starting to iterate. Listeners: ${superagentEmitter.listenerCount(input.sessionId)}`,
);

for await (const chunk of stream.fullStream) {
chunkCount++;
const c = chunk as { type?: string };

// Check if the resumed stream itself hits another approval
if (c.type === "tool-call-approval") {
sessionSuspended.add(input.sessionId);
console.log(
`[ai-chat/approveToolCall] Another tool approval required`,
);
}

superagentEmitter.emit(input.sessionId, {
type: "chunk",
chunk,
});
}

console.log(
`[ai-chat/approveToolCall] Resumed stream complete. Chunks: ${chunkCount}`,
);

// Only emit done if not suspended again
if (!sessionSuspended.has(input.sessionId)) {
superagentEmitter.emit(input.sessionId, {
type: "done",
});
sessionRunIds.delete(input.sessionId);
sessionContext.delete(input.sessionId);
}
} catch (error) {
console.error("[ai-chat/approveToolCall] Stream failed:", error);
superagentEmitter.emit(input.sessionId, {
type: "error",
error: error instanceof Error ? error.message : String(error),
});
}
})();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Resumed stream is not cancellable — no AbortController registered.

When approveToolCall fires the resumed stream, it doesn't create or register a new AbortController in sessionAbortControllers. The original controller was already deleted in the first stream's finally block. If the user calls abortSuperagent while the resumed stream is iterating, nothing happens — the stream keeps running.

Proposed fix
 			void (async () => {
+				const abortController = new AbortController();
+				sessionAbortControllers.set(input.sessionId, abortController);
+
 				try {
 					const ctx = sessionContext.get(input.sessionId);
 					const reqCtx = ctx
 						? new RequestContext([
 								["modelId", ctx.modelId],
 								["cwd", ctx.cwd],
 							])
 						: undefined;
 					// ... existing code ...
+				} catch (error) {
+					console.error("[ai-chat/approveToolCall] Stream failed:", error);
+					superagentEmitter.emit(input.sessionId, {
+						type: "error",
+						error: error instanceof Error ? error.message : String(error),
+					});
+				} finally {
+					sessionAbortControllers.delete(input.sessionId);
+					if (!sessionSuspended.has(input.sessionId)) {
+						sessionRunIds.delete(input.sessionId);
+						sessionContext.delete(input.sessionId);
+					}
 				}
🤖 Prompt for AI Agents
In `@apps/desktop/src/lib/trpc/routers/ai-chat/index.ts` around lines 763 - 827,
The resumed stream started in the approveToolCall block doesn't register an
AbortController so subsequent abortSuperagent calls can't cancel it; create a
new AbortController before calling superagent.approveToolCall / declineToolCall,
add its signal to approvalOpts (e.g., approvalOpts.signal = controller.signal),
store the controller in sessionAbortControllers keyed by input.sessionId, and
ensure you clean it up (delete from sessionAbortControllers) when the resumed
stream completes or errors (similar to the original stream's finally) so
abortSuperagent can cancel this resumed stream.

Comment on lines +125 to +136
return new Workspace({
id: `planner-workspace-${cwd}`,
name: `${cwd} (read-only)`,
filesystem: new LocalFilesystem({ basePath: cwd }),
sandbox: new LocalSandbox({ workingDirectory: cwd }),
tools: {
mastra_workspace_write_file: { enabled: false },
mastra_workspace_edit_file: { enabled: false },
mastra_workspace_delete: { enabled: false },
mastra_workspace_mkdir: { enabled: false },
},
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, locate and examine the superagent.ts file
find . -name "superagent.ts" -type f

Repository: superset-sh/superset

Length of output: 98


🏁 Script executed:

#!/bin/bash
# Get the content around lines 125-136 and 214-219
cat -n packages/agent/src/superagent.ts | sed -n '120,140p'
cat -n packages/agent/src/superagent.ts | sed -n '210,225p'

Repository: superset-sh/superset

Length of output: 1412


🏁 Script executed:

#!/bin/bash
# Search for how requestContext is created and how cwd is set
rg -n "requestContext" packages/agent/src/superagent.ts -A 2 -B 2

Repository: superset-sh/superset

Length of output: 791


🏁 Script executed:

#!/bin/bash
# Search for validation of cwd anywhere in the codebase
rg -n "cwd" packages/agent/src/ --type=ts -A 3 -B 3 | head -100

Repository: superset-sh/superset

Length of output: 3153


🏁 Script executed:

#!/bin/bash
# Look for the LocalFilesystem and LocalSandbox implementations
fd -e ts -e tsx | xargs rg -l "class LocalFilesystem|export.*LocalFilesystem"
fd -e ts -e tsx | xargs rg -l "class LocalSandbox|export.*LocalSandbox"

Repository: superset-sh/superset

Length of output: 48


🏁 Script executed:

#!/bin/bash
# Search for LocalFilesystem and LocalSandbox more broadly
rg -n "LocalFilesystem|LocalSandbox" packages/ --type=ts

Repository: superset-sh/superset

Length of output: 538


🏁 Script executed:

#!/bin/bash
# Check the imports in superagent.ts to find where LocalFilesystem/LocalSandbox come from
head -50 packages/agent/src/superagent.ts

Repository: superset-sh/superset

Length of output: 1824


🏁 Script executed:

#!/bin/bash
# Search across the entire codebase for these classes
rg "export.*class.*LocalFilesystem|export.*LocalFilesystem" --type=ts
rg "export.*class.*LocalSandbox|export.*LocalSandbox" --type=ts

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Look in the entire project for these imports/classes
fd -e ts -e tsx | xargs rg -l "LocalFilesystem" | head -10
fd -e ts -e tsx | xargs rg -l "LocalSandbox" | head -10

Repository: superset-sh/superset

Length of output: 129


🏁 Script executed:

#!/bin/bash
# Search for where requestContext is created/populated with cwd
rg -n "requestContext.*cwd|cwd.*requestContext" packages/ --type=ts -A 3 -B 3

Repository: superset-sh/superset

Length of output: 1061


🏁 Script executed:

#!/bin/bash
# Look for where the agents are called/invoked to see how requestContext is set
rg -n "executeAgent|runAgent|invoke" packages/ --type=ts -A 5 -B 2 | head -80

Repository: superset-sh/superset

Length of output: 3459


🏁 Script executed:

#!/bin/bash
# Find API endpoints or entry points that might accept cwd parameter
rg -n "cwd" packages/ --type=ts -B 5 | grep -E "(app\.|router\.|post|get|export|function)" | head -40

Repository: superset-sh/superset

Length of output: 349


🏁 Script executed:

#!/bin/bash
# Check agent-executor.ts more thoroughly to understand the flow
cat -n packages/agent/src/agent-executor.ts | head -100

Repository: superset-sh/superset

Length of output: 3148


🏁 Script executed:

#!/bin/bash
# Check the ExecuteAgentParams type definition
cat -n packages/agent/src/types.ts | head -50

Repository: superset-sh/superset

Length of output: 1569


🏁 Script executed:

#!/bin/bash
# Search for API routes or endpoints that call executeAgent
rg -n "executeAgent" packages/ --type=ts -B 5 -A 5 | head -100

Repository: superset-sh/superset

Length of output: 1329


🏁 Script executed:

#!/bin/bash
# Look for any path validation or sanitization functions in the codebase
rg -n "normalize|resolve|sanitize|validate.*path|allowlist|basePath" packages/ --type=ts | head -40

Repository: superset-sh/superset

Length of output: 3558


🏁 Script executed:

#!/bin/bash
# Check if there are any tests that show how cwd is used
fd -e test.ts -e spec.ts | xargs rg -l "cwd|executeAgent" | head -5

Repository: superset-sh/superset

Length of output: 382


🏁 Script executed:

#!/bin/bash
# Search for any API routes that accept or handle the cwd parameter
rg -n "cwd" packages/ --type=ts -B 10 | grep -E "(app\.|post|get|route|endpoint|api)" | head -40

Repository: superset-sh/superset

Length of output: 263


🏁 Script executed:

#!/bin/bash
# Look for any path normalization or resolution in the codebase
rg -n "path\.resolve|path\.normalize|realpath" packages/agent/ --type=ts

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Check the actual test files to see how cwd is used
cat -n packages/shared/src/terminal-link-parsing/link-parsing.test.ts | head -50

Repository: superset-sh/superset

Length of output: 1980


🏁 Script executed:

#!/bin/bash
# Search for the Mastra library documentation or usage of LocalFilesystem
rg -n "@mastra" packages/agent/src/ --type=ts -A 2

Repository: superset-sh/superset

Length of output: 1082


🏁 Script executed:

#!/bin/bash
# Look for any documentation or comments about path validation
rg -n "directory|path|traversal|security|validate" packages/agent/src/superagent.ts -i -B 2 -A 2

Repository: superset-sh/superset

Length of output: 2349


🏁 Script executed:

#!/bin/bash
# Search for where executeAgent is called to understand the entry point
rg -n "executeAgent\(" packages/ apps/ --type=ts -B 10 | head -80

Repository: superset-sh/superset

Length of output: 1964


🏁 Script executed:

#!/bin/bash
# Look for API routes or handlers that might receive cwd from users
find packages apps -name "*.ts" -o -name "*.tsx" | xargs rg -l "cwd" | xargs rg -B 15 "cwd" | grep -E "(route|handler|post|get|api)" | head -30

Repository: superset-sh/superset

Length of output: 3088


🏁 Script executed:

#!/bin/bash
# Check if there's any validation in the Anthropic SDK or Mastra library being used
rg -n "LocalFilesystem|LocalSandbox" packages/ --type=ts -A 10 | head -60

Repository: superset-sh/superset

Length of output: 2389


🏁 Script executed:

#!/bin/bash
# Look at the complete superagent.ts to understand the context better
wc -l packages/agent/src/superagent.ts

Repository: superset-sh/superset

Length of output: 100


🏁 Script executed:

#!/bin/bash
# Check what the mastra libraries do with basePath/workingDirectory
rg -n "basePath|workingDirectory" packages/ --type=ts

Repository: superset-sh/superset

Length of output: 435


🏁 Script executed:

#!/bin/bash
# Look at the agent-execution.ts file to see how cwd is passed to executeAgent
cat -n apps/desktop/src/lib/trpc/routers/ai-chat/utils/session-manager/agent-execution.ts | sed -n '60,100p'

Repository: superset-sh/superset

Length of output: 1369


🏁 Script executed:

#!/bin/bash
# Search for where the cwd value is obtained in the desktop app
rg -n "cwd" apps/desktop/src/lib/trpc/routers/ai-chat/ --type=ts -B 5 -A 5 | head -100

Repository: superset-sh/superset

Length of output: 8133


🏁 Script executed:

#!/bin/bash
# Look at the ExecuteAgentInput type to understand where cwd originates
rg -n "ExecuteAgentInput|interface.*Input" apps/desktop/src/lib/trpc/routers/ai-chat/utils/session-manager/ --type=ts -A 10 | head -60

Repository: superset-sh/superset

Length of output: 6337


🏁 Script executed:

#!/bin/bash
# Check if there's any validation of cwd in the desktop app or elsewhere
rg -n "cwd.*validate|validate.*cwd|path.*allowed|allowlist" packages/ apps/ --type=ts

Repository: superset-sh/superset

Length of output: 2264


🏁 Script executed:

#!/bin/bash
# Look at the path-validation.ts file to understand path validation mechanisms
cat -n apps/desktop/src/lib/trpc/routers/changes/security/path-validation.ts | head -180

Repository: superset-sh/superset

Length of output: 6042


🏁 Script executed:

#!/bin/bash
# Check where cwd originates in the session lifecycle
cat -n apps/desktop/src/lib/trpc/routers/ai-chat/utils/session-manager/session-lifecycle.ts | sed -n '1,80p'

Repository: superset-sh/superset

Length of output: 2455


🏁 Script executed:

#!/bin/bash
# Search for where StartSessionInput is created in the router
rg -n "StartSessionInput|startSession" apps/desktop/src/lib/trpc/routers/ai-chat/ --type=ts -B 5 -A 8 | head -80

Repository: superset-sh/superset

Length of output: 7704


🏁 Script executed:

#!/antml/bash
# Check if there's validation of cwd in the AI chat router
cat -n apps/desktop/src/lib/trpc/routers/ai-chat/index.ts | sed -n '200,350p'

Repository: superset-sh/superset

Length of output: 5451


🏁 Script executed:

#!/antml/bash
# Check if there's any validation of cwd between the API input and executeAgent call
cat -n apps/desktop/src/lib/trpc/routers/ai-chat/utils/session-manager/session-lifecycle.ts | sed -n '173,250p'

Repository: superset-sh/superset

Length of output: 2437


🏁 Script executed:

#!/bin/bash
# Search for any use of assertRegisteredWorktree in the ai-chat router
rg -n "assertRegisteredWorktree|validateRelativePath|RegisteredWorktree" apps/desktop/src/lib/trpc/routers/ai-chat/ --type=ts

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Check if there's validation in the startSession implementation
cat -n apps/desktop/src/lib/trpc/routers/ai-chat/utils/session-manager/session-lifecycle.ts | sed -n '173,220p'

Repository: superset-sh/superset

Length of output: 1512


🏁 Script executed:

#!/antml/bash
# Check if there are any comments or TODO about cwd validation in ai-chat
rg -n "TODO|FIXME|security|validate.*cwd|sanitize.*cwd" apps/desktop/src/lib/trpc/routers/ai-chat/ --type=ts -i

Repository: superset-sh/superset

Length of output: 456


Validate cwd against registered worktrees before passing to LocalFilesystem / LocalSandbox.

cwd is accepted directly from user input (z.string() at line 368 of the tRPC router) without validation, then passed through the session lifecycle to superagent.ts. An attacker could provide paths like /etc, ~/.ssh, or traversal sequences to access sensitive files outside intended workspace boundaries.

The codebase already has validation infrastructure (assertRegisteredWorktree() in path-validation.ts) used for file operations in other routers. Apply similar validation in the ai-chat router before storing or passing cwd to executeAgent(), or add safeguards in superagent.ts to restrict basePath and workingDirectory to known workspace roots.

Also applies to: 214-219

🤖 Prompt for AI Agents
In `@packages/agent/src/superagent.ts` around lines 125 - 136, The Workspace
creation is using unvalidated cwd from user input when constructing
LocalFilesystem and LocalSandbox (seen in Workspace, LocalFilesystem,
LocalSandbox usage), which can allow path traversal or access to arbitrary
system paths; before creating the Workspace (or before calling executeAgent in
the ai-chat router), call the existing assertRegisteredWorktree(cwd) validation
from path-validation.ts (or otherwise map/normalize cwd to a canonical
registered worktree root) and reject or normalize any cwd that fails validation;
apply the same guard where similar Workspace construction occurs (also around
the block at lines ~214-219) so only registered workspace roots are used as
basePath/workingDirectory.

Copy link
Copy Markdown
Contributor

@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: 1

🤖 Fix all issues with AI agents
In `@packages/agent/src/superagent.ts`:
- Around line 50-59: The hard-coded deceptive client identity is set in the
createAnthropic call: update the headers passed to createAnthropic (where
anthropicAuthToken and model are used) to stop impersonating the official Claude
CLI; keep the required "anthropic-beta" header but replace "user-agent":
"claude-cli/2.1.2 (external, cli)" and "x-app": "cli" with truthful identifiers
(for example "user-agent": "superagent/1.0" and "x-app": "superagent" or remove
x-app entirely), so the client accurately represents itself while preserving
OAuth beta flags.
🧹 Nitpick comments (3)
packages/agent/src/superagent.ts (3)

21-25: Module-level mutable token has no expiry/refresh handling.

anthropicAuthToken is a global singleton — if the OAuth token expires mid-session, all subsequent agent calls will fail with auth errors until setAnthropicAuthToken is called again. Consider:

  1. Accepting a token-provider callback (e.g., () => Promise<string>) instead of a static string, so the caller can handle refresh transparently.
  2. If staying with a static value, exposing a clearAnthropicAuthToken() for logout/session teardown to avoid stale credentials lingering in memory.

127-127: Redundant as string | undefined cast.

requestContext.get() already returns string | undefined per the type signature on line 39. The same cast appears on line 216 as well.

Suggested fix
-		const cwd = requestContext.get("cwd") as string | undefined;
+		const cwd = requestContext.get("cwd");

And similarly at line 216:

-		const cwd = requestContext.get("cwd") as string | undefined;
+		const cwd = requestContext.get("cwd");

66-69: Module-level side effects: PostgresStore and Mastra initialize eagerly on import.

Both storage (line 66) and mastra (line 232) establish infrastructure (Postgres connections, agent registry) at module load time. This means any file that imports anything from this module — even just setAnthropicAuthToken — triggers a database connection attempt. This can cause unexpected failures in tests, tooling, or any context where DATABASE_URL isn't configured.

Consider lazy initialization (factory function or deferred singleton) if this module's exports are consumed in varied contexts.

Also applies to: 231-241

Comment thread packages/agent/src/superagent.ts Outdated
Comment on lines +50 to +59
// OAuth requires specific beta flags and Claude CLI identity headers.
// See: https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/providers/anthropic.ts
return createAnthropic({
authToken: anthropicAuthToken,
headers: {
"anthropic-beta": "claude-code-20250219,oauth-2025-04-20",
"user-agent": "claude-cli/2.1.2 (external, cli)",
"x-app": "cli",
},
})(model);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Anthropic OAuth API required headers documentation

💡 Result:

Anthropic’s public Claude API (api.anthropic.com) does not document an “OAuth” flow; it documents API-key auth with required request headers: x-api-key, anthropic-version, and content-type: application/json. [1]

Required headers (Claude API):

  • x-api-key: <your key> [1]
  • anthropic-version: 2023-06-01 (required version header) [1][2]
  • content-type: application/json [1]

Optional (only if you’re using a beta feature):

  • anthropic-beta: <beta_feature_name> [3]

Bearer token header (OAuth-like) — only documented for specific hosted offerings (e.g., Microsoft Foundry / Entra ID), not the standard Anthropic endpoint:

  • Authorization: Bearer <token> [4]

🌐 Web query:

Anthropic claude-code beta flag oauth 2025

💡 Result:

For Claude Code’s OAuth flow in 2025, Anthropic gated OAuth behind an API beta header. If that header is missing, the API can return 401 with “OAuth authentication is currently not supported.” (a failure mode reported for Claude Code/extension). [3]

What to include

  • Send an anthropic-beta header that includes the OAuth beta, e.g.:
    • anthropic-beta: oauth-2025-04-20 (often alongside other betas in the same header) [1][2]

Why you might be seeing this

  • A documented Claude Code regression (example: v2.0.22) removed the required anthropic-beta header for OAuth, breaking OAuth-authenticated calls until the header was added back (or users downgraded). [2]

Reference: how beta headers work

  • Anthropic’s docs: set anthropic-beta: <beta-feature-name> (comma-separated for multiple betas), where names commonly look like feature-name-YYYY-MM-DD. [1]

Sources:
[1] Anthropic “Beta headers” documentation
[2] “Claude Code v2.0.22 OAuth Fix Patch” gist (documents missing header + shows oauth-2025-04-20)
[3] GitHub issue reporting OAuth tokens rejected with “OAuth authentication is currently not supported”


🌐 Web query:

Anthropic Terms of Service user-agent spoofing API client identity

💡 Result:

Anthropic’s public Commercial Terms of Service / Usage Policy don’t call out the HTTP User-Agent header specifically, but spoofing a user-agent to misrepresent who your API client is (or to evade enforcement) can fall under multiple binding restrictions:

  • Commercial Terms (API customers): Anthropic incorporates the Usage Policy by reference and says you must cooperate with reasonable requests to support compliance, including requests “to verify [your] identity and use of the Services.” If you’re spoofing client identity to frustrate attribution/enforcement, that conflicts with this framework. [1]
  • Usage Policy (applies to API use): Prohibits “gain[ing] unauthorized access … through technical attacks or social engineering” and “bypass[ing] security controls,” plus broader prohibitions on deceptive impersonation / fake personas and other abusive behavior (e.g., circumventing bans/guardrails). Using a spoofed User-Agent to impersonate another app/service (or to evade rate limits, blocks, monitoring, or attribution) is most likely to be treated as policy-violating circumvention/deception. [2]
  • If the spoofing is “pretend to be Anthropic” (e.g., ClaudeBot, Claude-User, Claude-SearchBot): Anthropic publicly documents these crawler identities and frames them as part of “transparen[cy] and choice” for website operators; masquerading as them is plainly deceptive (and may also create legal issues with third parties). [3]

Practical takeaway: Setting any User-Agent string isn’t inherently forbidden, but using it to misrepresent your identity/affiliation or to evade controls is risky and likely non-compliant with Anthropic’s Terms/Usage Policy. [1][2]

Sources
[1] Anthropic – Commercial Terms of Service (effective June 17, 2025) https://www.anthropic.com/legal/commercial-terms
[2] Anthropic – Usage Policy (effective Sept 15, 2025) https://www.anthropic.com/legal/aup
[3] Anthropic Help Center – Web crawling bots / how to block (ClaudeBot / Claude-User / Claude-SearchBot) https://support.anthropic.com/en/articles/8896518-does-anthropic-crawl-data-from-the-web-and-how-can-site-owners-block-the-crawler


Spoofed user-agent header violates Anthropic's Usage Policy.

The hard-coded "user-agent": "claude-cli/2.1.2" and "x-app": "cli" headers misrepresent the client identity as the official Claude CLI. Anthropic's Usage Policy prohibits "deceptive impersonation" and "circumventing security controls or enforcement," and explicitly states that spoofing identity to evade rate limits, monitoring, or attribution is non-compliant.

The beta headers (anthropic-beta: claude-code-20250219,oauth-2025-04-20) are officially documented as required for OAuth support in Claude Code 2025 and are not artifacts; they will be maintained as long as those features are in beta. However, the real issue is the misrepresented user-agent.

Use a truthful user-agent string (e.g., "superagent/1.0") to accurately identify this client application.

🤖 Prompt for AI Agents
In `@packages/agent/src/superagent.ts` around lines 50 - 59, The hard-coded
deceptive client identity is set in the createAnthropic call: update the headers
passed to createAnthropic (where anthropicAuthToken and model are used) to stop
impersonating the official Claude CLI; keep the required "anthropic-beta" header
but replace "user-agent": "claude-cli/2.1.2 (external, cli)" and "x-app": "cli"
with truthful identifiers (for example "user-agent": "superagent/1.0" and
"x-app": "superagent" or remove x-app entirely), so the client accurately
represents itself while preserving OAuth beta flags.

Copy link
Copy Markdown
Contributor

@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

🤖 Fix all issues with AI agents
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx`:
- Line 50: The UI toggle state thinkingEnabled is not being forwarded to the
backend: update the payload passed to triggerAgent.mutate inside handleSend to
include the thinkingEnabled boolean (so the agent receives the toggle), and add
thinkingEnabled to the useCallback dependency array for handleSend to avoid
stale closures; refer to the thinkingEnabled/setThinkingEnabled state, the
ThinkingToggle UI, the handleSend function, and the triggerAgent.mutate call
when making these changes.
- Around line 195-227: handleAnswer currently updates local message state but
conditionally sends answerQuestionMutation based on the mutable pendingApproval,
which can race and cause the mutation to be skipped; fix by capturing
pendingApproval into a local constant at the start of handleAnswer (e.g., const
approval = pendingApproval) then use that captured approval for the mutation and
clearing (answerQuestionMutation.mutate with approval.runId and
setPendingApproval(null)), or split responsibilities so the state update always
runs and the mutation is triggered from a stabilized ref/closure separate from
pendingApproval — change references to pendingApproval in handleAnswer to use
the captured variable to avoid the race.
- Around line 96-101: The onError for triggerAgent
(electronTrpc.aiChat.superagent.useMutation) only logs to console and sets
setIsStreaming(false); update the onError handler to also call setError(...)
with a user-facing message (e.g., err.message || String(err) or a fallback like
"Failed to trigger agent") so users see the failure; ensure you still call
setIsStreaming(false) and preserve existing console.error for debugging.

In `@packages/agent/package.json`:
- Around line 17-27: Update the invalid dependency versions in package.json:
change "@ai-sdk/anthropic" from "3.0.43" to "3.0.41", change "@mastra/ai-sdk"
from "1.0.4" to the published "0.3.3", and change "@mastra/memory" from "1.2.0"
to the stable "1.3.0" so npm install uses existing registry versions; leave the
other entries (e.g., "@tavily/core", "@mastra/pg", "linkedom") unchanged.

In `@packages/agent/src/superagent.ts`:
- Around line 24-28: The module-level mutable anthropicAuthToken and setter
setAnthropicAuthToken must be removed and the token passed per-invocation via
the existing requestContext (the same way modelId and cwd are handled); update
any functions in this module that currently read anthropicAuthToken to instead
accept or read a token field from requestContext, propagate the token from
callers that currently call setAnthropicAuthToken, and delete or deprecate
setAnthropicAuthToken to avoid global state. Ensure all references to
anthropicAuthToken are replaced with requestContext.<token> (or a named field
like requestContext.anthropicAuthToken) so concurrent sessions use their own
tokens.

In `@packages/agent/src/tools/ask-user-question.ts`:
- Around line 54-58: The current catch and final fallback both return { answers:
{} } which is indistinguishable from a successful empty response; change the
behavior in the askUserQuestion flow so errors are signaled explicitly (e.g.,
return a sentinel/result object with an error flag or message instead of
silently returning { answers: {} } or rethrow the error). Specifically, modify
the catch block around the toolAnswers parsing and the final return to produce a
distinguishable shape (for example include error: true / errorMessage or
success: false) or throw an error, and update callers of askUserQuestion (and
any code inspecting toolAnswers/answers) to handle that new sentinel/result so
the agent can retry or inform the user. Ensure the unique symbols touched are
the catch where toolAnswers is parsed and the final return that currently yields
{ answers: {} }.

In `@packages/agent/src/tools/web-fetch.ts`:
- Around line 14-19: The inputSchema defines prompt (z.string().optional()) but
the execute function never reads it; update the execute implementation in
web-fetch.ts to consume the prompt parameter from the parsed input and pass it
into the content-extraction/summarisation flow (e.g., forward to the summariser
or use to filter extracted text) or remove prompt from inputSchema if you intend
not to support it; specifically, modify the execute function that uses the
parsed input object to reference input.prompt and incorporate it into the
extraction/summary step (or delete the prompt field from inputSchema) so user
intent is not silently discarded.
- Around line 27-35: The fetch call using input.url in web-fetch.ts allows SSRF
because it doesn't validate scheme or the resolved IP; update the code around
the fetch invocation (the code referencing input.url and AbortSignal.timeout)
to: 1) allow only safe schemes (e.g., require https, optionally allow http) by
validating the URL.protocol; 2) perform a DNS resolution of the hostname and
validate each resolved IP against a denylist of private/reserved/link-local
ranges (RFC1918, localhost, 169.254.0.0/16, IPv6 link-local, etc.) and block if
any match; and 3) reject the request with a clear error instead of calling fetch
when validation fails. Ensure the hostname/IP check runs before calling fetch so
only public routable addresses are fetched.
- Around line 60-66: The truncation uses UTF-16 characters but bytes were
measured in UTF-8, so change the logic in the block around
TextEncoder/bytes/MAX_CONTENT_BYTES to operate on encoded bytes: encode content
to a Uint8Array with TextEncoder, if encoded.length > MAX_CONTENT_BYTES take
encoded.slice(0, MAX_CONTENT_BYTES), decode that byte slice back to a string
with TextDecoder (so truncated content is byte-accurate), then set content to
that decoded string plus the existing “[Content truncated — ${bytes} bytes
total, showing first ${MAX_CONTENT_BYTES}]” message; update references to bytes
to use encoded.length and use TextEncoder/TextDecoder instead of string.slice to
avoid cutting multi-byte characters.
🧹 Nitpick comments (9)
packages/agent/src/tools/ask-user-question.ts (1)

50-56: Unsafe cast of parsed JSON — validate with Zod instead of as.

JSON.parse(raw) as Record<string, string> trusts the shape blindly. If the injected toolAnswers value is malformed (e.g., nested objects, arrays, or numeric values), the tool will return data that violates its own outputSchema contract without any indication of failure.

Since you already have the exact Zod shape on Line 43, reuse it for runtime validation:

♻️ Proposed fix
 	execute: async (_input, context) => {
+		const answersSchema = z.record(z.string(), z.string());
 		const raw = context?.requestContext?.get("toolAnswers") as
 			| string
 			| undefined;
 		if (raw) {
 			try {
-				const answers = JSON.parse(raw) as Record<string, string>;
-				return { answers };
+				const parsed = answersSchema.safeParse(JSON.parse(raw));
+				if (parsed.success) {
+					return { answers: parsed.data };
+				}
+				return { answers: {} };
 			} catch {
 				return { answers: {} };
 			}
 		}
 		return { answers: {} };
 	},
apps/desktop/src/lib/trpc/routers/ai-chat/index.ts (4)

316-336: Remove verbose debug logging from production code.

The loop that logs tool-part keys and JSON-stringified content for the first 3 messages is development scaffolding that shouldn't ship. It runs on every getMessages call, adding unnecessary I/O and potentially logging sensitive prompt/tool data.

Proposed fix
 			.query(async ({ input }) => {
 				const result = await memory.recall({
 					threadId: input.threadId,
 				});
-				// Convert Mastra DB messages → AI SDK V5 UIMessage format
-				// This normalizes tool invocations with proper toolName, toolCallId, etc.
 				const uiMessages = toAISdkV5Messages(result.messages);
-				// Debug: log tool parts to verify shape
-				for (const msg of uiMessages.slice(0, 3)) {
-					const parts = (msg as Record<string, unknown>).parts as
-						| Array<Record<string, unknown>>
-						| undefined;
-					if (parts) {
-						for (const p of parts) {
-							if (String(p.type ?? "").startsWith("tool-")) {
-								console.log(
-									"[getMessages] V5 tool part keys:",
-									Object.keys(p),
-									"type:",
-									p.type,
-								);
-								console.log(
-									"[getMessages] V5 tool part:",
-									JSON.stringify(p, null, 2).slice(0, 500),
-								);
-							}
-						}
-					}
-				}
 				return uiMessages;
 			}),

558-559: Operator precedence makes this expression misleading.

projectContext + fileMentionContext || undefined parses as (projectContext + fileMentionContext) || undefined. It happens to work because concatenating two empty strings yields "" (falsy), but the intent is obscure and fragile. A reader might expect projectContext + (fileMentionContext || undefined), which would produce "<context>undefined".

Proposed fix — make the intent explicit
-						const contextInstructions =
-							projectContext + fileMentionContext || undefined;
+						const combined = projectContext + fileMentionContext;
+						const contextInstructions = combined || undefined;

621-636: Excessive per-chunk logging will flood the console in production.

The conditional on lines 624-631 logs for every text-delta, every tool* chunk, every finish/start/step-* event, and the first 5 chunks unconditionally. For a typical long response, nearly every chunk matches at least one of these conditions, making this effectively unconditional logging of the entire stream. Consider gating behind a debug flag or removing entirely.


832-923: answerQuestion duplicates nearly all of approveToolCall's resume-stream logic.

The body of answerQuestion (lines 855-920) is an almost line-for-line copy of the fire-and-forget block in approveToolCall (lines 746-827): build ctxEntries, inject pending answers, create RequestContext, call approveToolCall on the agent, iterate fullStream, detect re-suspension, emit done/error. The only difference is that answerQuestion always approves and pre-stores answers on line 849.

Extract a shared helper (e.g., resumeStream(sessionId, runId, approved, extraCtxEntries?)) to eliminate the duplication and ensure future fixes (like the missing AbortController registration flagged earlier) are applied in one place.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx (2)

158-164: handleModelSelect and handlePermissionModeSelect are trivial wrappers — consider passing setters directly.

These useCallback wrappers with empty dependency arrays are functionally equivalent to passing setSelectedModel and setPermissionMode directly. React state setters are already stable references.


46-46: Component is growing large — consider extracting message rendering logic.

ChatInterface handles state management, mutation orchestration, approval flows, and all rendering. The message list rendering block (lines 247–288) and the prompt input section (lines 302–361) are good candidates for extraction into sub-components, improving readability and testability.

Also applies to: 236-364

packages/agent/src/tools/web-fetch.ts (1)

48-58: Wrap body-read and parsing in a try/catch to return a graceful error.

response.text(), parseHTML, and Readability.parse() can all throw on malformed input or encoding issues. An unhandled exception here would bubble up as a raw tool failure. Catching and returning a structured error (similar to the !response.ok path) keeps the agent loop stable.

Proposed fix
-		const rawHtml = await response.text();
-		let content: string;
-
-		if (contentType.includes("text/html") || contentType.includes("xhtml")) {
-			const { document } = parseHTML(rawHtml);
-			const reader = new Readability(document);
-			const article = reader.parse();
-			content = article?.textContent?.trim() ?? rawHtml;
-		} else {
-			content = rawHtml;
-		}
+		let rawHtml: string;
+		try {
+			rawHtml = await response.text();
+		} catch {
+			return { content: "Failed to read response body", bytes: 0, status_code: statusCode };
+		}
+
+		let content: string;
+		try {
+			if (contentType.includes("text/html") || contentType.includes("xhtml")) {
+				const { document } = parseHTML(rawHtml);
+				const reader = new Readability(document);
+				const article = reader.parse();
+				content = article?.textContent?.trim() ?? rawHtml;
+			} else {
+				content = rawHtml;
+			}
+		} catch {
+			content = rawHtml;
+		}
packages/agent/src/tools/web-search.ts (1)

36-47: Consider wrapping the Tavily API call in a try/catch for better error messages.

If the Tavily API call fails (network error, rate limit, invalid response), the raw error will propagate without context. A try/catch with a descriptive re-throw would improve debuggability for end users of the agent.

♻️ Suggested improvement
 		const client = tavily({ apiKey });
-		const response = await client.search(input.query, {
-			maxResults: input.maxResults,
-		});
+		let response;
+		try {
+			response = await client.search(context.query, {
+				maxResults: context.maxResults,
+			});
+		} catch (error) {
+			throw new Error(
+				`Web search failed for query "${context.query}": ${error instanceof Error ? error.message : String(error)}`,
+			);
+		}

const [selectedModel, setSelectedModel] =
useState<ModelOption>(DEFAULT_MODEL);
const [modelSelectorOpen, setModelSelectorOpen] = useState(false);
const [thinkingEnabled, setThinkingEnabled] = useState(false);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

thinkingEnabled is tracked in state but never sent to the backend.

The toggle updates local state (line 50), and the UI renders a ThinkingToggle (line 324), but handleSend at lines 139–145 doesn't include thinkingEnabled in the triggerAgent.mutate(...) payload. The feature toggle has no effect on agent behavior.

Proposed fix
 		triggerAgent.mutate({
 			sessionId,
 			text,
 			modelId: selectedModel.id,
 			cwd,
 			permissionMode,
+			thinkingEnabled,
 		});

And add thinkingEnabled to the useCallback dependency array.

Also applies to: 139-145

🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx`
at line 50, The UI toggle state thinkingEnabled is not being forwarded to the
backend: update the payload passed to triggerAgent.mutate inside handleSend to
include the thinkingEnabled boolean (so the agent receives the toggle), and add
thinkingEnabled to the useCallback dependency array for handleSend to avoid
stale closures; refer to the thinkingEnabled/setThinkingEnabled state, the
ThinkingToggle UI, the handleSend function, and the triggerAgent.mutate call
when making these changes.

Comment on lines 96 to 101
const triggerAgent = electronTrpc.aiChat.superagent.useMutation({
onError: (err) => {
console.error("[chat] Agent trigger failed:", err);
setIsStreaming(false);
},
[sessionId],
);

const { allMessages } = useClaudeCodeHistory({
sessionId,
liveMessages: messages,
onRename: handleRename,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

triggerAgent error handler doesn't set the error message for the user.

The onError callback logs to console but doesn't call setError(...), so if the mutation fails (network issue, server error), the user sees no feedback — the UI just silently stops streaming.

Proposed fix
 const triggerAgent = electronTrpc.aiChat.superagent.useMutation({
 	onError: (err) => {
 		console.error("[chat] Agent trigger failed:", err);
+		setError(err.message ?? "Failed to start agent");
 		setIsStreaming(false);
 	},
 });
🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx`
around lines 96 - 101, The onError for triggerAgent
(electronTrpc.aiChat.superagent.useMutation) only logs to console and sets
setIsStreaming(false); update the onError handler to also call setError(...)
with a user-facing message (e.g., err.message || String(err) or a fallback like
"Failed to trigger agent") so users see the failure; ensure you still call
setIsStreaming(false) and preserve existing console.error for debugging.

Comment on lines 195 to 227
const handleAnswer = useCallback(
(toolCallId: string, answers: Record<string, string>) => {
// Update local state to mark the tool call as answered
setMessages((prev) =>
prev.map((msg) => {
if (msg.role !== "assistant") return msg;
return {
...msg,
parts: msg.parts.map((part) =>
part.type === "tool-call" && part.toolCallId === toolCallId
? {
...part,
status: "done" as const,
result: { answers },
}
: part,
),
};
}),
);

// Resume the agent stream with answers injected into RequestContext
if (pendingApproval) {
answerQuestionMutation.mutate({
sessionId,
runId: pendingApproval.runId,
answers,
});
setPendingApproval(null);
}
},
[interruptAgent, sessionId, stop],
[pendingApproval, answerQuestionMutation, sessionId],
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

handleAnswer couples the question-answer flow tightly to pendingApproval.

The local state update (lines 198–214) uses toolCallId to find the right part — good. But the mutation dispatch (lines 217–224) depends on pendingApproval being non-null, and it's cleared immediately. If handleAnswer is called when pendingApproval has already been cleared (e.g., race with a stream event), the mutation silently doesn't fire despite the local state being updated, leaving the agent stuck.

Consider capturing pendingApproval at the start of the function or separating the two concerns:

Proposed fix
 const handleAnswer = useCallback(
 	(toolCallId: string, answers: Record<string, string>) => {
+		const approval = pendingApproval;
 		// Update local state to mark the tool call as answered
 		setMessages((prev) =>
 			prev.map((msg) => { /* ... */ }),
 		);
 
-		if (pendingApproval) {
+		if (approval) {
 			answerQuestionMutation.mutate({
 				sessionId,
-				runId: pendingApproval.runId,
+				runId: approval.runId,
 				answers,
 			});
 			setPendingApproval(null);
 		}
 	},
 	[pendingApproval, answerQuestionMutation, sessionId],
 );
🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx`
around lines 195 - 227, handleAnswer currently updates local message state but
conditionally sends answerQuestionMutation based on the mutable pendingApproval,
which can race and cause the mutation to be skipped; fix by capturing
pendingApproval into a local constant at the start of handleAnswer (e.g., const
approval = pendingApproval) then use that captured approval for the mutation and
clearing (answerQuestionMutation.mutate with approval.runId and
setPendingApproval(null)), or split responsibilities so the state update always
runs and the mutation is triggered from a stabilized ref/closure separate from
pendingApproval — change references to pendingApproval in handleAnswer to use
the captured variable to avoid the race.

Comment thread packages/agent/package.json Outdated
Comment on lines +17 to +27
"@ai-sdk/anthropic": "^3.0.43",
"@anthropic-ai/claude-agent-sdk": "^0.2.38",
"@mastra/ai-sdk": "^1.0.4",
"@mastra/core": "^1.3.0",
"@mastra/memory": "^1.2.0",
"@mastra/pg": "^1.3.0",
"@mozilla/readability": "^0.6.0",
"@superset/durable-session": "workspace:*",
"@tanstack/ai": "^0.3.0",
"@tavily/core": "^0.7.1",
"linkedom": "^0.18.12",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify package versions exist on npm registry

packages=(
  "@ai-sdk/anthropic@3.0.43"
  "@mastra/ai-sdk@1.0.4"
  "@mastra/core@1.3.0"
  "@mastra/memory@1.2.0"
  "@mastra/pg@1.3.0"
  "@tavily/core@0.7.1"
  "linkedom@0.18.12"
)

for pkg in "${packages[@]}"; do
  echo "=== Checking $pkg ==="
  # Extract package name and version
  if [[ $pkg == @* ]]; then
    # Handle scoped packages
    name=$(echo "$pkg" | rev | cut -d'@' -f2- | rev)
    version=$(echo "$pkg" | rev | cut -d'@' -f1 | rev)
  else
    name=$(echo "$pkg" | cut -d'@' -f1)
    version=$(echo "$pkg" | cut -d'@' -f2)
  fi
  
  # Check if version exists on npm
  response=$(curl -s "https://registry.npmjs.org/${name}/${version}")
  if echo "$response" | grep -q '"version"'; then
    echo "✓ Version exists"
  else
    echo "✗ Version not found or error"
    echo "Response: $(echo "$response" | head -c 200)"
  fi
done

Repository: superset-sh/superset

Length of output: 1639


🌐 Web query:

@ai-sdk/anthropic 3.0.43 security vulnerability

💡 Result:

I couldn’t find any published security vulnerability/advisory specifically affecting @ai-sdk/anthropic v3.0.43.

  • @ai-sdk/anthropic v3.0.43 doesn’t appear in public trackers I checked (e.g., Snyk’s version list shows 3.0.41 but not 3.0.43), so the “3.0.43” version may be a typo, unpublished, or released after the tracker snapshot. [1]
  • The upstream repo vercel/ai has no published GitHub Security Advisories at all (at least at the time of the snapshot). [2]
  • Multiple package scanners/trackers show no known/direct vulnerabilities for @ai-sdk/anthropic versions they list. [1], [3]

Note: there is a CVE for the ai npm package (not @ai-sdk/anthropic) about bypassing filetype whitelists in uploads, affecting ai versions < 5.0.52 (and some betas). [4]

If you share the advisory/CVE/GHSA link (or the tool output flagging 3.0.43), I can map it precisely to affected versions and remediation.

Sources: [1] [2] [3] [4]


🌐 Web query:

@mastra/ai-sdk @mastra/core security vulnerabilities 2025 2026

💡 Result:

Findings (2025–2026)

  • @mastra/core: As of Feb 2026, Snyk lists no known direct vulnerabilities for @mastra/core (latest shown: 1.3.0). [1]
  • @mastra/ai-sdk: I did not find any public CVEs/advisories specifically for @mastra/ai-sdk; Aikido Intel reports 0 vulnerabilities for the package (latest shown: 0.3.3). [2]

Related Mastra advisory (not these two packages, but in the ecosystem)

  • CVE-2025-61685 / GHSA-xh92-rqrq-227v: A directory traversal / directory listing disclosure affecting @mastra/mcp-docs-server (patched in 0.17.0)—this is not @mastra/core or @mastra/ai-sdk, but it may matter if your project uses that MCP server. [3][4]

Sources: Snyk package page for @mastra/core [1], Aikido Intel package page for @mastra/ai-sdk [2], NVD CVE entry [3], GitHub Security Advisory [4].


🌐 Web query:

linkedom 0.18.12 security vulnerability

💡 Result:

No public security vulnerability specific to linkedom v0.18.12 appears to be currently disclosed:

  • GitHub (WebReflection/linkedom): the project shows no published security advisories. [1]
  • Snyk (npm linkedom): reports 0 direct vulnerabilities for v0.18.12 and lists 0.18.12 as the latest non‑vulnerable version (published 21 Aug 2025). [2]
  • Aikido Intel: reports no vulnerabilities found for the package. [3]

If you’re seeing an alert in a specific scanner, it’s likely an indirect/dependency or heuristic/supply-chain flag—share the exact finding (CVE/GHSA/OSV ID or scanner output) and I can validate it against primary sources.

Sources: [1] [2] [3]


🏁 Script executed:

#!/bin/bash
# More robust check for npm packages using simpler parsing

echo "=== Checking `@tavily/core` latest versions ==="
curl -s "https://registry.npmjs.org/@tavily/core" | jq '.versions | keys | .[-5:]' 2>/dev/null || echo "Could not fetch"

echo ""
echo "=== Checking `@mastra/pg` latest versions ==="
curl -s "https://registry.npmjs.org/@mastra/pg" | jq '.versions | keys | .[-5:]' 2>/dev/null || echo "Could not fetch"

echo ""
echo "=== Checking `@mastra/memory` latest versions ==="
curl -s "https://registry.npmjs.org/@mastra/memory" | jq '.versions | keys | .[-5:]' 2>/dev/null || echo "Could not fetch"

echo ""
echo "=== Checking `@ai-sdk/anthropic` version 3.0.41 vs 3.0.43 ==="
curl -s "https://registry.npmjs.org/@ai-sdk/anthropic" | jq '.versions | keys | map(select(startswith("3.0"))) | sort_by(split(".") | map(tonumber))' 2>/dev/null || echo "Could not fetch"

Repository: superset-sh/superset

Length of output: 510


Fix package version references that do not exist on npm registry.

Three dependencies reference versions that are not published:

  • @ai-sdk/anthropic@3.0.43 does not exist; latest available is 3.0.41
  • @mastra/ai-sdk@1.0.4 does not exist; only 0.3.3 is available
  • @mastra/memory@1.2.0 does not exist as a stable release; only alpha versions are published, with latest stable being 1.3.0

Update these to valid published versions to prevent npm install failures. The other dependencies (@tavily/core@0.7.1, @mastra/pg@1.3.0, and linkedom@0.18.12) exist and have no known security vulnerabilities.

🤖 Prompt for AI Agents
In `@packages/agent/package.json` around lines 17 - 27, Update the invalid
dependency versions in package.json: change "@ai-sdk/anthropic" from "3.0.43" to
"3.0.41", change "@mastra/ai-sdk" from "1.0.4" to the published "0.3.3", and
change "@mastra/memory" from "1.2.0" to the stable "1.3.0" so npm install uses
existing registry versions; leave the other entries (e.g., "@tavily/core",
"@mastra/pg", "linkedom") unchanged.

Comment on lines +24 to +28
let anthropicAuthToken: string | null = null;

export function setAnthropicAuthToken(token: string) {
anthropicAuthToken = token;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Global mutable anthropicAuthToken is shared across all concurrent sessions.

anthropicAuthToken is a module-level singleton. If multiple users or sessions call setAnthropicAuthToken with different OAuth tokens, they silently overwrite each other — the last writer wins and all subsequent agent invocations (including those belonging to other sessions) use that token. This is both a correctness bug (wrong credentials) and a security issue (token leakage across sessions).

Pass the token per-invocation via requestContext instead of relying on module-level state, which is already the pattern used for modelId and cwd.

Suggested approach
-let anthropicAuthToken: string | null = null;
-
-export function setAnthropicAuthToken(token: string) {
-	anthropicAuthToken = token;
-}
-
 ...
 function resolveModel({
 	requestContext,
 }: {
 	requestContext: { get: (key: string) => string | undefined };
 }) {
 	const modelId =
 		requestContext.get("modelId") ?? "anthropic/claude-sonnet-4-5";
+	const authToken = requestContext.get("anthropicAuthToken");
 
-	if (anthropicAuthToken) {
+	if (authToken) {
 		const slashIdx = modelId.indexOf("/");
 		const provider = slashIdx > -1 ? modelId.slice(0, slashIdx) : "anthropic";
 		const model = slashIdx > -1 ? modelId.slice(slashIdx + 1) : modelId;
 
 		if (provider === "anthropic") {
 			return createAnthropic({
-				authToken: anthropicAuthToken,
+				authToken,
 				headers: { ... },
 			})(model);
 		}
 	}
 	return modelId;
 }

The caller that currently calls setAnthropicAuthToken(token) would instead pass the token through requestContext when invoking the agent.

🤖 Prompt for AI Agents
In `@packages/agent/src/superagent.ts` around lines 24 - 28, The module-level
mutable anthropicAuthToken and setter setAnthropicAuthToken must be removed and
the token passed per-invocation via the existing requestContext (the same way
modelId and cwd are handled); update any functions in this module that currently
read anthropicAuthToken to instead accept or read a token field from
requestContext, propagate the token from callers that currently call
setAnthropicAuthToken, and delete or deprecate setAnthropicAuthToken to avoid
global state. Ensure all references to anthropicAuthToken are replaced with
requestContext.<token> (or a named field like requestContext.anthropicAuthToken)
so concurrent sessions use their own tokens.

Comment on lines +54 to +58
} catch {
return { answers: {} };
}
}
return { answers: {} };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Silent empty-answers fallback may mislead the agent.

Both error paths (parse failure on Line 54 and missing toolAnswers on Line 58) return { answers: {} }, which is indistinguishable from a successful result with no answers. The agent has no signal that something went wrong and may proceed as if the user declined to answer.

Consider either logging/throwing on the error path or returning a distinguishable sentinel so the agent can retry or inform the user.

🤖 Prompt for AI Agents
In `@packages/agent/src/tools/ask-user-question.ts` around lines 54 - 58, The
current catch and final fallback both return { answers: {} } which is
indistinguishable from a successful empty response; change the behavior in the
askUserQuestion flow so errors are signaled explicitly (e.g., return a
sentinel/result object with an error flag or message instead of silently
returning { answers: {} } or rethrow the error). Specifically, modify the catch
block around the toolAnswers parsing and the final return to produce a
distinguishable shape (for example include error: true / errorMessage or
success: false) or throw an error, and update callers of askUserQuestion (and
any code inspecting toolAnswers/answers) to handle that new sentinel/result so
the agent can retry or inform the user. Ensure the unique symbols touched are
the catch where toolAnswers is parsed and the final return that currently yields
{ answers: {} }.

Comment on lines +14 to +19
prompt: z
.string()
.optional()
.describe(
"Optional prompt describing what information to look for on the page",
),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

prompt is declared in the input schema but never used in execute.

The agent (or caller) can supply a prompt describing what to look for, but the execute function never reads it. This silently discards user intent. Either remove the field from inputSchema or incorporate it into the content-extraction logic (e.g., pass it to a summarisation step or use it to filter content).

🤖 Prompt for AI Agents
In `@packages/agent/src/tools/web-fetch.ts` around lines 14 - 19, The inputSchema
defines prompt (z.string().optional()) but the execute function never reads it;
update the execute implementation in web-fetch.ts to consume the prompt
parameter from the parsed input and pass it into the
content-extraction/summarisation flow (e.g., forward to the summariser or use to
filter extracted text) or remove prompt from inputSchema if you intend not to
support it; specifically, modify the execute function that uses the parsed input
object to reference input.prompt and incorporate it into the extraction/summary
step (or delete the prompt field from inputSchema) so user intent is not
silently discarded.

Comment thread packages/agent/src/tools/web-fetch.ts Outdated
Comment on lines +27 to +35
const response = await fetch(input.url, {
headers: {
"User-Agent":
"Mozilla/5.0 (compatible; SupersetAgent/1.0; +https://superset.sh)",
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
},
redirect: "follow",
signal: AbortSignal.timeout(15_000),
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

SSRF risk: no validation that the URL points to a public host.

The tool fetches any URL the agent provides. A prompt-injection or a malicious link could direct it to internal network addresses (e.g., http://169.254.169.254/... for cloud metadata, http://localhost:..., private RFC-1918 ranges). Consider:

  • Blocking private/reserved IP ranges and link-local addresses before fetching.
  • Restricting the scheme to https (and optionally http).
  • Resolving the hostname and validating the resolved IP before connecting.
🤖 Prompt for AI Agents
In `@packages/agent/src/tools/web-fetch.ts` around lines 27 - 35, The fetch call
using input.url in web-fetch.ts allows SSRF because it doesn't validate scheme
or the resolved IP; update the code around the fetch invocation (the code
referencing input.url and AbortSignal.timeout) to: 1) allow only safe schemes
(e.g., require https, optionally allow http) by validating the URL.protocol; 2)
perform a DNS resolution of the hostname and validate each resolved IP against a
denylist of private/reserved/link-local ranges (RFC1918, localhost,
169.254.0.0/16, IPv6 link-local, etc.) and block if any match; and 3) reject the
request with a clear error instead of calling fetch when validation fails.
Ensure the hostname/IP check runs before calling fetch so only public routable
addresses are fetched.

Comment thread packages/agent/src/tools/web-fetch.ts Outdated
Comment on lines +60 to +66
const bytes = new TextEncoder().encode(content).length;

if (bytes > MAX_CONTENT_BYTES) {
content =
content.slice(0, MAX_CONTENT_BYTES) +
`\n\n[Content truncated — ${bytes} bytes total, showing first ${MAX_CONTENT_BYTES}]`;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Character-based slice doesn't honour the byte-based limit.

bytes is measured in UTF-8 bytes, but content.slice(0, MAX_CONTENT_BYTES) truncates by UTF-16 code units. For multi-byte content (CJK, emoji, etc.), 50 000 characters can encode to well over 50 000 bytes, so the truncation under-cuts for ASCII but overshoots for non-ASCII text.

If byte-accurate truncation matters, encode first, then truncate the byte array:

Proposed fix
-		const bytes = new TextEncoder().encode(content).length;
-
-		if (bytes > MAX_CONTENT_BYTES) {
-			content =
-				content.slice(0, MAX_CONTENT_BYTES) +
-				`\n\n[Content truncated — ${bytes} bytes total, showing first ${MAX_CONTENT_BYTES}]`;
-		}
+		const encoded = new TextEncoder().encode(content);
+		const bytes = encoded.length;
+
+		if (bytes > MAX_CONTENT_BYTES) {
+			content =
+				new TextDecoder().decode(encoded.slice(0, MAX_CONTENT_BYTES)) +
+				`\n\n[Content truncated — ${bytes} bytes total, showing first ${MAX_CONTENT_BYTES}]`;
+		}
📝 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 bytes = new TextEncoder().encode(content).length;
if (bytes > MAX_CONTENT_BYTES) {
content =
content.slice(0, MAX_CONTENT_BYTES) +
`\n\n[Content truncated — ${bytes} bytes total, showing first ${MAX_CONTENT_BYTES}]`;
}
const encoded = new TextEncoder().encode(content);
const bytes = encoded.length;
if (bytes > MAX_CONTENT_BYTES) {
content =
new TextDecoder().decode(encoded.slice(0, MAX_CONTENT_BYTES)) +
`\n\n[Content truncated — ${bytes} bytes total, showing first ${MAX_CONTENT_BYTES}]`;
}
🤖 Prompt for AI Agents
In `@packages/agent/src/tools/web-fetch.ts` around lines 60 - 66, The truncation
uses UTF-16 characters but bytes were measured in UTF-8, so change the logic in
the block around TextEncoder/bytes/MAX_CONTENT_BYTES to operate on encoded
bytes: encode content to a Uint8Array with TextEncoder, if encoded.length >
MAX_CONTENT_BYTES take encoded.slice(0, MAX_CONTENT_BYTES), decode that byte
slice back to a string with TextDecoder (so truncated content is byte-accurate),
then set content to that decoded string plus the existing “[Content truncated —
${bytes} bytes total, showing first ${MAX_CONTENT_BYTES}]” message; update
references to bytes to use encoded.length and use TextEncoder/TextDecoder
instead of string.slice to avoid cutting multi-byte characters.

@Kitenite Kitenite merged commit 4d74787 into main Feb 15, 2026
14 of 15 checks passed
@Kitenite Kitenite deleted the m-agent-two branch February 15, 2026 08:20
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.

2 participants