Skip to content

feat(desktop): add diagnostics, database explorer, and workspace UX improvements#44

Merged
MocA-Love merged 20 commits intomainfrom
feat/add-some-tool
Apr 1, 2026
Merged

feat(desktop): add diagnostics, database explorer, and workspace UX improvements#44
MocA-Love merged 20 commits intomainfrom
feat/add-some-tool

Conversation

@MocA-Love
Copy link
Copy Markdown
Owner

@MocA-Love MocA-Love commented Apr 1, 2026

close #45
close #46

概要

  • Desktop app に言語診断、Database Explorer、Git/Browser 周りの改善に加えて、VS Code ライクなワークスペース検索サイドバーを追加します。
  • 差分は主に apps/desktop に集中しており、一部 workspace-fs / host-service 側に検索・置換 API の拡張が入っています。

主な変更

  • 言語診断基盤を追加し、TypeScript/JavaScript/TSX/JSX、JSON/JSONC、TOML、Dart の diagnostics を扱えるようにしました。
  • Problems タブを追加し、ワークスペース全体のエラー・警告を一覧、絞り込み、再取得、該当ファイルへのジャンプできるようにしました。
  • Settings に diagnostics の有効/無効切り替えを追加しました。
  • 右サイドバーとタブペインに Database Explorer を追加し、SQLite/Postgres 接続管理、クエリ実行、テーブルプレビュー、セル詳細表示、編集補助を追加しました。
  • database cell detail 表示を改善し、Markdown/JSON を見やすく扱えるようにしました。
  • Browser に編集可能な bookmark bar を追加し、追加・編集・削除・並び替えに対応しました。
  • Browser の新規 split でのリンクオープン挙動を修正しました。
  • Git sidebar の repository panel を強化し、GitHub issue 作成、PR確認、workflow 実行と実行状況追跡、avatar 表示を追加しました。
  • repository panel の初期状態を折りたたみに変更しました。
  • リソース使用量ヘッダーに CPU 表示を追加し、開発時の heap / Bun 設定も調整しました。
  • 右サイドバーに Search タブを追加し、ワークスペース全文検索、include/exclude glob、正規表現検索、Match Case、結果のファイル単位グルーピング、行・列ジャンプ、ファイル単位置換、ワークスペース全体置換、Cmd/Ctrl+Shift+F を追加しました。
  • workspace-fs / filesystem TRPC に、正規表現対応の検索・置換 API を追加しました。
  • 言語サービス構成のドキュメントを追加しました。

補足

  • ベースブランチは fork 側の main です。
  • 確認できているのは packages/workspace-fspackages/host-service の typecheck です。
  • apps/desktop の全体 typecheck は、この差分外の既存 TypeScript エラーがあるため現状では通っていません。
  • 手元でのランタイム確認は未実施です。

Summary by CodeRabbit

  • New Features

    • IDE language diagnostics (TypeScript, JS, JSON, TOML, Dart) with provider management, Problems panel, editor diagnostics/decorations, and workspace sync.
    • Database Explorer pane for discovering, inspecting, previewing, and running SQL on SQLite/Postgres.
    • Search & Replace across files with regex/case options and detailed replace results.
    • Enhanced GitHub workflows: create issues, upload assets, dispatch workflows, and rerun PR checks.
    • Browser: bookmarks UI, bookmark bar, reload/hard-reload menu & shortcuts.
    • New “Prevent Mac sleep during agent tasks” setting.
  • Documentation

    • Added language services architecture and provider integration guide.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 1, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 472f1e82-f0ab-4a1f-91bf-e8c6b46a5931

📥 Commits

Reviewing files that changed from the base of the PR and between 87d87d5 and 6931a8d.

📒 Files selected for processing (6)
  • apps/desktop/src/lib/trpc/routers/databases/index.ts
  • apps/desktop/src/lib/trpc/routers/filesystem/index.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts
  • apps/desktop/src/main/lib/language-services/providers/typescript/TypeScriptLanguageProvider.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/DiagnosticsSettings/DiagnosticsSettings.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/DatabasesView/DatabasesView.tsx

📝 Walkthrough

Walkthrough

Adds a language‑services subsystem (providers, manager, diagnostics store, LSP/stdio client), Problems UI and editor diagnostics, search-and-replace (regex/case) with replace operation, a Database Explorer (SQLite/Postgres), GitHub workflow/issue integrations, browser bookmarks, hotkeys/browser shortcuts, and macOS agent sleep prevention.

Changes

Cohort / File(s) Summary
Language Services Core
apps/desktop/src/main/lib/language-services/types.ts, utils.ts, diagnostics-store.ts, manager.ts, lsp/StdioJsonRpcClient.ts
New types, utils, diagnostics store with versioned snapshots, manager coordinating providers, and a stdio JSON‑RPC client for LSP communication.
Language Service Providers
apps/desktop/src/main/lib/language-services/providers/*
New providers: TypeScript (tsserver), JSON (vscode-json-languageservice), TOML (Taplo), Dart (Dart LSP). Each manages lifecycle, diagnostics normalization, and workspace sessions.
Renderer Sync & Router
apps/desktop/src/lib/trpc/routers/language-services/index.ts, apps/desktop/src/renderer/providers/LanguageServicesProvider/*
tRPC endpoints to open/change/close docs, get/subscribe diagnostics, and renderer component syncing editor documents and provider enablement.
Diagnostics UI & Editor Integration
apps/desktop/src/renderer/screens/.../ProblemsView/*, CodeEditor.tsx, CodeMirrorDiffViewer.tsx, DiagnosticsSettings.tsx
Problems sidebar with grouping/filtering, editor/diff diagnostic decorations, settings toggles per provider, diagnostic subscription wiring.
Search & Replace (Core + API + UI)
packages/workspace-fs/src/{search,types}.ts, packages/workspace-fs/src/..., packages/host-service/src/trpc/router/filesystem/*, apps/desktop/src/renderer/.../SearchView/**
Search gains regex/case options, new replaceContent operation (per-file updates/conflicts/failures), host/transport/trpc plumbing, and full Search/Replace sidebar UI.
Database Explorer (Core + UI)
apps/desktop/src/lib/trpc/routers/databases/index.ts, .../DatabaseExplorerPane/*, stores/tabs/*
TRPC for discovery, schema inspection, previews and SQL execution for SQLite/Postgres; UI pane, tab/pane factories, and tabs store integration.
GitHub Integration & Review UI
apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts, .../RepositoryPanel/*, ReviewPanel.tsx
Repo overview, create issues, upload assets, dispatch workflows, list/rerun runs; UI repository panel with issue composer and workflow controls; identity candidate shape changed to include avatars.
Browser Features & Bookmarks
apps/desktop/src/renderer/stores/browser-bookmarks.ts, .../BrowserPane/*, BrowserToolbar, BookmarkBar/*
Persisted bookmarks store (CRUD, reorder/toggle), toolbar bookmark toggle, bookmark bar with DnD and edit dialog, webview reload handling via shortcut/menu.
Tabs / Sidebar / Pane Types
apps/desktop/src/shared/tabs-types.ts, apps/desktop/src/renderer/stores/tabs/*, sidebar-state.ts
Adds database-explorer pane type, tab/pane factories and store methods, new sidebar tabs (Search, Problems, Databases), and pane routing.
Hotkeys & Browser Shortcut Events
apps/desktop/src/shared/hotkeys.ts, apps/desktop/src/renderer/lib/browser-shortcut-events.ts, apps/desktop/src/main/lib/menu.ts, apps/desktop/src/lib/trpc/routers/menu.ts
New Browser hotkeys and menu items, renderer event dispatch/listener for browser shortcuts, menu emits browser-action events when no focused webview.
Agent Sleep Prevention
packages/local-db/*, apps/desktop/src/main/lib/agent-setup/templates/*, terminal/env.ts, apps/desktop/src/lib/trpc/routers/settings/index.ts
DB migration and constant, settings endpoints, propagate SUPERSET_PREVENT_AGENT_SLEEP into agent wrapper/env and add caffeinate-based sleep inhibition in hook templates.
Search/Editor Misc & Docs
apps/desktop/package.json, apps/desktop/docs/LANGUAGE_SERVICES.md, MarkdownRenderer.tsx, various renderer/layout updates
Dev script/node opts and deps, new LANGUAGE_SERVICES doc, minor UI prop and layout wiring to include LanguageServicesProvider and new sidebar tabs.

Sequence Diagram(s)

sequenceDiagram
  participant Renderer as Renderer Process
  participant TRPC as tRPC Router
  participant Manager as LanguageService Manager (Main)
  participant Provider as Language Provider (tsserver/json/taplo/dart)
  participant Store as Diagnostics Store

  Renderer->>TRPC: openDocument(workspaceId, document)
  TRPC->>Manager: openDocument(document)
  Manager->>Provider: openDocument(document)
  Provider->>Provider: analyze / run LSP/tsserver
  Provider->>Store: setFileDiagnostics(file, diagnostics)
  Store->>Store: update version & emit workspace event
  Renderer->>TRPC: subscribeDiagnostics(workspaceId)
  TRPC-->>Renderer: diagnostics update notification
Loading
sequenceDiagram
  participant UI as Search UI (Renderer)
  participant TRPC as tRPC Router
  participant FSHost as workspace-fs host
  participant Search as search.ts
  participant FS as Filesystem

  UI->>TRPC: searchContent(query, options)
  TRPC->>FSHost: searchContent(...)
  FSHost->>Search: searchContent(options)
  Search->>FS: glob & read files
  Search-->>TRPC: matches grouped by file
  TRPC-->>UI: results
  UI->>TRPC: replaceContent(query,replacement,paths?)
  TRPC->>FSHost: replaceContent(...)
  FSHost->>Search: replaceContent(...)
  Search->>FS: read, write (with precondition)
  Search-->>TRPC: updated/conflicts/failed
  TRPC-->>UI: replace result
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 I hopped through files and linted each line,

Tsserver and Taplo now sing in a line.
Search that replaces and DBs to explore,
Bookmarks, Problems—diagnostics galore.
A tiny rabbit cheers: "Features—ask for more!"

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/add-some-tool

@MocA-Love MocA-Love self-assigned this Apr 1, 2026
@MocA-Love MocA-Love marked this pull request as ready for review April 1, 2026 17:46
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 19

Note

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

Caution

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

⚠️ Outside diff range comments (1)
apps/desktop/src/lib/trpc/routers/filesystem/index.ts (1)

243-256: ⚠️ Potential issue | 🟠 Major

Don't trim the query at the API boundary.

Trimming here changes the operation itself: " " becomes a no-op, and literals/patterns like " foo" or "bar " can never be found or replaced. Only query === "" should short-circuit; otherwise pass the raw query through unchanged. The mirrored host-service router should stay aligned with the same behavior.

Suggested fix
-				const trimmedQuery = input.query.trim();
-				if (!trimmedQuery) {
+				if (input.query.length === 0) {
 					return { matches: [] };
 				}
…
-					query: trimmedQuery,
+					query: input.query,-				const trimmedQuery = input.query.trim();
-				if (!trimmedQuery) {
+				if (input.query.length === 0) {
 					return {
 						replacements: 0,
 						filesUpdated: 0,
 						updated: [],
 						conflicts: [],
 						failed: [],
 					};
 				}
…
-					query: trimmedQuery,
+					query: input.query,

Also applies to: 263-283

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

In `@apps/desktop/src/lib/trpc/routers/filesystem/index.ts` around lines 243 -
256, The code trims the query at the API boundary (creating trimmedQuery from
input.query.trim()) which alters search semantics; instead, only short-circuit
when input.query === "" and pass the raw input.query into
getServiceForWorkspace(...).searchContent (and the mirrored host-service router)
so leading/trailing spaces are preserved for literal/pattern searches—remove use
of trimmedQuery, use input.query directly for the query param, and apply the
same change to the other block referenced (the one around lines 263-283).
🟡 Minor comments (13)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx-333-347 (1)

333-347: ⚠️ Potential issue | 🟡 Minor

Allow empty diff snapshots to hydrate the document state.

"" is a valid diffData.modified value for deletions and truncations. The length === 0 guard skips applyLoadedDocumentContent in that case, so the backing document never gets updated to the empty snapshot and can keep stale state/diagnostics.

🩹 Proposed fix
 	useEffect(() => {
 		if (
 			viewMode !== "diff" ||
 			isDirty ||
-			!diffData ||
-			diffData.modified.length === 0
+			!diffData
 		) {
 			return;
 		}
 
 		applyLoadedDocumentContent(
 			documentKey,
 			diffData.modified,
 			workingCopyRevision ?? rawRevision ?? null,
 		);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx`
around lines 333 - 347, The current effect early-returns when
diffData.modified.length === 0, which prevents applying valid empty-string
snapshots (deletions/truncations); update the guard so it only returns when
diffData is falsy or diffData.modified is null/undefined (not when it's an empty
string or empty array). Concretely, in the useEffect that checks
viewMode/isDirty/diffData, remove the length===0 check and replace it with a
nullish check (e.g., diffData?.modified == null) so
applyLoadedDocumentContent(documentKey, diffData.modified, workingCopyRevision
?? rawRevision ?? null) runs for empty-string snapshots as well.
apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/RepositoryPanel/RepositoryPanel.tsx-257-265 (1)

257-265: ⚠️ Potential issue | 🟡 Minor

Label the icon-only remove buttons.

These controls render only LuX, so assistive tech has no accessible name for them. Add an aria-label (and ideally a title) so users can tell whether the button removes a tracked run or an attached asset.

Also applies to: 950-958

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

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/RepositoryPanel/RepositoryPanel.tsx`
around lines 257 - 265, The remove Buttons in RepositoryPanel render only the
LuX icon so they lack accessible names; update the Button that calls
onRemove(tracked.workflowId) (and the similar Button used for attached assets)
to include an appropriate aria-label (e.g., aria-label={`Remove tracked run
${tracked.workflowId}`} or aria-label={`Remove attached asset ${asset.name}`})
and also add a title attribute with the same brief text; locate the Buttons in
RepositoryPanel (the one wrapping <LuX /> and any other icon-only remove Buttons
around the repository/asset UI) and add these attributes so assistive tech and
hover tooltips convey the action and target.
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/components/BookmarkBarItem/components/EditBookmarkDialog/EditBookmarkDialog.tsx-42-47 (1)

42-47: ⚠️ Potential issue | 🟡 Minor

Trim and validate the URL before saving.

The current submit path forwards url as-is, so this dialog can persist empty or whitespace-padded bookmark URLs. Trim before onSave and block submit when the URL is blank.

✂️ Proposed fix
 				<form
 					className="space-y-4"
 					onSubmit={(event) => {
 						event.preventDefault();
-						onSave({ title, url });
+						const nextTitle = title.trim();
+						const nextUrl = url.trim();
+						if (!nextUrl) {
+							return;
+						}
+						onSave({ title: nextTitle, url: nextUrl });
 					}}
 				>
@@
-						<Button type="submit">Save</Button>
+						<Button type="submit" disabled={!url.trim()}>
+							Save
+						</Button>

Also applies to: 71-79

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

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/components/BookmarkBarItem/components/EditBookmarkDialog/EditBookmarkDialog.tsx`
around lines 42 - 47, The form submit handler currently calls onSave({ title,
url }) with the raw url; update the onSubmit in EditBookmarkDialog to trim and
validate the URL first by computing const trimmedUrl = url.trim(), then block
submission (e.g., set an error state or return early) when trimmedUrl === "" and
only call onSave({ title, url: trimmedUrl }) when valid; apply the same
trim-and-validate change to the other submit handler in this component that
currently forwards title and url as-is.
apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts-493-513 (1)

493-513: ⚠️ Potential issue | 🟡 Minor

Add the missing supported-language keywords here.

searchSettings() only indexes title, description, and keywords, but this entry omits javascript, jsx, and jsonc. Searches for those providers will not surface the new diagnostics toggle even though this PR adds them.

➕ Suggested keyword additions
 		keywords: [
 			"diagnostics",
 			"problems",
 			"errors",
 			"warnings",
+			"javascript",
 			"typescript",
+			"jsx",
 			"tsx",
 			"json",
+			"jsonc",
 			"toml",
 			"dart",
 			"flutter",
 			"language server",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts`
around lines 493 - 513, The keywords array for the settings entry identified by
SETTING_ITEM_ID.BEHAVIOR_LANGUAGE_DIAGNOSTICS is missing some supported-language
tokens so searchSettings() won't index them; update the keywords array in that
object to include the additional language identifiers (e.g., "javascript",
"jsx", "jsonc") so title/description/keywords indexing by searchSettings() will
surface this diagnostics toggle for those providers.
apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/CodeEditor.tsx-601-613 (1)

601-613: ⚠️ Potential issue | 🟡 Minor

Memoize editorTheme to prevent unnecessary effect reconfiguration on every render.

The getEditorTheme() function returns a new object reference on every call. Without memoization at line 450, editorTheme will have a different reference on each render, causing the effect at lines 601–613 to re-run unnecessarily even when the theme hasn't changed. Wrap the call with useMemo keyed on activeTheme:

const editorTheme = useMemo(() => getEditorTheme(activeTheme), [activeTheme]);

This pattern is already correctly implemented in DiffScrollbarDecorations.tsx (line 161).

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

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/CodeEditor.tsx`
around lines 601 - 613, The effect reconfigures diagnostics because editorTheme
is a new object each render; memoize the result of getEditorTheme so its
reference only changes when activeTheme changes. Replace direct calls to
getEditorTheme() that set editorTheme with a useMemo wrapper keyed on
activeTheme (e.g., const editorTheme = useMemo(() =>
getEditorTheme(activeTheme), [activeTheme])) so the diagnostics effect (which
uses diagnosticsCompartment.reconfigure and buildDiagnosticDecorations) only
runs when the actual theme changes.
apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/SearchView/utils/searchPattern/searchPattern.tsx-74-97 (1)

74-97: ⚠️ Potential issue | 🟡 Minor

Skip zero-width matches instead of marking the next character.

Patterns like \b, ^, $, or lookaheads produce match[0].length === 0. The current matchLength = 1 branch highlights the following character, so the preview becomes wrong for valid regex searches. Advance lastIndex to avoid the loop, but don't synthesize a one-character <mark>.

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

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/SearchView/utils/searchPattern/searchPattern.tsx`
around lines 74 - 97, The loop currently treats zero-length regex matches by
faking a one-character match and highlighting the next character; instead,
detect when matchText (match[0]) has length 0 before creating the <mark>, do NOT
set matchLength = 1 or push a synthesized <mark>, and simply advance
regex.lastIndex (or set it to match.index + 1 for global regex) and continue the
loop so we skip the zero-width match without modifying cursor or inserting a
mark; adjust the logic around matchText/matchLength, regex.lastIndex, nodes, and
cursor in the while (match) loop inside searchPattern.tsx accordingly.
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/DatabaseExplorerPane/DatabaseExplorerPane.tsx-70-76 (1)

70-76: ⚠️ Potential issue | 🟡 Minor

Reset the pane auto-title when the selected connection disappears.

The !currentConnection branch returns early, so clearing/removing the connection leaves the old DB: … title behind. Please clear the auto title in that branch using whatever “no auto title” value setPaneAutoTitle expects.

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

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/DatabaseExplorerPane/DatabaseExplorerPane.tsx`
around lines 70 - 76, The early-return branch in the useEffect (watching
currentConnection, paneId, setPaneAutoTitle) leaves the previous "DB: …" title
intact; update that !currentConnection branch to clear the auto-title by calling
setPaneAutoTitle(paneId, <the component's "no auto title" sentinel>) so the pane
title is removed when the connection disappears (use the project's expected
sentinel such as undefined/null/empty string as appropriate).
apps/desktop/src/renderer/stores/browser-bookmarks.ts-110-135 (1)

110-135: ⚠️ Potential issue | 🟡 Minor

Clear stale favicons when a bookmark points to a new URL.

updateBookmark always carries forward the previous faviconUrl when the caller doesn't supply one. If a user edits a bookmark from one site to another, the bar can keep showing the old site's icon.

💡 Suggested change
 					const updatedBookmark: BrowserBookmark = {
 						...targetBookmark,
 						url: normalizedUrl,
 						title: bookmark.title.trim() || normalizedUrl,
-						faviconUrl: bookmark.faviconUrl ?? targetBookmark.faviconUrl,
+						faviconUrl:
+							bookmark.faviconUrl ??
+							(normalizeBookmarkUrl(targetBookmark.url) === normalizedUrl
+								? targetBookmark.faviconUrl
+								: undefined),
 					};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/stores/browser-bookmarks.ts` around lines 110 -
135, The updateBookmark handler (updateBookmark) currently always preserves
targetBookmark.faviconUrl via "faviconUrl: bookmark.faviconUrl ??
targetBookmark.faviconUrl", which leaves a stale icon when the URL was changed
but no new favicon was provided. Change the favicon assignment so that if
bookmark.faviconUrl is provided use it; otherwise if
normalizeBookmarkUrl(targetBookmark.url) !== normalizedUrl then clear the
favicon (set faviconUrl to undefined/null) else keep targetBookmark.faviconUrl.
Use the existing symbols normalizeBookmarkUrl, targetBookmark, normalizedUrl and
bookmark.faviconUrl to implement this conditional.
apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ProblemsView/ProblemsView.tsx-462-467 (1)

462-467: ⚠️ Potential issue | 🟡 Minor

Replace array index with stable diagnostic fields in list key.

The current key includes ${index}, which shifts when diagnostics reorder after filtering or refreshing, breaking React's component state tracking. Use only the stable diagnostic fields to build the key.

♻️ Suggested change
- key={`${groupKey}:${problem.code ?? "no-code"}:${problem.line ?? 0}:${index}`}
+ key={[
+   groupKey,
+   problem.providerId,
+   problem.source,
+   problem.code ?? "no-code",
+   problem.line ?? 0,
+   problem.column ?? 0,
+   problem.endLine ?? 0,
+   problem.endColumn ?? 0,
+   problem.message,
+ ].join(":")}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ProblemsView/ProblemsView.tsx`
around lines 462 - 467, The list key currently uses the array index in
ProblemsView's problems.map (see the ContextMenu key template
`${groupKey}:${problem.code ?? "no-code"}:${problem.line ?? 0}:${index}`), which
causes instability when diagnostics reorder; remove the index and build the key
from only stable diagnostic fields such as problem.id (or fallback to
`${problem.relativePath ?? "no-path"}:${problem.line ?? 0}:${problem.character
?? 0}:${problem.code ?? "no-code"}` if id is unavailable), and update the
ContextMenu key to use that stable composite string so React can reliably track
items.
apps/desktop/src/main/lib/language-services/providers/json/JsonLanguageProvider.ts-67-80 (1)

67-80: ⚠️ Potential issue | 🟡 Minor

Remote schema fetching lacks timeout and error handling.

The schemaRequestService uses fetch without a timeout, which could hang indefinitely if a schema server is unresponsive. Consider adding an AbortController with a reasonable timeout.

🛡️ Proposed fix with timeout
 schemaRequestService: async (uri) => {
   if (uri.startsWith("file://")) {
     return await fs.readFile(new URL(uri), "utf8");
   }

-  const response = await fetch(uri);
+  const controller = new AbortController();
+  const timeoutId = setTimeout(() => controller.abort(), 10_000);
+  try {
+    const response = await fetch(uri, { signal: controller.signal });
+    if (!response.ok) {
+      throw new Error(`Failed to load schema: ${uri} (${response.status})`);
+    }
+    return await response.text();
+  } finally {
+    clearTimeout(timeoutId);
+  }
-  if (!response.ok) {
-    throw new Error(`Failed to load schema: ${uri} (${response.status})`);
-  }
-
-  return await response.text();
 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/main/lib/language-services/providers/json/JsonLanguageProvider.ts`
around lines 67 - 80, The schemaRequestService inside the jsonService uses fetch
without a timeout and no abort handling; update schemaRequestService to create
an AbortController, start a short timeout (e.g. 3–10s) that calls
controller.abort(), pass controller.signal into fetch(uri, { signal }), and
clear the timeout on success; wrap the fetch and response handling in try/catch
to translate fetch/abort errors into a clear Error (including uri and status or
abort reason), while leaving the file:// branch using fs.readFile unchanged;
reference jsonService and schemaRequestService when making these changes.
apps/desktop/src/lib/trpc/routers/diagnostics/index.ts-319-319 (1)

319-319: ⚠️ Potential issue | 🟡 Minor

Synchronous TypeScript compilation may block the event loop.

The getTypeScriptProblems procedure uses .query() and performs synchronous ts.createProgram calls. For large workspaces with multiple tsconfig.json files, this can block the main thread significantly.

Consider using .mutation() (which better signals a heavy operation) or offloading to a worker thread for large projects.

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

In `@apps/desktop/src/lib/trpc/routers/diagnostics/index.ts` at line 319, The
getTypeScriptProblems procedure currently defined with .query() runs synchronous
ts.createProgram calls which can block the event loop for large workspaces;
change the router entry for getTypeScriptProblems to use .mutation() instead of
.query() to signal a heavy operation and/or move the ts.createProgram work into
a worker thread (using worker_threads or a child process) so the synchronous
compilation does not run on the main thread; update the procedure implementation
(getTypeScriptProblems) to dispatch the heavy compilation task to the worker and
return results asynchronously once the worker completes.
apps/desktop/src/main/lib/language-services/providers/typescript/TypeScriptLanguageProvider.ts-687-712 (1)

687-712: ⚠️ Potential issue | 🟡 Minor

sendRequest has no timeout for unresponsive tsserver.

If tsserver becomes unresponsive (infinite loop, deadlock), the Promise will never resolve. Consider adding a timeout that rejects the Promise after a reasonable duration (e.g., 30 seconds).

🛡️ Proposed fix with timeout
+const REQUEST_TIMEOUT_MS = 30_000;
+
 private async sendRequest(
   session: WorkspaceSession,
   command: string,
   args?: unknown,
 ): Promise<TsServerResponse> {
   const seq = ++session.seq;
   const payload: TsServerRequest = {
     seq,
     type: "request",
     command,
     arguments: args,
   };
   const content = `${JSON.stringify(payload)}\n`;

   return await new Promise<TsServerResponse>((resolve, reject) => {
+    const timeoutId = setTimeout(() => {
+      session.requestResolvers.delete(seq);
+      reject(new Error(`tsserver request timed out: ${command}`));
+    }, REQUEST_TIMEOUT_MS);
+
-    session.requestResolvers.set(seq, { resolve, reject });
+    session.requestResolvers.set(seq, {
+      resolve: (value) => {
+        clearTimeout(timeoutId);
+        resolve(value);
+      },
+      reject: (error) => {
+        clearTimeout(timeoutId);
+        reject(error);
+      },
+    });
     session.process.stdin.write(content, "utf8", (error) => {
       if (!error) {
         return;
       }

+      clearTimeout(timeoutId);
       session.requestResolvers.delete(seq);
       reject(error);
     });
   });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/main/lib/language-services/providers/typescript/TypeScriptLanguageProvider.ts`
around lines 687 - 712, The sendRequest Promise can hang if tsserver never
responds; add a timeout (e.g., 30s) inside sendRequest that rejects the Promise
and cleans up the resolver if no response arrives. Specifically, when creating
the Promise in sendRequest (for WorkspaceSession), start a setTimeout that calls
session.requestResolvers.delete(seq) and rejects with a TimeoutError after the
duration; ensure you clear that timer when the resolver is resolved or rejected
(both in the stdin.write callback error branch and where the corresponding
resolver.resolve is invoked elsewhere). Use the existing seq,
session.requestResolvers and session.process.stdin.write locations to wire up
the timer and cleanup so unresolved requests cannot leak.
apps/desktop/src/main/lib/language-services/providers/dart/DartLanguageProvider.ts-304-311 (1)

304-311: ⚠️ Potential issue | 🟡 Minor

Don't fall back to workspaceErrors for an active session.

After a failed dart/reanalyze, the map keeps the old message. Lines 320-321 then null-coalesce into that stale entry even after a later success sets session.lastError = null, so the provider stays stuck in error until the workspace is recreated.

Patch sketch
 		try {
 			await session.client.request("dart/reanalyze");
 			session.lastError = null;
+			this.workspaceErrors.delete(args.workspaceId);
 		} catch (error) {
 			session.lastError =
 				error instanceof Error ? error.message : String(error);
 			this.workspaceErrors.set(args.workspaceId, session.lastError);
 		}
@@
-		const lastError =
-			session?.lastError ?? this.workspaceErrors.get(args.workspaceId) ?? null;
+		const lastError = session
+			? session.lastError
+			: this.workspaceErrors.get(args.workspaceId) ?? null;

Also applies to: 319-321

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

In
`@apps/desktop/src/main/lib/language-services/providers/dart/DartLanguageProvider.ts`
around lines 304 - 311, The provider falls back to this.workspaceErrors for an
active session, which leaves stale errors in the map and causes the provider to
stay in error; update the dart/reanalyze success path (where
session.client.request("dart/reanalyze") sets session.lastError = null) to also
remove any stale entry by calling this.workspaceErrors.delete(args.workspaceId),
and ensure the catch still sets both session.lastError and
this.workspaceErrors.set(args.workspaceId, ...). Also change any lookup logic
that computes the current error for an active session to prefer
session.lastError exclusively (do not null-coalesce into this.workspaceErrors
when session exists).
🧹 Nitpick comments (11)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/BookmarkBar.tsx (1)

43-46: Skip no-op bookmark moves when dropped on the same item.

At Line 45, you can avoid unnecessary store updates/rerenders by returning early when active.id === over.id.

♻️ Proposed tweak
 const handleDragEnd = ({ active, over }: DragEndEvent) => {
 	if (!over) return;
+	if (active.id === over.id) return;
 	moveBookmark(String(active.id), String(over.id));
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/BookmarkBar.tsx`
around lines 43 - 46, The drag-end handler handleDragEnd currently always calls
moveBookmark even when the item is dropped on itself; update handleDragEnd to
return early when there's no meaningful move by checking that over exists and
that the active and over ids are not equal (e.g., compare String(active.id) !==
String(over.id)) before calling moveBookmark, so moveBookmark is only invoked
for real reorders.
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/FileViewerContent.tsx (1)

209-222: Move diagnostics subscription out of per-file viewer instances.

At Line 209, each mounted FileViewerContent creates its own diagnostics subscription and invalidates the same workspace query. With many open panes, this multiplies invalidations and refetch churn for each diagnostics event.

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

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/FileViewerContent.tsx`
around lines 209 - 222, FileViewerContent is creating a per-instance diagnostics
subscription using
electronTrpc.languageServices.subscribeDiagnostics.useSubscription which causes
duplicate invalidations of
trpcUtils.languageServices.getWorkspaceDiagnostics.invalidate for the same
workspaceId; lift this subscription out of FileViewerContent into a single
higher-level owner (e.g., WorkspaceView or TabsContent) so only one subscription
exists per workspaceId, enable it only when workspaceId is present, and have
that single subscriber call
trpcUtils.languageServices.getWorkspaceDiagnostics.invalidate({ workspaceId })
on data events; remove the subscription code from FileViewerContent and instead
either provide the subscription via context or manage it in the parent component
that already knows workspaceId.
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/CodeMirrorDiffViewer/CodeMirrorDiffViewer.tsx (1)

139-198: Extract shared diagnostic helpers to reduce duplication.

createDiagnosticsTheme and buildDiagnosticDecorations are duplicated verbatim from CodeEditor.tsx. Consider extracting these to a shared utility module (e.g., utils/codemirror-diagnostics.ts) to maintain DRY principles and ensure consistent behavior across both editors.

Example extraction

Create a new file:

// utils/codemirror-diagnostics.ts
import { EditorState } from "@codemirror/state";
import { Decoration, EditorView } from "@codemirror/view";
import type { getEditorTheme } from "shared/themes";

export type DiagnosticEntry = {
  line: number | null;
  column: number | null;
  endLine: number | null;
  endColumn: number | null;
  severity: "error" | "warning" | "info" | "hint";
};

export function createDiagnosticsTheme(theme: ReturnType<typeof getEditorTheme>) {
  // ... existing implementation
}

export function buildDiagnosticDecorations(
  doc: EditorState["doc"],
  diagnostics: DiagnosticEntry[],
) {
  // ... existing implementation
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/CodeMirrorDiffViewer/CodeMirrorDiffViewer.tsx`
around lines 139 - 198, The two functions createDiagnosticsTheme and
buildDiagnosticDecorations in CodeMirrorDiffViewer.tsx are duplicated from
CodeEditor.tsx; extract them into a shared module (e.g.,
utils/codemirror-diagnostics.ts) that exports the functions and a
DiagnosticEntry type, update both CodeEditor.tsx and CodeMirrorDiffViewer.tsx to
import createDiagnosticsTheme and buildDiagnosticDecorations from that new
module, and ensure imports for EditorState, Decoration, EditorView and
getEditorTheme are adjusted in the shared file so both consumers compile
unchanged.
apps/desktop/src/renderer/providers/LanguageServicesProvider/LanguageServicesProvider.tsx (1)

174-219: Consider adding error handling for document lifecycle mutations.

The openDocument, changeDocument, and closeDocument mutations are fire-and-forget (void prefix). If these fail, diagnostics won't work for those documents, but users won't receive any feedback. Consider adding .catch() handlers to log errors, at minimum for debugging purposes.

Example error logging
 if (!prev) {
-	void electronTrpcClient.languageServices.openDocument.mutate({
+	electronTrpcClient.languageServices.openDocument.mutate({
 		workspaceId: tracked.workspaceId,
 		absolutePath: tracked.absolutePath,
 		languageId: tracked.languageId,
 		content: tracked.content,
 		version: tracked.version,
-	});
+	}).catch((err) => {
+		console.error("[LanguageServicesProvider] Failed to open document:", tracked.absolutePath, err);
+	});
 	continue;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/providers/LanguageServicesProvider/LanguageServicesProvider.tsx`
around lines 174 - 219, The document lifecycle mutations
(electronTrpcClient.languageServices.openDocument.mutate, changeDocument.mutate,
closeDocument.mutate) are currently invoked fire-and-forget and lack error
handling; update the logic in the useEffect that iterates over
trackedDocuments/previousRef to attach .catch handlers (or await in an async
wrapper) to each mutate call and log failures with contextual info (documentKey,
workspaceId, absolutePath, operation name) so errors are observable for
debugging and diagnostics.
apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/DiagnosticsSettings/DiagnosticsSettings.tsx (1)

55-58: Unknown providers should not render as live toggles.

For unknown providerIds, checked falls back to provider.enabled, but onCheckedChange returns immediately, so the control looks interactive and then snaps back on the next render. Either disable unsupported rows explicitly or key the preference state by providerId so new providers do not require touching this component.

Also applies to: 76-83

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

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/DiagnosticsSettings/DiagnosticsSettings.tsx`
around lines 55 - 58, The toggle appears interactive for unknown providers
because checked falls back to provider.enabled while onCheckedChange is a no-op;
change behavior so unknown providers render disabled and derive checked only
from the preferences map. Specifically, in DiagnosticsSettings use
isKnownProviderId(...) to set disabled={true} when false, compute checked as
!!enabledProviders[providerId] (not provider.enabled) and keep onCheckedChange
early-return, and apply the same change to the other toggle block (lines
handling the other provider rendering around where onCheckedChange is defined)
so unknown provider rows look and behave non-interactive.
apps/desktop/src/main/lib/language-services/manager.ts (2)

183-183: Consider adding a dispose method for application shutdown.

The manager singleton maintains stateful providers with external processes (tsserver). For clean application shutdown, a dispose() method that calls disposeWorkspace for all known workspaces would ensure proper cleanup.

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

In `@apps/desktop/src/main/lib/language-services/manager.ts` at line 183, Add a
dispose method to LanguageServiceManager that cleanly shuts down all workspace
resources: implement LanguageServiceManager.dispose() to iterate over the
manager's tracked workspaces (the internal collection used by methods like
disposeWorkspace and getWorkspace/getOrCreateWorkspace) and call
disposeWorkspace for each, then clear the collection; expose this on the
exported singleton languageServiceManager so the application shutdown path can
call languageServiceManager.dispose() to terminate tsserver processes and other
external resources.

27-35: Method name mismatch: syncDocument calls changeDocument.

The syncDocument method internally calls provider.changeDocument. This semantic mismatch may confuse consumers. If "sync" implies creating-if-not-exists behavior, consider having the provider's changeDocument check if the document is open first (which TypeScriptLanguageProvider.changeDocument already does at line 263-266).

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

In `@apps/desktop/src/main/lib/language-services/manager.ts` around lines 27 - 35,
The syncDocument method calls provider.changeDocument which is a semantic
mismatch; update syncDocument to either call a provider API named
syncDocument/openDocument (if providers implement that) or ensure
provider.changeDocument performs create-if-not-exists behavior like
TypeScriptLanguageProvider.changeDocument (lines referencing
TypeScriptLanguageProvider.changeDocument). Concretely: modify syncDocument to
invoke the provider method that expresses "sync/create-if-missing" (e.g.,
provider.syncDocument or provider.openDocument) or add the existence check and
creation logic before calling provider.changeDocument so providers receive
consistent semantics.
apps/desktop/src/lib/trpc/routers/diagnostics/index.ts (1)

91-105: Duplicate utility: normalizeRelativePath already exists in utils.ts.

This function duplicates toRelativeWorkspacePath from apps/desktop/src/main/lib/language-services/utils.ts. Consider importing and reusing the shared utility to maintain DRY.

♻️ Suggested refactor
+import { toRelativeWorkspacePath } from "@/main/lib/language-services/utils";
 // ...
-function normalizeRelativePath(
-	workspacePath: string,
-	fileName: string,
-): string | null {
-	const relativePath = path.relative(workspacePath, fileName);
-	if (
-		!relativePath ||
-		relativePath.startsWith("..") ||
-		path.isAbsolute(relativePath)
-	) {
-		return null;
-	}
-
-	return relativePath.split(path.sep).join("/");
-}
+// Use toRelativeWorkspacePath from utils instead
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/diagnostics/index.ts` around lines 91 -
105, The function normalizeRelativePath duplicates existing utility
toRelativeWorkspacePath; remove the local normalizeRelativePath implementation
and import toRelativeWorkspacePath from the shared utils module, then replace
uses of normalizeRelativePath with toRelativeWorkspacePath (ensure argument
order/workspace semantics match); keep return types and error/null behavior
consistent and update any call sites in this file to handle null/undefined the
same way.
apps/desktop/src/main/lib/language-services/providers/toml/TomlLanguageProvider.ts (1)

232-265: All TOML diagnostics reported as "error" severity.

The mapDiagnostic function hardcodes severity: "error" (line 262). If Taplo's LintError includes severity information in the future, this won't adapt. Consider checking if the error object has severity metadata.

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

In
`@apps/desktop/src/main/lib/language-services/providers/toml/TomlLanguageProvider.ts`
around lines 232 - 265, The mapDiagnostic function currently hardcodes severity:
"error" causing all Taplo LintError entries to be reported as errors; update
mapDiagnostic (in TomlLanguageProvider) to inspect the incoming LintError for
any severity/level field (e.g., error.severity, error.level or similar) and map
it to the LanguageServiceDiagnostic severity (e.g., "error" | "warning" |
"info"); default to "error" if no metadata exists and ensure the returned object
uses that mapped value instead of the hardcoded string.
apps/desktop/src/main/lib/language-services/providers/typescript/TypeScriptLanguageProvider.ts (1)

158-172: Duplicate utility: toRelativeWorkspacePath already exists in utils.ts.

This function duplicates the implementation from apps/desktop/src/main/lib/language-services/utils.ts. Import the shared utility instead.

♻️ Suggested refactor
+import { toRelativeWorkspacePath } from "../../utils";
 // ...
-function toRelativeWorkspacePath(
-	workspacePath: string,
-	absolutePath: string,
-): string | null {
-	const relativePath = path.relative(workspacePath, absolutePath);
-	// ...
-}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/main/lib/language-services/providers/typescript/TypeScriptLanguageProvider.ts`
around lines 158 - 172, The local toRelativeWorkspacePath implementation in
TypeScriptLanguageProvider.ts duplicates the one in language-services/utils.ts;
remove the local function and import the shared utility instead by adding an
import for toRelativeWorkspacePath from the utils module and updating any local
references to use the imported symbol (keep existing call sites in
TypeScriptLanguageProvider like any uses inside methods such as resolvePath or
where toRelativeWorkspacePath is referenced). Ensure the import path points to
apps/desktop/src/main/lib/language-services/utils (or the module's relative
path) and delete the duplicate function block to avoid shadowing.
apps/desktop/src/main/lib/language-services/utils.ts (1)

52-70: Potential off-by-one in CRLF handling.

When encountering \r\n, incrementing index inside the loop (line 62) causes the for loop's index += 1 to skip the character after \n. This is intentional to skip the \n that's part of CRLF, but the column calculation after a \r (not followed by \n) resets to 1 correctly.

However, if the offset falls exactly on the \n of a \r\n pair, the position calculation may be slightly off. Consider adding a comment clarifying this edge case behavior.

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

In `@apps/desktop/src/main/lib/language-services/utils.ts` around lines 52 - 70,
The CRLF branch inside the loop (variables boundedOffset, content, index, line,
column) increments index to skip the '\n' of a '\r\n' pair which can cause an
off-by-one when the target offset equals that '\n'; update the code by adding a
clear comment above the '\r' handling that states: we intentionally increment
index to consume the '\n' of a CRLF pair so the '\n' is not processed
separately, and note the edge case where an offset landing exactly on the '\n'
will be treated as part of the preceding CRLF (explain expected behavior), and
optionally add a test or adjust logic to explicitly handle offset === index+1 if
you want a different semantics.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cdbd0a13-fb60-43ae-840e-bae8e0540360

📥 Commits

Reviewing files that changed from the base of the PR and between 3a9bc6e and c38680e.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (84)
  • apps/desktop/docs/LANGUAGE_SERVICES.md
  • apps/desktop/package.json
  • apps/desktop/src/lib/trpc/routers/databases/index.ts
  • apps/desktop/src/lib/trpc/routers/diagnostics/index.ts
  • apps/desktop/src/lib/trpc/routers/filesystem/index.ts
  • apps/desktop/src/lib/trpc/routers/index.ts
  • apps/desktop/src/lib/trpc/routers/language-services/index.ts
  • apps/desktop/src/lib/trpc/routers/ui-state/index.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/types.ts
  • apps/desktop/src/main/lib/language-services/diagnostics-store.ts
  • apps/desktop/src/main/lib/language-services/lsp/StdioJsonRpcClient.ts
  • apps/desktop/src/main/lib/language-services/manager.ts
  • apps/desktop/src/main/lib/language-services/providers/dart/DartLanguageProvider.ts
  • apps/desktop/src/main/lib/language-services/providers/json/JsonLanguageProvider.ts
  • apps/desktop/src/main/lib/language-services/providers/toml/TomlLanguageProvider.ts
  • apps/desktop/src/main/lib/language-services/providers/typescript/TypeScriptLanguageProvider.ts
  • apps/desktop/src/main/lib/language-services/types.ts
  • apps/desktop/src/main/lib/language-services/utils.ts
  • apps/desktop/src/renderer/components/MarkdownRenderer/MarkdownRenderer.tsx
  • apps/desktop/src/renderer/providers/LanguageServicesProvider/LanguageServicesProvider.tsx
  • apps/desktop/src/renderer/providers/LanguageServicesProvider/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/ResourceConsumption/ResourceConsumption.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/layout.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/BehaviorSettings/BehaviorSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/DiagnosticsSettings/DiagnosticsSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/DiagnosticsSettings/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/BrowserPane.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/BookmarkBar.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/components/BookmarkBarItem/BookmarkBarItem.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/components/BookmarkBarItem/components/EditBookmarkDialog/EditBookmarkDialog.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/components/BookmarkBarItem/components/EditBookmarkDialog/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/components/BookmarkBarItem/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BrowserToolbar/BrowserToolbar.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/hooks/usePersistentWebview/usePersistentWebview.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/DatabaseExplorerPane/DatabaseExplorerPane.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/DatabaseExplorerPane/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/CodeMirrorDiffViewer/CodeMirrorDiffViewer.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/FileViewerContent.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/ChangesView.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/RepositoryPanel/RepositoryPanel.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/RepositoryPanel/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/ReviewPanel/ReviewPanel.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/DatabasesView/DatabasesView.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/DatabasesView/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ProblemsView/ProblemsView.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ProblemsView/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/SearchView/SearchView.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/SearchView/components/SearchFileGroup/SearchFileGroup.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/SearchView/components/SearchFileGroup/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/SearchView/components/SearchMatchItem/SearchMatchItem.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/SearchView/components/SearchMatchItem/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/SearchView/components/SearchToolbar/SearchToolbar.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/SearchView/components/SearchToolbar/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/SearchView/hooks/useContentSearch/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/SearchView/hooks/useContentSearch/useContentSearch.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/SearchView/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/SearchView/types.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/SearchView/utils/searchPattern/searchPattern.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/CodeEditor.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/loadLanguageSupport.ts
  • apps/desktop/src/renderer/stores/browser-bookmarks.ts
  • apps/desktop/src/renderer/stores/database-sidebar.ts
  • apps/desktop/src/renderer/stores/language-service-preferences.ts
  • apps/desktop/src/renderer/stores/sidebar-state.ts
  • apps/desktop/src/renderer/stores/tabs/store.ts
  • apps/desktop/src/renderer/stores/tabs/types.ts
  • apps/desktop/src/renderer/stores/tabs/utils.ts
  • apps/desktop/src/shared/detect-language.ts
  • apps/desktop/src/shared/hotkeys.ts
  • apps/desktop/src/shared/tabs-types.ts
  • bunfig.toml
  • packages/host-service/src/trpc/router/filesystem/filesystem.ts
  • packages/workspace-fs/src/client/index.ts
  • packages/workspace-fs/src/core/service.ts
  • packages/workspace-fs/src/host/service.ts
  • packages/workspace-fs/src/search.ts
  • packages/workspace-fs/src/types.ts

Comment thread apps/desktop/src/lib/trpc/routers/databases/index.ts
Comment on lines +317 to +330
SELECT
t.table_schema,
t.table_name,
t.table_type,
c.column_name,
c.data_type,
c.is_nullable,
c.ordinal_position
FROM information_schema.tables t
JOIN information_schema.columns c
ON t.table_schema = c.table_schema
AND t.table_name = c.table_name
WHERE t.table_schema NOT IN ('pg_catalog', 'information_schema')
ORDER BY t.table_schema, t.table_name, c.ordinal_position
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

wc -l apps/desktop/src/lib/trpc/routers/databases/index.ts

Repository: MocA-Love/superset

Length of output: 118


🏁 Script executed:

cat -n apps/desktop/src/lib/trpc/routers/databases/index.ts | sed -n '310,340p'

Repository: MocA-Love/superset

Length of output: 1100


🏁 Script executed:

cat -n apps/desktop/src/lib/trpc/routers/databases/index.ts | sed -n '535,550p'

Repository: MocA-Love/superset

Length of output: 719


🏁 Script executed:

cat -n apps/desktop/src/lib/trpc/routers/databases/index.ts | sed -n '575,590p'

Repository: MocA-Love/superset

Length of output: 824


🏁 Script executed:

rg "ctid" apps/desktop/src/lib/trpc/routers/databases/index.ts -n

Repository: MocA-Love/superset

Length of output: 468


🏁 Script executed:

cat -n apps/desktop/src/lib/trpc/routers/databases/index.ts | sed -n '295,335p'

Repository: MocA-Love/superset

Length of output: 1386


🏁 Script executed:

cat -n apps/desktop/src/lib/trpc/routers/databases/index.ts | sed -n '520,560p'

Repository: MocA-Love/superset

Length of output: 1777


🏁 Script executed:

rg "inspectPostgres|previewData|getRow" apps/desktop/src/lib/trpc/routers/databases/index.ts -B 2 -A 1 | head -80

Repository: MocA-Love/superset

Length of output: 115


🏁 Script executed:

cat -n apps/desktop/src/lib/trpc/routers/databases/index.ts | sed -n '1,50p'

Repository: MocA-Love/superset

Length of output: 1706


🏁 Script executed:

cat -n apps/desktop/src/lib/trpc/routers/databases/index.ts | sed -n '500,520p'

Repository: MocA-Love/superset

Length of output: 832


🏁 Script executed:

cat -n apps/desktop/src/lib/trpc/routers/databases/index.ts | sed -n '565,585p'

Repository: MocA-Love/superset

Length of output: 958


🏁 Script executed:

rg "previewPostgres|getPostgresRow" apps/desktop/src/lib/trpc/routers/databases/index.ts -B 5 | head -50

Repository: MocA-Love/superset

Length of output: 223


🏁 Script executed:

cat -n apps/desktop/src/lib/trpc/routers/databases/index.ts | sed -n '496,510p'

Repository: MocA-Love/superset

Length of output: 548


🏁 Script executed:

rg "table_type.*VIEW|table_type.*TABLE" apps/desktop/src/lib/trpc/routers/databases/index.ts

Repository: MocA-Love/superset

Length of output: 44


🌐 Web query:

PostgreSQL ctid column views system columns

💡 Result:

The ctid is a system column in PostgreSQL present in every table, representing the physical location of a row version as a tuple identifier (tid) of type tid, formatted as (block_number, tuple_offset). It changes on updates or VACUUM FULL and should not be used as a permanent row identifier. System columns like ctid are implicitly defined for tables but are not part of user-defined column lists. In information_schema.columns (which covers both tables and views), system columns such as ctid are explicitly excluded. For views, which have no physical storage or rows of their own, system columns do not exist. When querying a view, SELECT * returns only the columns defined in the view's query (user columns from underlying tables). The ctid column from underlying base tables can be accessed explicitly if the view's SELECT includes it (e.g., CREATE VIEW v AS SELECT ctid, * FROM t;), but it is not automatically included and is not a property of the view itself. In view update operations (UPDATE/DELETE on updatable views), PostgreSQL internally uses ctid from underlying tables or wholerow references, but this is not exposed in standard SELECT output from the view. Examples: - On a table: SELECT ctid, * FROM my_table; shows ctid. - On a standard view without ctid in definition: SELECT * FROM my_view; does not show ctid. - To include: CREATE VIEW my_view AS SELECT ctid, col1 FROM my_table;

Citations:


🏁 Script executed:

cat -n apps/desktop/src/lib/trpc/routers/databases/index.ts | sed -n '330,380p'

Repository: MocA-Love/superset

Length of output: 1646


🏁 Script executed:

grep -n "table_type\|table_schema\|TABLE\|VIEW" apps/desktop/src/lib/trpc/routers/databases/index.ts | head -30

Repository: MocA-Love/superset

Length of output: 1279


🏁 Script executed:

cat -n apps/desktop/src/lib/trpc/routers/databases/index.ts | sed -n '395,415p'

Repository: MocA-Love/superset

Length of output: 808


🏁 Script executed:

rg "inspectPostgres" apps/desktop/src -l

Repository: MocA-Love/superset

Length of output: 223


🏁 Script executed:

cat -n apps/desktop/src/lib/trpc/routers/databases/index.ts | sed -n '390,410p'

Repository: MocA-Love/superset

Length of output: 709


🏁 Script executed:

cat -n apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/DatabasesView/DatabasesView.tsx | head -100

Repository: MocA-Love/superset

Length of output: 3029


🏁 Script executed:

rg "inspectPostgres|previewPostgres|getPostgresRow" apps/desktop/src/renderer -A 3 -B 3 | head -80

Repository: MocA-Love/superset

Length of output: 5074


🏁 Script executed:

rg "type.*view|type.*table" apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/DatabasesView/DatabasesView.tsx -i | head -20

Repository: MocA-Love/superset

Length of output: 161


🏁 Script executed:

cat -n apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/DatabasesView/DatabasesView.tsx | grep -A 10 "selectedTable"

Repository: MocA-Love/superset

Length of output: 14195


🏁 Script executed:

rg "WHERE type = 'table'" apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/DatabasesView/DatabasesView.tsx -B 5 -A 5

Repository: MocA-Love/superset

Length of output: 306


🏁 Script executed:

rg "type.*==.*view|type.*===.*view|\.filter.*type" apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/DatabasesView/DatabasesView.tsx -i -A 2 -B 2

Repository: MocA-Love/superset

Length of output: 44


🏁 Script executed:

cat -n apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/DatabasesView/DatabasesView.tsx | sed -n '2300,2430p'

Repository: MocA-Love/superset

Length of output: 6086


🏁 Script executed:

cat -n apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/DatabasesView/DatabasesView.tsx | sed -n '1050,1080p'

Repository: MocA-Love/superset

Length of output: 1290


🏁 Script executed:

rg "selectedTable\.type|table\.type" apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/DatabasesView/DatabasesView.tsx

Repository: MocA-Love/superset

Length of output: 44


🏁 Script executed:

rg "activeTables|activeSchemaQuery" apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/DatabasesView/DatabasesView.tsx -B 3 -A 3 | head -50

Repository: MocA-Love/superset

Length of output: 1328


🏁 Script executed:

cat -n apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/DatabasesView/DatabasesView.tsx | sed -n '850,900p'

Repository: MocA-Love/superset

Length of output: 1967


Fix the inspection query to exclude views, or handle them separately in preview/detail paths.

The inspectPostgres query returns both tables and views from information_schema.tables, but previewPostgresTable and getPostgresRowDetail require ctid—a system column that exists only on tables, not views. When users select a view from the explorer, preview and detail operations will fail with an error about missing ctid. Either filter the inspection query to include only tables (WHERE t.table_type = 'TABLE'), or modify the preview/detail paths to handle views separately.

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

In `@apps/desktop/src/lib/trpc/routers/databases/index.ts` around lines 317 - 330,
The inspection query currently returns both tables and views causing
previewPostgresTable and getPostgresRowDetail to fail (ctid only exists on real
tables); update the inspectPostgres query to only return real tables by adding a
filter on t.table_type (e.g., WHERE t.table_type = 'BASE TABLE' in addition to
the existing NOT IN clause), or alternatively update previewPostgresTable and
getPostgresRowDetail to detect views (via table_type) and handle them separately
(skip ctid-dependent logic or fetch rows via a view-safe primary-key-based
path).

Comment thread apps/desktop/src/lib/trpc/routers/databases/index.ts
Comment thread apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts Outdated
Comment on lines +204 to +230
function workflowSupportsDispatch({
repoPath,
workflowPath,
}: {
repoPath: string;
workflowPath?: string;
}): boolean {
if (!workflowPath) {
return false;
}

const absolutePath = path.join(repoPath, workflowPath);
if (!existsSync(absolutePath)) {
return false;
}

try {
const content = readFileSync(absolutePath, "utf8");
return (
/^\s*workflow_dispatch\s*:/m.test(content) ||
/^\s*on\s*:\s*workflow_dispatch\s*$/m.test(content) ||
/^\s*on\s*:\s*\[[^\]]*\bworkflow_dispatch\b[^\]]*\]/m.test(content)
);
} catch {
return false;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -t f "git-status.ts" --path "apps/desktop/**"

Repository: MocA-Love/superset

Length of output: 295


🏁 Script executed:

fd -t f "git-status.ts" apps/desktop/

Repository: MocA-Love/superset

Length of output: 131


🏁 Script executed:

wc -l apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts

Repository: MocA-Love/superset

Length of output: 136


🏁 Script executed:

cat -n apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts | sed -n '204,230p'

Repository: MocA-Love/superset

Length of output: 814


🏁 Script executed:

cat -n apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts | sed -n '500,520p'

Repository: MocA-Love/superset

Length of output: 818


🏁 Script executed:

cat -n apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts | sed -n '1,50p'

Repository: MocA-Love/superset

Length of output: 1856


🏁 Script executed:

cat -n apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts | sed -n '450,510p'

Repository: MocA-Love/superset

Length of output: 2475


🏁 Script executed:

cat -n apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts | sed -n '380,470p'

Repository: MocA-Love/superset

Length of output: 2821


🏁 Script executed:

cat -n apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts | sed -n '330,410p'

Repository: MocA-Love/superset

Length of output: 2647


🏁 Script executed:

cat -n apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts | sed -n '60,120p'

Repository: MocA-Love/superset

Length of output: 2093


🏁 Script executed:

cat -n apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts | sed -n '260,330p'

Repository: MocA-Love/superset

Length of output: 2448


🏁 Script executed:

cat -n apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts | sed -n '520,550p'

Repository: MocA-Love/superset

Length of output: 910


Dispatch support is being inferred from the local checkout, not the target repo/ref.

getGitHubRepositoryOverview() fetches the workflow list from GitHub API (line 446), but lines 509-512 filter these workflows by checking local file existence. If the worktree is on an older branch or lacks a workflow file, workflowSupportsDispatch() returns false and the workflow is hidden from the UI even though the repository on GitHub can still dispatch it.

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

In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts` around
lines 204 - 230, The code hides workflows by checking the local checkout via
workflowSupportsDispatch (which uses existsSync/readFileSync) instead of using
the workflow data fetched from GitHub (getGitHubRepositoryOverview); update the
logic to determine dispatch support from the remote workflow info or the
workflow file content returned by the GitHub API rather than the local
filesystem. Concretely: stop using existsSync/readFileSync in
workflowSupportsDispatch for filtering, either (a) change
workflowSupportsDispatch to accept the workflow file content (string) or a
boolean flag from the API and detect workflow_dispatch from that content, or (b)
replace the local-file check where workflows are filtered (the code that calls
workflowSupportsDispatch) to consult the workflows returned by
getGitHubRepositoryOverview (or fetch the workflow content via the GitHub API)
and base the dispatch-supported filter on that remote data. Ensure references to
symbols: workflowSupportsDispatch and getGitHubRepositoryOverview are updated so
UI filtering uses remote/ API-derived information, not the local worktree.

Comment on lines +30 to +37
const trimmedQuery = query.trim();
const validationError = useMemo(
() => getSearchValidationError(trimmedQuery, isRegex),
[trimmedQuery, isRegex],
);
const debouncedQuery = useDebouncedValue(trimmedQuery, 150);
const isDebouncing =
trimmedQuery.length > 0 && trimmedQuery !== debouncedQuery;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Preserve whitespace in the search input.

query.trim() changes the term itself, so users cannot search for indentation, a single space, or literals/regexes with meaningful leading/trailing whitespace. Treat only "" as empty here; otherwise the search request and the UI state drift from user intent.

Suggested fix
-	const trimmedQuery = query.trim();
+	const rawQuery = query;
 	const validationError = useMemo(
-		() => getSearchValidationError(trimmedQuery, isRegex),
-		[trimmedQuery, isRegex],
+		() => getSearchValidationError(rawQuery, isRegex),
+		[rawQuery, isRegex],
 	);
-	const debouncedQuery = useDebouncedValue(trimmedQuery, 150);
-	const isDebouncing =
-		trimmedQuery.length > 0 && trimmedQuery !== debouncedQuery;
+	const debouncedQuery = useDebouncedValue(rawQuery, 150);
+	const isDebouncing = rawQuery.length > 0 && rawQuery !== debouncedQuery;-		hasQuery: trimmedQuery.length > 0,
+		hasQuery: rawQuery.length > 0,

Also applies to: 74-77

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

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/SearchView/hooks/useContentSearch/useContentSearch.ts`
around lines 30 - 37, Don't trim the user's search term; preserve
leading/trailing whitespace for validation, debouncing, and isDebouncing logic.
Replace trimmedQuery with the original query string for calls to
getSearchValidationError, useDebouncedValue, and the isDebouncing calculation
(e.g., remove the const trimmedQuery = query.trim() and change validationError,
debouncedQuery, and isDebouncing to use query), but still treat only query ===
"" as empty when gating empty-state behavior; also apply the same change to the
other occurrence around the symbols at lines 74-77 (the other use of
useDebouncedValue/getSearchValidationError/isDebouncing).

Comment on lines +166 to +183
const runReplace = useCallback(
async (paths?: string[]) => {
if (!workspaceId || !query.trim() || validationError) {
return;
}

try {
const result = await replaceMutation.mutateAsync({
workspaceId,
query,
replacement,
includeHidden: false,
includePattern,
excludePattern,
isRegex,
caseSensitive,
paths,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Trim the query before calling replaceContent.

This callback treats query.trim() as the canonical search text, but the mutation still receives raw query. A query like " foo " can therefore produce visible matches and then replace nothing.

💡 Suggested change
  async (paths?: string[]) => {
-   if (!workspaceId || !query.trim() || validationError) {
+   const normalizedQuery = query.trim();
+   if (!workspaceId || !normalizedQuery || validationError) {
      return;
    }

    try {
      const result = await replaceMutation.mutateAsync({
        workspaceId,
-       query,
+       query: normalizedQuery,
        replacement,
        includeHidden: false,
📝 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 runReplace = useCallback(
async (paths?: string[]) => {
if (!workspaceId || !query.trim() || validationError) {
return;
}
try {
const result = await replaceMutation.mutateAsync({
workspaceId,
query,
replacement,
includeHidden: false,
includePattern,
excludePattern,
isRegex,
caseSensitive,
paths,
});
const runReplace = useCallback(
async (paths?: string[]) => {
const normalizedQuery = query.trim();
if (!workspaceId || !normalizedQuery || validationError) {
return;
}
try {
const result = await replaceMutation.mutateAsync({
workspaceId,
query: normalizedQuery,
replacement,
includeHidden: false,
includePattern,
excludePattern,
isRegex,
caseSensitive,
paths,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/SearchView/SearchView.tsx`
around lines 166 - 183, The runReplace callback uses query.trim() for validation
but still passes the raw query to replaceMutation.mutateAsync; create a
trimmedQuery (e.g., const trimmedQuery = query.trim()) inside runReplace and
pass trimmedQuery (not query) into replaceMutation.mutateAsync (and any
downstream call sites like replaceContent if present) so replacements use the
canonical trimmed search text; keep the existing validation using trimmedQuery
to avoid double work.

Comment on lines +67 to +70
export const useDatabaseSidebarStore = create<DatabaseSidebarState>()(
devtools(
persist(
(set, get) => ({
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't persist raw database secrets in this store.

Because this store is wrapped in persist, every Postgres connectionString is written to disk in plaintext, and queryHistory.sql may retain sensitive literals too. That is a real credential/privacy leak for the new database explorer. Keep secrets in secure OS/Electron storage and persist only non-sensitive metadata or opaque references here.

Also applies to: 92-100, 163-183, 203-233

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

In `@apps/desktop/src/renderer/stores/database-sidebar.ts` around lines 67 - 70,
The store created by useDatabaseSidebarStore (type DatabaseSidebarState) is
wrapped in persist and currently persists sensitive fields like connectionString
and queryHistory.sql; change the state shape and persistence so that secrets are
never stored in this persisted slice: remove or replace connectionString and any
queryHistory.sql literals with non-sensitive metadata or opaque references/IDs,
and move actual secrets to secure OS/Electron storage (e.g.,
secureStore.get/set) accessed at runtime by functions in the store (e.g.,
loadConnection, getQueryHistory) so persisted data only contains non-sensitive
metadata/ids and not raw credentials or SQL literals.

Comment thread bunfig.toml
@@ -1,3 +1,3 @@
[install]
minimumReleaseAge = 604800
#minimumReleaseAge = 604800
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Re-enable minimumReleaseAge to avoid weakening supply-chain protection.

Commenting this out removes the 7-day quarantine safeguard for newly published packages. Keep this policy enabled and resolve blocked installs by selecting an older compliant version instead of disabling the guard.

Suggested fix
 [install]
-#minimumReleaseAge = 604800
+minimumReleaseAge = 604800
 linker = "isolated" # Prevent hoisting from resolving `path-scurry@2` to `lru-cache@6` (missing `LRUCache`), which breaks the `@superset/desktop` postinstall native rebuild.

Based on learnings: When a package install/update/add command fails due to age security policy (7-day minimum release age), choose an older version that satisfies the policy instead of disabling the policy or using bypass flags.

📝 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
#minimumReleaseAge = 604800
[install]
minimumReleaseAge = 604800
linker = "isolated" # Prevent hoisting from resolving `path-scurry@2` to `lru-cache@6` (missing `LRUCache`), which breaks the `@superset/desktop` postinstall native rebuild.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bunfig.toml` at line 2, Uncomment and re-enable the minimumReleaseAge setting
(restore minimumReleaseAge = 604800) in bunfig.toml so the 7-day quarantine
policy is enforced; do not remove or bypass this guard—if installs fail due to
the age policy, resolve by selecting an older package version that meets
minimumReleaseAge rather than disabling or commenting out the setting.

Comment on lines +354 to +375
function compileSearchPattern({
query,
isRegex = false,
caseSensitive,
}: Pick<
SearchContentOptions,
"query" | "isRegex" | "caseSensitive"
>): CompiledSearchPattern {
const resolvedCaseSensitive = resolveCaseSensitive(
query,
caseSensitive,
isRegex,
);
const flags = resolvedCaseSensitive ? "gu" : "giu";
const source = isRegex ? query : escapeRegExp(query);

return {
isRegex,
caseSensitive: resolvedCaseSensitive,
regex: new RegExp(source, flags),
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What are best practices for preventing ReDoS attacks when accepting user-provided regex patterns in Node.js?

💡 Result:

Best practices for preventing ReDoS attacks when accepting user-provided regex patterns in Node.js focus on avoiding direct execution of untrusted patterns with the native RegExp engine, which is vulnerable to catastrophic backtracking. 1. Do not execute user-provided regex patterns directly. Instead, reject them or use safe alternatives. Native Node.js RegExp can be exploited with evil regexes like /^([a-zA-Z]+)+$/ combined with crafted input, freezing the single-threaded event loop {1,2,3,6,10,13}. 2. Use the 're2' package, a drop-in replacement using Google's RE2 engine that guarantees linear-time matching and prevents backtracking-based ReDoS. Wrap user patterns in try-catch to fallback if unsupported features (backreferences, lookaheads) are present: const RE2 = require('re2'); try { const re = new RE2(userPattern); re.exec(input); } catch (e) { // Reject or sanitize } RE2 supports most RegExp syntax but rejects ReDoS-prone features {13,14,6}. 3. Validate/sanitize user patterns before use: - Check with 'safe-regex' (or forks like safe-regex2): detects nested quantifiers but has false positives/negatives. const safe = require('safe-regex'); if (!safe(userPattern)) reject; {1,18} - Restrict pattern complexity: limit length, forbid nested quantifiers (+*, ? inside repeats), backrefs. 4. Always limit input string length (e.g., express.json({limit: '10kb'})) and rate-limit requests to prevent amplification {2,3,6}. 5. Run regex in worker threads or with timeouts using child_process to isolate from main event loop {7}. 6. Prefer schema validators (Joi, Zod) over raw regex for common cases like email/username {2,5}. 7. Monitor CPU/performance; use WAF for extra protection {1}. Node.js official docs do not cover user-provided regex specifically, as per threat model it's app-level {11,12}.

Citations:


🏁 Script executed:

# First, let's examine the search.ts file around the mentioned lines
head -n 380 packages/workspace-fs/src/search.ts | tail -n 50

Repository: MocA-Love/superset

Length of output: 1028


🏁 Script executed:

# Get a broader view of the file to understand the context
wc -l packages/workspace-fs/src/search.ts

Repository: MocA-Love/superset

Length of output: 102


🏁 Script executed:

# Check how compileSearchPattern is called and where user input comes from
rg "compileSearchPattern" packages/workspace-fs/src/search.ts -B 5 -A 5

Repository: MocA-Love/superset

Length of output: 600


🏁 Script executed:

# Look for any input validation or safety checks in the file
rg "regex|RegExp|pattern" packages/workspace-fs/src/search.ts -i | head -20

Repository: MocA-Love/superset

Length of output: 733


🏁 Script executed:

# Find the SearchContentOptions interface definition
rg "interface SearchContentOptions|type SearchContentOptions" packages/workspace-fs/src/search.ts -A 10

Repository: MocA-Love/superset

Length of output: 311


🏁 Script executed:

# Find the searchContent and replaceContent function signatures
rg "export.*function (searchContent|replaceContent)" packages/workspace-fs/src/search.ts -A 8

Repository: MocA-Love/superset

Length of output: 408


🏁 Script executed:

# Check if there's any input validation or length checks
rg "query.*length|validate|sanitize|trim" packages/workspace-fs/src/search.ts -B 2 -A 2

Repository: MocA-Love/superset

Length of output: 2159


🏁 Script executed:

# Look at the calling context - where is this used from?
rg "searchContent|replaceContent" packages/workspace-fs/src/search.ts | head -5

Repository: MocA-Love/superset

Length of output: 283


ReDoS vulnerability: User-supplied regex patterns compiled without validation.

The compileSearchPattern function at line 372 directly constructs a RegExp from user input when isRegex is true: const source = isRegex ? query : escapeRegExp(query). The exported searchContent and replaceContent functions accept this query parameter directly from users with only basic trimming and empty checks—no regex complexity validation. Malicious patterns like (a+)+$ combined with crafted input can trigger catastrophic backtracking, freezing the single-threaded Node.js event loop and causing denial of service.

Recommended mitigations:

  • Use the re2 package (Google's RE2 engine) as a drop-in replacement to guarantee linear-time matching
  • Validate patterns with safe-regex before compilation
  • Add query length limits and request rate-limiting
  • Run regex operations in worker threads with timeout mechanisms to isolate from the event loop
🧰 Tools
🪛 ast-grep (0.42.0)

[warning] 372-372: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(source, flags)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

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

In `@packages/workspace-fs/src/search.ts` around lines 354 - 375,
compileSearchPattern currently constructs a RegExp directly from user input (via
the isRegex branch) which can lead to ReDoS; update compileSearchPattern and the
callers searchContent and replaceContent to validate or sandbox user regexes
before compilation: run the raw query through a safe-regex check (or limit
allowed quantifier constructs), enforce a strict max query length, and if
isRegex is true either compile with the re2 engine instead of RegExp or perform
validation and refuse/escape unsafe patterns; also consider executing regex work
in a worker with a timeout for extra safety. Ensure you update references to
escapeRegExp use for non-regex queries and add clear error handling for
rejected/unsafe patterns so callers get deterministic failures instead of
compiling dangerous regexes.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx (1)

539-548: ⚠️ Potential issue | 🟠 Major

Scope SEARCH_IN_FILES hotkey to active workspace pages.

The SEARCH_IN_FILES hotkey is registered with undefined options, bypassing the isActive gating used by other hotkeys in this component. This means the shortcut can trigger from inactive workspace pages. Either add an isActive guard inside handleSearchInFiles or pass hotkeyOptions to the registration.

🔧 Proposed fix: Pass hotkeyOptions to scope the hotkey
-	useAppHotkey("SEARCH_IN_FILES", handleSearchInFiles, undefined, [
+	useAppHotkey("SEARCH_IN_FILES", handleSearchInFiles, hotkeyOptions, [
 		handleSearchInFiles,
 	]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/`$workspaceId/page.tsx
around lines 539 - 548, The SEARCH_IN_FILES hotkey is currently registered
without scoping and can fire on inactive workspace pages; fix by either adding
an early isActive check inside handleSearchInFiles or, preferably, pass
hotkeyOptions to useAppHotkey so the hotkey is only active when the workspace
page is active (e.g., useAppHotkey("SEARCH_IN_FILES", handleSearchInFiles, {
isActive: /* same active predicate used by other hotkeys */ },
[handleSearchInFiles])). Ensure the guard uses the same active predicate as
other hotkeys in this component and keep the existing behavior in
handleSearchInFiles (setSidebarOpen, setSidebarMode(SidebarMode.Tabs),
setRightSidebarTab(RightSidebarTab.Search)).
🧹 Nitpick comments (1)
apps/desktop/src/main/lib/agent-setup/templates/gemini-hook.template.sh (1)

31-63: Consider extracting the sleep inhibition logic into a reusable function.

This sleep inhibition block is duplicated verbatim across gemini-hook.template.sh, cursor-hook.template.sh, and copilot-hook.template.sh. The notify-hook.template.sh already extracts this into a _superset_manage_sleep_inhibitor function, which improves readability.

While template files are deployed independently (so some duplication is acceptable), the inline approach here makes future maintenance harder—any bug fix or enhancement needs to be applied to 4 files.

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

In `@apps/desktop/src/main/lib/agent-setup/templates/gemini-hook.template.sh`
around lines 31 - 63, The duplicated sleep-inhibition block in
gemini-hook.template.sh (guarded by SUPERSET_WRAPPER_PID,
SUPERSET_PREVENT_AGENT_SLEEP and EVENT_TYPE) should be extracted into a reusable
function named like _superset_manage_sleep_inhibitor (matching
notify-hook.template.sh) and invoked from the case statement; move the logic
that creates _superset_sleep_dir, reads/writes the _superset_pid_file,
starts/stops caffeinate, and checks kill -0 into that function, preserve the
checks for Darwin and command -v caffeinate, and replace the inline block in
gemini-hook.template.sh with a single call to _superset_manage_sleep_inhibitor
so future changes only need to be applied once.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/`$workspaceId/page.tsx:
- Around line 539-548: The SEARCH_IN_FILES hotkey is currently registered
without scoping and can fire on inactive workspace pages; fix by either adding
an early isActive check inside handleSearchInFiles or, preferably, pass
hotkeyOptions to useAppHotkey so the hotkey is only active when the workspace
page is active (e.g., useAppHotkey("SEARCH_IN_FILES", handleSearchInFiles, {
isActive: /* same active predicate used by other hotkeys */ },
[handleSearchInFiles])). Ensure the guard uses the same active predicate as
other hotkeys in this component and keep the existing behavior in
handleSearchInFiles (setSidebarOpen, setSidebarMode(SidebarMode.Tabs),
setRightSidebarTab(RightSidebarTab.Search)).

---

Nitpick comments:
In `@apps/desktop/src/main/lib/agent-setup/templates/gemini-hook.template.sh`:
- Around line 31-63: The duplicated sleep-inhibition block in
gemini-hook.template.sh (guarded by SUPERSET_WRAPPER_PID,
SUPERSET_PREVENT_AGENT_SLEEP and EVENT_TYPE) should be extracted into a reusable
function named like _superset_manage_sleep_inhibitor (matching
notify-hook.template.sh) and invoked from the case statement; move the logic
that creates _superset_sleep_dir, reads/writes the _superset_pid_file,
starts/stops caffeinate, and checks kill -0 into that function, preserve the
checks for Darwin and command -v caffeinate, and replace the inline block in
gemini-hook.template.sh with a single call to _superset_manage_sleep_inhibitor
so future changes only need to be applied once.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f8a1e54b-3a15-486e-a425-ceb1be7e703d

📥 Commits

Reviewing files that changed from the base of the PR and between c38680e and 87d87d5.

📒 Files selected for processing (27)
  • apps/desktop/src/lib/trpc/routers/menu.ts
  • apps/desktop/src/lib/trpc/routers/settings/index.ts
  • apps/desktop/src/main/lib/agent-setup/agent-wrappers-common.ts
  • apps/desktop/src/main/lib/agent-setup/templates/copilot-hook.template.sh
  • apps/desktop/src/main/lib/agent-setup/templates/cursor-hook.template.sh
  • apps/desktop/src/main/lib/agent-setup/templates/gemini-hook.template.sh
  • apps/desktop/src/main/lib/agent-setup/templates/notify-hook.template.sh
  • apps/desktop/src/main/lib/menu-events.ts
  • apps/desktop/src/main/lib/menu.ts
  • apps/desktop/src/main/lib/terminal/env.ts
  • apps/desktop/src/renderer/lib/browser-shortcut-events.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/PaneViewer/PaneViewer.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/PaneViewer/pane-viewer.model.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/layout.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/BehaviorSettings/BehaviorSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/ReviewPanel/ReviewPanel.tsx
  • apps/desktop/src/renderer/stores/hotkeys/store.ts
  • apps/desktop/src/shared/browser-shortcuts.ts
  • apps/desktop/src/shared/constants.ts
  • apps/desktop/src/shared/hotkeys.ts
  • packages/local-db/drizzle/0038_groovy_sister_grimm.sql
  • packages/local-db/drizzle/meta/0038_snapshot.json
  • packages/local-db/drizzle/meta/_journal.json
  • packages/local-db/src/schema/schema.ts
✅ Files skipped from review due to trivial changes (9)
  • apps/desktop/src/main/lib/menu-events.ts
  • apps/desktop/src/shared/constants.ts
  • packages/local-db/drizzle/0038_groovy_sister_grimm.sql
  • apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/PaneViewer/pane-viewer.model.ts
  • apps/desktop/src/main/lib/menu.ts
  • packages/local-db/drizzle/meta/_journal.json
  • packages/local-db/drizzle/meta/0038_snapshot.json
  • apps/desktop/src/main/lib/terminal/env.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/BehaviorSettings/BehaviorSettings.tsx
  • apps/desktop/src/shared/hotkeys.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts

@MocA-Love MocA-Love merged commit c20006b into main Apr 1, 2026
13 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant