Skip to content

chore: squash all-work branch changes#4

Closed
anduimagui wants to merge 13 commits intodevfrom
clean/all-work
Closed

chore: squash all-work branch changes#4
anduimagui wants to merge 13 commits intodevfrom
clean/all-work

Conversation

@anduimagui
Copy link
Owner

@anduimagui anduimagui commented Mar 11, 2026

Summary

  • Recreate the all-work integration branch from dev as a squashed tracking branch that is easier to review than local/all-work.
  • Preserve the combined app, desktop, UI, session, model, and project-management work from the integration branch.
  • Track the additional upstream project-close command palette work from anomalyco/opencode#17018 on top of the existing stack.

Upstream PRs referenced

Notes

  • clean/all-work remains the clean tracking branch for the combined work.
  • The older local/all-work PR is still available as an unsquashed reference branch if needed.

@github-actions
Copy link

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@github-actions
Copy link

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@coderabbitai
Copy link

coderabbitai bot commented Mar 11, 2026

📝 Walkthrough

Walkthrough

A comprehensive feature release adding workspace pinning with persistence, project grouping and sub-project management, session modal with deep linking, native Git repository cloning, model favorites and quick select, rich markdown copy support, and enhanced file reference parsing across the application.

Changes

Cohort / File(s) Summary
Workspace Pinning
packages/app/e2e/actions.ts, packages/app/e2e/selectors.ts, packages/app/src/pages/layout/sidebar-workspace.tsx
Added setWorkspacePinned action and workspacePinToggleSelector, workspaceDividerSelector selectors to support toggling and persisting workspace pinned state; integrated pin/unpin UI controls and divider rendering in workspace sidebar.
Project Grouping & Sub-Projects
packages/app/src/pages/layout/helpers.ts, packages/app/src/pages/layout/helpers.test.ts, packages/app/src/pages/layout/sidebar-project.tsx, packages/app/src/pages/layout/sidebar-shell.tsx, packages/app/src/pages/layout/layout.tsx
Introduced createProjectGroups, projectGroupID, projectGroupLabel utilities and effectiveWorkspacePinnedOrder for grouping projects by worktree; added addSubProject, removeSubProject, hasParentProject context hooks; updated sidebar rendering with subproject animations and parent-child relationship management.
Session Modal & Deep Linking
packages/app/src/pages/layout/deep-links.ts, packages/app/src/pages/layout/layout.tsx, packages/session-modal/*
Restructured deep-link parsing to support open-project and open-session actions via new DeepLinkAction type and collectDeepLinkActions function; created new session-modal package with server discovery, session listing, sorting by priority, and deep-link navigation to desktop app.
Git Repository Cloning
packages/app/src/components/dialog-open-project.helpers.ts, packages/app/src/components/dialog-open-project.helpers.test.ts, packages/app/src/components/dialog-open-project.tsx, packages/app/src/context/platform.tsx, packages/desktop/src-tauri/src/lib.rs, packages/desktop/src/bindings.ts, packages/desktop/src/index.tsx
Added DialogOpenProject component with Git URL validation, clone target path resolution, and error handling; exposed platform APIs cloneGitRepository, normalizeProjectPath, getDefaultCloneDirectory, setDefaultCloneDirectory; implemented Tauri command for cloning with WSL support on Windows and numeric suffix collision avoidance.
Model Favorites & Quick Select
packages/app/src/components/dialog-select-model.tsx, packages/app/src/components/dialog-select-model-unpaid.tsx, packages/app/src/context/models.tsx, packages/app/src/context/local.tsx, packages/app/src/components/settings-models.tsx
Introduced favorite and quick (slots a/b) APIs in models context with toggle and selection management; added favorites sorting, UI toggle buttons with tooltips; implemented quick-select settings with Clear buttons and dynamic labels.
Rich Markdown Copy
packages/ui/src/components/markdown-copy.ts, packages/ui/src/components/markdown-copy.test.ts, packages/ui/src/components/message-part.tsx, packages/ui/src/components/session-turn.tsx, packages/app/src/context/settings.tsx
Created markdownClipboardHTML serialization and writeClipboardPayload utilities with fallback support; added AssistantCopyFormat setting ("plain" | "rich" | "ask") with UI dropdown for mode selection in message copy flow; integrated clipboard write via platform API.
File Reference Parsing
packages/ui/src/components/markdown-file-ref.ts, packages/ui/src/components/markdown-file-ref.test.ts, packages/ui/src/components/markdown.tsx, packages/ui/src/components/message-part.tsx, packages/ui/src/context/data.tsx
Added parseCodeFileRef parser supporting file:// URLs, relative paths, and #L suffixes; integrated onOpenFilePath handler in Markdown and message rendering to open files in review panel; added looksLikePath and normalizeProjectPath validation helpers.
E2E Tests & Workspace Tests
packages/app/e2e/projects/projects-switch.spec.ts, packages/app/e2e/projects/workspaces.spec.ts, packages/app/e2e/projects/actions.ts
Added workspace pinning tests with persistence and isolation per project; refactored workspace-switching to use waitSlug instead of explicit sidebar interactions; imported createTestProject helper for test setup.
Settings & UI Dialogs
packages/app/src/components/dialog-settings.tsx, packages/app/src/components/settings-general.tsx, packages/app/src/components/settings-keybinds.tsx, packages/app/src/components/dialog-select-session.tsx, packages/app/src/pages/session/message-timeline.tsx
Changed default command palette keybinding from mod+shift+p to mod+p; added SettingsTab prop to DialogSettings for tab control; created DialogSelectSession component for session listing and selection; added desktop clone path management in settings; integrated assistantCopyMode prop throughout message rendering.
Desktop & Platform Integration
packages/desktop/src-tauri/src/server.rs, packages/desktop/package.json, packages/desktop/scripts/predev.ts, packages/desktop/src/index.tsx, packages/desktop/src/menu.ts, packages/desktop/src-tauri/capabilities/default.json
Added get_default_clone_directory and set_default_clone_directory Tauri commands with OS-specific defaults; implemented writeClipboardText for clipboard write; added menu items for session rename, jump to top/bottom, archive/unarchive, clone project, search sessions; added clipboard-manager:allow-write-text permission.
Project Icon Management
packages/app/src/components/dialog-edit-project.tsx, packages/app/src/context/layout.tsx, packages/app/src/context/global-sync/bootstrap.ts, packages/app/src/context/layout.test.ts, packages/app/src/pages/layout/sidebar-items.tsx, packages/opencode/src/project/project.ts
Introduced pickProjectIcon helper to merge icon url/override from child and metadata; updated icon payload to use icon.url instead of icon.override in project updates; added icon discovery and override support in project loading with fallback chain; extended icon shape to include optional override field.
Session Features & Commands
packages/app/src/pages/session.tsx, packages/app/src/pages/session/helpers.ts, packages/app/src/pages/session/helpers.test.ts, packages/app/src/pages/session/use-session-commands.tsx, packages/app/src/components/session/session-header.tsx, packages/app/src/pages/session/terminal-panel.tsx
Added jumpToTop command and session copy with clipboard support; extended createOpenReviewFile to accept openReviewPanel and setSelectedLines callbacks for line-based file opening; implemented copy session ID button in header; added terminal focus management for desktop; integrated session archive/unarchive commands with undo support.
Server & Database Changes
packages/opencode/src/server/routes/session.ts, packages/opencode/src/session/index.ts, packages/opencode/src/context/server.tsx, packages/opencode/src/context/server.test.ts
Extended setArchived to accept optional updated timestamp and nullable time; added normalizeWorktree helper to deduplicate projects by normalized path; updated session archiving to track update time and return modified session info.
Internationalization
packages/app/src/i18n/en.ts, packages/desktop/src/i18n/en.ts, packages/ui/src/i18n/en.ts
Added translation keys for workspace features, session commands, model actions, desktop menu items, copy modes, and keyboard shortcuts across app and desktop packages.
Configuration & Utilities
packages/opencode/src/config/config.ts, packages/opencode/src/cli/cmd/desktop.ts, packages/opencode/src/index.ts, packages/opencode/src/project/project.ts, packages/opencode/src/util/which.ts, packages/script/src/index.ts, packages/script/src/version.ts, packages/script/src/version.test.ts
Implemented package-based agent loading from node_modules with loadPackageAgents and pluginVersion helpers; added DesktopCommand CLI command for opening projects via deep links; extended version handling with sanitizeChannel, sanitizePreviewVersion, and previewVersion utilities.
UI Components & Utilities
packages/ui/src/components/markdown.tsx, packages/ui/src/components/message-part.css, packages/ui/src/components/message-part.tsx, packages/ui/src/hooks/filter-search.ts, packages/ui/src/hooks/filter-search.test.ts, packages/ui/src/hooks/use-filtered-list.tsx, packages/ui/src/context/dialog.tsx, packages/util/src/session-transcript.ts
Refactored code block rendering to support file links and selection-aware copy; introduced normalize and fuzzy search utilities with prefix boosting; created formatSessionTranscript for markdown export; added drag-drop prevention for file navigation and file-path click handling.
Configuration & Build
.github/pull_request_template.md, package.json, packages/app/src/app.tsx, packages/desktop/src-tauri/tauri.session-modal.conf.json, packages/desktop/src-tauri/src/constants.rs, packages/opencode/script/preload.js, packages/web/src/content/docs/keybinds.mdx, script/check-preload.ts, packages/session-modal/*
Updated PR template with concise formatting; added predev build step to desktop dev script; introduced openSessionSearchOnStart config flag; created session-modal Vite project with TypeScript and Solid; added preload verification script; updated keybinds documentation.

Sequence Diagrams

sequenceDiagram
    participant User as User
    participant DialogOpenProject as DialogOpenProject
    participant Platform as Platform API
    participant Tauri as Tauri Backend
    participant System as System (git)
    participant Filesystem as Filesystem

    User->>DialogOpenProject: Select git mode, enter URL
    DialogOpenProject->>DialogOpenProject: Validate Git URL
    activate DialogOpenProject
    DialogOpenProject->>Platform: cloneGitRepository(url, targetDir)
    activate Platform
    Platform->>Tauri: clone_git_repository command
    activate Tauri
    Tauri->>Tauri: Resolve target directory (default or provided)
    Tauri->>System: git clone url directory
    activate System
    System->>Filesystem: Create repository
    Filesystem-->>System: Repository created
    deactivate System
    System-->>Tauri: Success, directory path
    Tauri-->>Platform: Return cloned directory path
    deactivate Tauri
    Platform-->>DialogOpenProject: Return directory
    deactivate Platform
    DialogOpenProject->>DialogOpenProject: List files, emit onSelect
    DialogOpenProject-->>User: Open project with new directory
    deactivate DialogOpenProject
Loading
sequenceDiagram
    participant Tauri as Desktop App (Tauri)
    participant SessionModal as Session Modal (Vite)
    participant SDK as OpenCode SDK
    participant Server as Server API

    Tauri->>SessionModal: Launch iframe/window
    activate SessionModal
    SessionModal->>Tauri: awaitInitialization()
    activate Tauri
    Tauri->>Server: Get server URL, auth
    Server-->>Tauri: Server ready data
    Tauri-->>SessionModal: URL, credentials, source
    deactivate Tauri
    SessionModal->>SDK: Initialize client(baseUrl, auth)
    activate SDK
    SDK->>Server: List projects, sessions
    Server-->>SDK: Projects and sessions
    SDK-->>SessionModal: Data
    deactivate SDK
    SessionModal->>SessionModal: Sort by priority, filter "needs attention"
    User->>SessionModal: Click session
    SessionModal-->>SessionModal: Build opencode://open-session deep link
    SessionModal->>Tauri: Navigate to deep link
    activate Tauri
    Tauri->>Tauri: Parse deep link, navigate to session
    Tauri-->>User: Open session in main window
    deactivate Tauri
    deactivate SessionModal
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Hops with glee through workspaces pinned so tight,
Projects grouped like gardens, each subproject right,
Sessions modal-searching, deep links set to go,
Git clones sprouting swiftly, watch favorites grow!
Rich text copies flowing, files linked with care,
A bouncy release, beyond compare! 🎉

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch clean/all-work

…te' into clean/all-work

# Conflicts:
#	packages/app/src/i18n/en.ts
#	packages/app/src/pages/layout.tsx
#	packages/ui/src/components/message-part.tsx
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Note

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

🟠 Major comments (27)
packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx-340-341 (1)

340-341: ⚠️ Potential issue | 🟠 Major

Fix scoped-agent insertion too, not just rendering.

This avoids the doubled @ in the menu, but selecting a scoped agent still inserts @@scope/name because onSelect passes agent.name into insertPart(), and insertPart() always prepends another @. Please normalize the inserted value the same way as the display text.

Suggested fix
       .map(
         (agent): AutocompleteOption => ({
           // Avoid double @ for scoped package agents (e.g., `@openpets/coder/pr-review`)
           display: agent.name.startsWith("@") ? agent.name : "@" + agent.name,
           onSelect: () => {
-            insertPart(agent.name, {
+            insertPart(agent.name.startsWith("@") ? agent.name.slice(1) : agent.name, {
               type: "agent",
               name: agent.name,
               source: {
                 start: 0,
                 end: 0,
                 value: "",
               },
             })
           },
         }),
       )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx` around
lines 340 - 341, The selected scoped-agent value is being double-@ inserted
because onSelect passes agent.name into insertPart() while insertPart() always
prepends "@"; update the onSelect handler (where agent objects are used in the
autocomplete list) to normalize the value the same way as the display logic:
compute selectedValue = agent.name.startsWith("@") ? agent.name : "@" +
agent.name and pass selectedValue into insertPart(), so both rendering and
insertion use identical normalization for agent.name.
packages/util/src/session-transcript.ts-45-45 (1)

45-45: ⚠️ Potential issue | 🟠 Major

Avoid exporting raw reasoning parts by default.

This branch serializes part.type === "reasoning" verbatim into the transcript. If reasoning parts are meant to stay hidden anywhere else in the product, this export path bypasses that and leaks internal model traces.

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

In `@packages/util/src/session-transcript.ts` at line 45, The current serializer
in session-transcript.ts returns reasoning parts verbatim when part.type ===
"reasoning" (the `_Thinking:_\n\n${part.text}\n\n` branch), which can leak
internal model traces; change the behavior to never export raw reasoning text —
either skip these parts or replace them with a fixed redaction placeholder like
"[internal reasoning hidden]" (preserve other part types as-is), and update the
function that builds transcripts to use that placeholder instead of returning
part.text for reasoning parts.
packages/util/src/session-transcript.ts-48-56 (1)

48-56: ⚠️ Potential issue | 🟠 Major

Make tool-state rendering resilient to falsy and non-JSON inputs.

part.state.input ? ... skips valid inputs like 0, false, and "", and JSON.stringify(part.state.input, null, 2) can throw for BigInt or circular objects, which would fail the entire transcript export.

🛠️ Proposed hardening
+const stringifyForTranscript = (value: unknown) => {
+  try {
+    return JSON.stringify(value, null, 2)
+  } catch {
+    return String(value)
+  }
+}
+
 const formatPart = (part: Part) => {
   if (part.type === "text" && part.text && !part.synthetic) return `${part.text}\n\n`
   if (part.type === "reasoning" && part.text) return `_Thinking:_\n\n${part.text}\n\n`
   if (part.type !== "tool" || !part.tool || !part.state) return ""
 
-  const input = part.state.input
-    ? `\n**Input:**\n\`\`\`json\n${JSON.stringify(part.state.input, null, 2)}\n\`\`\`\n`
+  const input = part.state.input !== undefined
+    ? `\n**Input:**\n\`\`\`json\n${stringifyForTranscript(part.state.input)}\n\`\`\`\n`
     : ""
   const output =
-    part.state.status === "completed" && part.state.output
+    part.state.status === "completed" && part.state.output !== undefined
       ? `\n**Output:**\n\`\`\`\n${part.state.output}\n\`\`\`\n`
       : ""
   const error =
-    part.state.status === "error" && part.state.error ? `\n**Error:**\n\`\`\`\n${part.state.error}\n\`\`\`\n` : ""
+    part.state.status === "error" && part.state.error !== undefined
+      ? `\n**Error:**\n\`\`\`\n${part.state.error}\n\`\`\`\n`
+      : ""
   return `**Tool: ${part.tool}**\n${input}${output}${error}\n`
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/util/src/session-transcript.ts` around lines 48 - 56, The current
checks use truthiness and raw JSON.stringify which will skip valid falsy inputs
(0, false, "") and can throw on BigInt/circular values; change the guards to
test for null/undefined (e.g., part.state.input !== undefined &&
part.state.input !== null) for input, output and error, and wrap JSON
serialization in a safe try/catch: attempt JSON.stringify(part.state.input,
null, 2) and on failure fall back to a safe string representation (e.g.,
String(part.state.input) or util.inspect(part.state.input) / converting BigInt
to string); update the template builders (the variables input, output, error
near part.state.input/part.state.output/part.state.error) to use these
null/undefined checks and the safe-serialize fallback so transcript generation
never throws on non-JSON values.
packages/opencode/src/config/config.ts-739-744 (1)

739-744: ⚠️ Potential issue | 🟠 Major

Prompt-based package agents drop their own name.

agentName is computed above, but the parsed config never includes it. Callers that read config.agent[key].name—the same shape produced by the local loaders and the agent/ package path—will get undefined for prompts loaded from npm packages.

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

In `@packages/opencode/src/config/config.ts` around lines 739 - 744, The parsed
Agent config is missing its name field so package-based prompt agents end up
with undefined names; update the config creation in the block that constructs
const config: Agent to include a name property using the already-computed
agentName (e.g. name: frontmatter.name || agentName) so callers expecting
config.agent[key].name get the correct value; modify the object literal that
currently sets description, prompt, mode, ...frontmatter to also set name
accordingly.
packages/opencode/src/config/config.ts-288-288 (1)

288-288: ⚠️ Potential issue | 🟠 Major

Normalize the target version in needsInstall() as well.

If Installation.VERSION is not valid semver, Line 288 writes *, but needsInstall() still compares @opencode-ai/plugin against the raw version string. That makes the dependency look stale on every load and re-triggers installs indefinitely.

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

In `@packages/opencode/src/config/config.ts` at line 288, needsInstall() currently
compares the installed '@opencode-ai/plugin' version against raw
Installation.VERSION which can be non-semver and causes perpetual reinstalls;
update needsInstall() to use the same normalized targetVersion logic as Line 288
(i.e., use "*" when Installation.isLocal() is true, otherwise call
pluginVersion(Installation.VERSION)) so the comparison uses the canonical
targetVersion value; reference the existing pluginVersion(...) helper and
Installation.isLocal()/Installation.VERSION when locating where to change the
comparison.
packages/opencode/src/config/config.ts-218-220 (1)

218-220: ⚠️ Potential issue | 🟠 Major

Don't merge project package agents after the managed override.

This runs after the enterprise-managed config load, so agents from Instance.worktree/node_modules can overwrite managed agent fields and break the documented precedence order. Move this scan before Lines 212-216, or re-apply managed config afterward.

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

In `@packages/opencode/src/config/config.ts` around lines 218 - 220, The project
node_modules agent load currently merges into result.agent after the
enterprise-managed override, allowing Instance.worktree package agents (loaded
via loadPackageAgents and merged with mergeDeep into result.agent) to overwrite
managed settings; move the projectNodeModules scan and the call that sets
result.agent = mergeDeep(...) to execute before the managed config application
(the code that applies the managed override), or if easier, keep it where it is
but re-apply the managed configuration merge after calling loadPackageAgents so
the managed config wins; update references to Instance.worktree,
loadPackageAgents, mergeDeep, and result.agent accordingly so the precedence is
correct.
packages/opencode/src/config/config.ts-672-717 (1)

672-717: ⚠️ Potential issue | 🟠 Major

Malformed package prompts currently fail the entire config load.

Unlike loadAgent() and loadCommand(), these paths call ConfigMarkdown.parse() without a recovery path. One bad markdown file in a dependency will make Config.get() reject for the whole workspace instead of logging and skipping the bad package entry.

Also applies to: 720-752

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

In `@packages/opencode/src/config/config.ts` around lines 672 - 717, In
loadAgentsFromPackage, surround the call(s) to ConfigMarkdown.parse (used in the
PACKAGE_AGENT_GLOB and PACKAGE_PROMPT_GLOB handling) with a try/catch so a
malformed markdown file does not throw and abort Config.get; on parse failure
catch the error, log a clear message including the package name and the file
path (and the error) and then continue to the next file (skip adding that agent
to result), mirroring the resilient behavior in loadAgent/loadCommand and
ensuring result only gets populated with successfully parsed Agent.safeParse
entries (e.g., when constructing and assigning parsed.data to
result[prefixedName]).
script/check-preload.ts-9-13 (1)

9-13: ⚠️ Potential issue | 🟠 Major

Don't regex-parse bunfig.toml here.

bunfig.toml is TOML, so preload entries can legally use single-quoted literal strings and comments. The current regex-based approach has two concrete problems:

  1. Misses single-quoted entries: A regex for "([^"]+)" only extracts double-quoted strings. Single-quoted values like preload = ['./preload.ts'] are silently ignored.
  2. Matches commented-out entries: The regex preload\s*=\s*\[([\s\S]*?)\] can match commented-out lines like # preload = ["./ghost.ts"], reporting files that shouldn't be preloaded.

Bun supports TOML as a standard parsed format, so use a real TOML parser instead.

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

In `@script/check-preload.ts` around lines 9 - 13, The current code builds preload
by regexing the file text (the const text and const preload logic using
/preload\s*=\s*\[([\s\S]*?)\]/ and /"([^"]+)"/), which misses single-quoted TOML
strings and picks up commented-out lines; replace this with a proper TOML parse:
read the file text, parse it with a TOML parser, then extract the preload array
from the parsed object (handling both single- and double-quoted strings
naturally and ignoring comments), and assign that array to the preload variable
instead of using the regex extraction.
script/check-preload.ts-7-8 (1)

7-8: ⚠️ Potential issue | 🟠 Major

Fix the node_modules skip predicate.

node_modules/pkg/bunfig.toml at the repo root never matches "/node_modules/", so dependency bunfigs still get scanned. That can make this check fail on files the script meant to ignore.

♻️ Proposed fix
 for await (const file of new Bun.Glob("**/bunfig.toml").scan(".")) {
-  if (file.includes("/node_modules/")) continue
+  if (file.split(/[\\/]/).includes("node_modules")) continue
   const text = await Bun.file(file).text()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@script/check-preload.ts` around lines 7 - 8, The skip predicate currently
checks file.includes("/node_modules/"), which misses repo-root paths like
"node_modules/pkg/bunfig.toml"; update the predicate used in the Bun.Glob scan
loop (the file variable in the for-await over new Bun.Glob(...).scan(".")) to
detect node_modules as a path segment instead of requiring leading/trailing
slashes—for example replace the string check with a path-segment-aware test
(e.g. a RegExp like /(^|\/)node_modules\// or an includes("node_modules/") that
accounts for both leading and non-leading occurrences) so dependency bunfigs are
correctly ignored.
packages/app/src/components/dialog-select-session.tsx-169-172 (1)

169-172: ⚠️ Potential issue | 🟠 Major

Don't collapse fetch failures into an empty state.

A rejected session.list() currently becomes [], so the dialog shows “empty” instead of “failed to load”. Because cached stays unset and inflight is cleared in finally, the next search/input will silently retry the same failing request.

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

In `@packages/app/src/components/dialog-select-session.tsx` around lines 169 -
172, The current catch in dialog-select-session's fetch chain swallows
session.list() failures by returning [] and leaving state.cached unset, which
hides errors and causes silent retries; instead, in the promise chain for
session.list() remove the catch that maps failures to [] and handle errors
explicitly: on rejection set a state.error (or state.loadFailed) flag/message
and do not populate state.cached, then keep the finally block to clear
state.inflight; update any UI consumers to show the “failed to load” state when
state.error is set. Ensure you reference the same session.list() call site and
state properties (state.inflight, state.cached, add
state.error/state.loadFailed) so failures are surfaced rather than collapsed
into an empty list.
packages/app/src/components/dialog-select-session.tsx-210-212 (1)

210-212: ⚠️ Potential issue | 🟠 Major

Add "directory" to filterKeys to align with the items filter logic.

The items() function filters on title, description, and directory (line 185), but filterKeys only includes the first two. This causes items matching only on directory to be excluded by the subsequent fuzzy filter in useFilteredList.

        items={items}
        key={(item) => item.id}
        filterKeys={["title", "description", "directory"]}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app/src/components/dialog-select-session.tsx` around lines 210 -
212, The filterKeys prop on the list is missing "directory", causing
directory-only matches to be excluded; update the component where you pass
filterKeys (the prop alongside items and key in dialog-select-session.tsx) to
include "directory" (i.e., change filterKeys from ["title","description"] to
["title","description","directory"]) so it lines up with the items() filter
logic used earlier and the fuzzy filtering in useFilteredList.
packages/app/src/components/dialog-select-session.tsx-101-103 (1)

101-103: ⚠️ Potential issue | 🟠 Major

Use <Show> here; <Match> is inert outside <Switch>.

Line 101 wraps the preview branch in Match without a parent Switch. In Solid, <Match> is only a marker element that <Switch> inspects to determine which branch to render—it has no conditional logic of its own. The latest assistant text will not render. Use <Show> for standalone conditionals.

Suggested fix
-import { createMemo, Match, type Accessor, Switch } from "solid-js"
+import { createMemo, Match, Show, type Accessor, Switch } from "solid-js"
...
-          <Match when={showLatest() && latest()}>
-            <div class="text-12-regular text-text-weak truncate pr-4 mt-0.5">{latest()}</div>
-          </Match>
+          <Show when={showLatest() && latest()}>
+            {(text) => <div class="text-12-regular text-text-weak truncate pr-4 mt-0.5">{text()}</div>}
+          </Show>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app/src/components/dialog-select-session.tsx` around lines 101 -
103, The preview branch uses <Match> outside a <Switch>, so replace the <Match
when={showLatest() && latest()}> wrapper with Solid's <Show when={showLatest()
&& latest()}> and close it with </Show>, keeping the inner div and {latest()}
unchanged; also ensure Show is imported from 'solid-js' in the
dialog-select-session component so the conditional around showLatest() and
latest() actually renders.
packages/opencode/src/server/routes/session.ts-270-271 (1)

270-271: ⚠️ Potential issue | 🟠 Major

Keep time_updated server-owned.

updated is now part of the public PATCH payload and is forwarded during archive/unarchive. That lets callers backdate or future-date a session, which skews the time_updated ordering/filtering used by Session.list() and Session.listGlobal(). Stamp this on the server instead and keep any backfill/import path internal.

💡 Suggested change
       validator(
         "json",
         z.object({
           title: z.string().optional(),
           time: z
             .object({
               archived: z.number().nullable().optional(),
-              updated: z.number().optional(),
             })
             .optional(),
         }),
       ),
@@
         if (updates.time && "archived" in updates.time) {
           session = await Session.setArchived({
             sessionID,
             time: updates.time.archived ?? null,
-            updated: updates.time.updated,
+            updated: Date.now(),
           })
         }

Also applies to: 284-289

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

In `@packages/opencode/src/server/routes/session.ts` around lines 270 - 271, The
public session PATCH/schema must not accept client-supplied updated timestamps;
remove the client-facing "updated" field from the Zod schemas in session route
(the occurrences around the snippet where "archived:
z.number().nullable().optional(), updated: z.number().optional()," and the
similar block at lines ~284-289) and instead set/overwrite the session's
updated/time_updated server-side in the handlers that perform archive/unarchive
and general PATCH updates (the code paths that feed into Session.list() and
Session.listGlobal()); ensure server code stamps the current time (or
appropriate internal value) into the session record before saving so clients
cannot backdate/future-date sessions.
packages/app/src/components/dialog-select-model.tsx-96-113 (1)

96-113: ⚠️ Potential issue | 🟠 Major

Give the favorite toggle an accessible name and state.

This is an icon-only control. Right now assistive tech won't get a reliable label, and it also can't tell whether the model is already favorited.

♿ Suggested fix
           <Tooltip
             value={language.t(favorite(i.provider.id, i.id) ? "dialog.model.unfavorite" : "dialog.model.favorite")}
           >
             <IconButton
               icon="circle-check"
               variant="ghost"
               size="small"
               class="shrink-0"
+              aria-label={language.t(
+                favorite(i.provider.id, i.id) ? "dialog.model.unfavorite" : "dialog.model.favorite",
+              )}
+              aria-pressed={favorite(i.provider.id, i.id)}
               classList={{
                 "opacity-100": favorite(i.provider.id, i.id),
                 "opacity-30": !favorite(i.provider.id, i.id),
               }}
               onMouseDown={(event) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app/src/components/dialog-select-model.tsx` around lines 96 - 113,
The icon-only favorite toggle lacks accessible name and state; update the
IconButton used inside the Tooltip to include an accessible label and a toggle
state by adding aria-label set to the same localized string you compute
(language.t(favorite(...) ? "dialog.model.unfavorite" :
"dialog.model.favorite")) and aria-pressed bound to the boolean
favorite(i.provider.id, i.id); keep the existing onClick/onMouseDown and Tooltip
but ensure IconButton receives aria-label and aria-pressed so assistive tech can
read its name and whether it is currently favorited (use the existing
favorite(...) and toggleFavorite(...) helpers).
packages/desktop/scripts/predev.ts-5-21 (1)

5-21: ⚠️ Potential issue | 🟠 Major

Add explicit bounds checking for Windows and Linux architectures.

target() silently falls back to x64 binaries for any unrecognized architecture. Windows ARM (and any other non-x64 Windows host) will build/copy the wrong sidecar; same applies to non-ARM/non-x64 Linux hosts. Currently supported targets are only: aarch64-apple-darwin, x86_64-apple-darwin, x86_64-pc-windows-msvc, x86_64-unknown-linux-gnu, and aarch64-unknown-linux-gnu.

Fail fast for unsupported configurations instead of silently selecting x64:

🛠️ Suggested fix
 function target() {
   const env = Bun.env.TAURI_ENV_TARGET_TRIPLE
   if (env) return env

   if (process.platform === "darwin") {
     return process.arch === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin"
   }

   if (process.platform === "win32") {
-    return "x86_64-pc-windows-msvc"
+    if (process.arch === "x64") return "x86_64-pc-windows-msvc"
+    throw new Error(`Unsupported Windows architecture: ${process.arch}`)
   }

   if (process.arch === "arm64") {
     return "aarch64-unknown-linux-gnu"
   }

-  return "x86_64-unknown-linux-gnu"
+  if (process.arch === "x64") return "x86_64-unknown-linux-gnu"
+  throw new Error(`Unsupported Linux architecture: ${process.arch}`)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop/scripts/predev.ts` around lines 5 - 21, The target()
function currently falls back silently to x86_64 for unknown arches; update
target() to explicitly validate architectures for each platform and fail fast on
unsupported combos: keep the Bun.env.TAURI_ENV_TARGET_TRIPLE override, then for
darwin keep the existing arm64 vs x86_64 branching, for win32 only accept
process.arch === "x64" and return "x86_64-pc-windows-msvc" otherwise throw an
error (or process.exit) indicating unsupported Windows architecture, and for
linux only accept "arm64" (return "aarch64-unknown-linux-gnu") or "x64" (return
"x86_64-unknown-linux-gnu") otherwise throw an error indicating unsupported
Linux architecture; implement these checks in the existing target() function to
avoid silent fallbacks.
packages/app/src/pages/session/use-session-commands.tsx-286-301 (1)

286-301: ⚠️ Potential issue | 🟠 Major

Fix keybind conflicts in sessionCommand and modelCommand definitions.

Two keybinds are assigned twice, causing command shadowing:

  • mod+shift+arrowdown: assigned to both message.bottom (line 298) and model.quick.switch (line 317)
  • mod+shift+arrowup: assigned to both message.top (line 290) and model.quick.switch.reverse (line 333)

Choose distinct keybinds for these commands to ensure both remain accessible.

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

In `@packages/app/src/pages/session/use-session-commands.tsx` around lines 286 -
301, Two commands share the same hotkeys causing shadowing: change the duplicate
keybinds so sessionCommand ids "message.top" and "message.bottom" keep their
current mod+shift+arrowup/arrowdown while modelCommand ids "model.quick.switch"
and "model.quick.switch.reverse" use distinct keys (for example
mod+shift+arrowright and mod+shift+arrowleft, or mod+k / mod+shift+k) instead;
update the keybind strings in the modelCommand definitions (look for
modelCommand with id "model.quick.switch" and "model.quick.switch.reverse") so
they no longer collide with sessionCommand ids "message.top" and
"message.bottom".
packages/desktop/package.json-12-12 (1)

12-12: ⚠️ Potential issue | 🟠 Major

Route dev:session-modal through predev and use cross-platform environment variable handling.

This command calls tauri dev directly, skipping the sidecar binary preparation handled by predev (which builds and copies the opencode CLI binary to src-tauri/sidecars). Additionally, the VITE_OPEN_SESSION_SEARCH=1 syntax is POSIX-only and won't work on Windows shells.

Follow the pattern from the root dev:desktop command: run predev first, then tauri dev. Set the environment variable in a cross-platform way (e.g., via .env file, vite config, or a wrapper script).

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

In `@packages/desktop/package.json` at line 12, Update the "dev:session-modal" npm
script so it runs the predev preparation step before invoking tauri dev and does
not rely on POSIX-only inline env vars; specifically, change the script that
currently named "dev:session-modal" to call the existing predev script (same as
"dev:desktop" uses) and then run "tauri dev", and move the
VITE_OPEN_SESSION_SEARCH=1 setting into a cross-platform mechanism (for example
add it to the project .env, Vite config, or a small wrapper script that sets
process.env before starting) so Windows shells are supported and the opencode
sidecar is built/copied by predev.
packages/desktop/src-tauri/src/lib.rs-615-633 (1)

615-633: ⚠️ Potential issue | 🟠 Major

Avoid a live GitHub clone in a unit test.

This test requires outbound network access, GitHub availability, and a system git binary. It will flap in CI and fail for offline contributors. Please switch to a local temporary repo fixture or gate it behind an explicit integration-test opt-in.

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

In `@packages/desktop/src-tauri/src/lib.rs` around lines 615 - 633, The test
test_clone_with_git_real_repository calls clone_with_git with a real GitHub URL
causing network and git-binary dependence; replace it with a local temporary
repository fixture or gate it as an opt-in integration test. To fix, create a
local repo in temp_dir (e.g., initialize with git2 or run `git init` + create a
commit) and then call clone_with_git using a file:// or local path URL to that
repo, or alternatively mark test_clone_with_git_real_repository with #[ignore]
or move it behind a feature/#[cfg(test_integration)] so it only runs when
integration tests are explicitly enabled; ensure you still assert that the
target/.git exists and clean up temp files.
packages/desktop/src-tauri/src/lib.rs-433-437 (1)

433-437: ⚠️ Potential issue | 🟠 Major

The WSL clone path is unreachable right now.

This branch keys off get_wsl_config(), but packages/desktop/src-tauri/src/server.rs still returns WslConfig { enabled: false } unconditionally. On Windows, enabling WSL in the UI will still fall through to native git clone.

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

In `@packages/desktop/src-tauri/src/lib.rs` around lines 433 - 437, The WSL branch
never runs because get_wsl_config() always returns WslConfig { enabled: false }
in server.rs; update the code that constructs and returns WslConfig in server.rs
so it reflects the actual persisted/UI setting (or reads the same config source
the UI writes) instead of hardcoding false, so that get_wsl_config() on Windows
can return enabled=true when the user enables WSL and allow clone_with_wsl(&url,
directory.as_deref()) to execute.
packages/app/src/pages/session.tsx-821-826 (1)

821-826: ⚠️ Potential issue | 🟠 Major

Make jumpToTop reveal the cached history first.

createSessionHistoryWindow() only renders the tail of the timeline. This handler just scrolls the current window to top = 0, so the next backfill preserves the viewport and long sessions never actually jump to their beginning.

🛠️ Suggested fix
   const jumpToTop = () => {
     const el = scroller
     if (!el) return
     autoScroll.pause()
-    el.scrollTo({ top: 0, behavior: "auto" })
+    historyWindow.setTurnStart(0)
+    requestAnimationFrame(() => {
+      scroller?.scrollTo({ top: 0, behavior: "auto" })
+    })
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app/src/pages/session.tsx` around lines 821 - 826, The jumpToTop
handler currently just scrolls the visible window; first ensure the timeline
window is repositioned to include the cached/earliest history before scrolling:
call the function that renders the full/history tail
(createSessionHistoryWindow) or whatever routine that expands the session
history window to the beginning, await or wait for that update to complete, then
pause autoScroll and call scroller.scrollTo({ top: 0, behavior: "auto" }); keep
the existing guards (if (!scroller) return) and ensure any async call to
createSessionHistoryWindow is awaited or its completion observed before
performing the scroll.
packages/ui/src/components/markdown-copy.ts-42-54 (1)

42-54: ⚠️ Potential issue | 🟠 Major

Add error handling for clipboard.write() rejections at runtime.

Even when ClipboardItem and clipboard.write exist, the operation can reject with NotAllowedError in non-secure contexts, without user activation, or when blocked by permissions/policy. Currently, such failures cause the copy to fail silently instead of degrading to plain text.

Wrap the write() call in a try-catch block to fall back to writeText() as a final attempt:

Suggested fix
 export async function writeClipboardPayload(input: { text: string; html?: string }) {
   const clipboard = navigator.clipboard
   if (!clipboard) return
   if (!input.html || typeof ClipboardItem === "undefined" || typeof clipboard.write !== "function") {
     await clipboard.writeText(input.text)
     return
   }
-  await clipboard.write([
-    new ClipboardItem({
-      "text/plain": new Blob([input.text], { type: "text/plain" }),
-      "text/html": new Blob([input.html], { type: "text/html" }),
-    }),
-  ])
+  try {
+    await clipboard.write([
+      new ClipboardItem({
+        "text/plain": new Blob([input.text], { type: "text/plain" }),
+        "text/html": new Blob([input.html], { type: "text/html" }),
+      }),
+    ])
+  } catch {
+    await clipboard.writeText(input.text)
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/markdown-copy.ts` around lines 42 - 54, In
writeClipboardPayload, wrap the clipboard.write([...]) call in a try-catch
inside the writeClipboardPayload function (when ClipboardItem and
clipboard.write are available) and on error call await
clipboard.writeText(input.text) as a fallback; ensure you still return after
successful write or fallback and reference the existing symbols clipboard,
ClipboardItem, clipboard.write, and clipboard.writeText so failures (e.g.,
NotAllowedError) degrade to plain-text copy instead of failing silently.
packages/session-modal/src/main.tsx-212-220 (1)

212-220: ⚠️ Potential issue | 🟠 Major

Preserve discovered auth when reloading the server URL.

saveBaseUrl() replaces the whole connection with { baseUrl, source: "manual" }. In the Tauri flow that drops the initialized username/password, so pressing Reload once can turn a working authenticated modal into repeated 401s.

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

In `@packages/session-modal/src/main.tsx` around lines 212 - 220, saveBaseUrl
currently overwrites the whole connection object, dropping initialized auth
(username/password/token) and causing 401s; instead, read the current connection
state and call setConnection with a merged object that preserves existing auth
fields while updating baseUrl and source, and keep the localStorage key
"session-modal.base-url" and log call unchanged; update the saveBaseUrl function
to merge the existing connection (e.g., spread existing connection) with {
baseUrl: value, source: "manual" } before calling setConnection so
username/password are retained.
packages/app/src/pages/layout.tsx-280-289 (1)

280-289: ⚠️ Potential issue | 🟠 Major

Derive expandedProject from the active route as well.

groupedProjects() hides every subproject unless its parent matches store.expandedProject, but the new effects only clean that state—they never populate it from currentDir(). Refreshing or deep-linking directly into a child project can therefore leave the active project hidden in the rail until something calls navigateToProject().

Also applies to: 336-347

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

In `@packages/app/src/pages/layout.tsx` around lines 280 - 289, groupedProjects
currently only expands subprojects when store.expandedProject matches a parent,
but store.expandedProject is never derived from the route/current directory, so
deep-links can hide the active project; update the logic that computes
store.expandedProject (or compute a derivedExpanded variable inside createMemo
groupedProjects) to also consider currentDir() / the active route: derive the
parent workspace key from currentDir() (using workspaceKey(currentDir())) and
treat that as the expanded key if store.expandedProject is undefined, so
groupedProjects (and the similar block at 336-347) will include the active child
project even after refresh without requiring navigateToProject().
packages/app/src/pages/layout.tsx-1171-1180 (1)

1171-1180: ⚠️ Potential issue | 🟠 Major

Handle rejected unarchive requests in the command action.

This handler drops the promise returned by unarchiveSession(), so failures surface as unhandled rejections and the user gets no toast or recovery path. It should mirror the error handling used in archiveSession().

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

In `@packages/app/src/pages/layout.tsx` around lines 1171 - 1180, The unarchive
command handler drops the promise from unarchiveSession(), causing unhandled
rejections; update the onSelect for id "session.unarchive" to mirror
archiveSession() by retrieving const session = selectedSession(), calling
unarchiveSession(session, session.time.updated ?? session.time.created) and
awaiting it (or attaching .then/.catch), and ensure failures are caught so the
same toast/error recovery path used by archiveSession() is invoked (show error
toast and handle any rollback or state updates).
packages/app/src/components/dialog-open-project.tsx-21-25 (1)

21-25: ⚠️ Potential issue | 🟠 Major

Render the locked path flow too.

lockMode=true with mode="path" currently hides both input sections, so the dialog shows a submit button with no way to enter or browse a directory. Since the props expose that combination, the path-only variant is unusable.

Also applies to: 152-191, 193-243

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

In `@packages/app/src/components/dialog-open-project.tsx` around lines 21 - 25,
The DialogOpenProject component currently hides both input sections when
lockMode is true, making the mode="path" variant unusable; update the JSX
conditional logic so that when props.mode === "path" you still render the path
input flow even if props.lockMode is true (render it as readOnly/disabled or
show the locked path value and disable the browse button as appropriate), while
preserving the existing locked behavior for the project selection flow; locate
the conditional blocks that render the path input and project input inside
DialogOpenProject and change the conditions that check props.lockMode to allow
the path branch to render when props.mode === "path".
packages/session-modal/src/main.tsx-146-187 (1)

146-187: ⚠️ Potential issue | 🟠 Major

Isolate per-directory fetch failures.

This aggregate load uses Promise.all all the way down, so one stale/inaccessible directory can reject the entire resource and flip the whole modal into the error state. These per-directory failures should degrade to [], not take down the rest of the session list.

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

In `@packages/session-modal/src/main.tsx` around lines 146 - 187, The
per-directory fetch inside the directories.map used to build rows can reject the
whole Promise.all; wrap the async callback (the function that builds scoped via
client(value.baseUrl, auth, directory) and calls scoped.session.list,
scoped.permission.list, scoped.question.list, scoped.session.status) in a
try/catch so any error for a single directory is caught, logged, and the
callback returns an empty array for that directory ([]) instead of throwing;
keep the rest of the mapping logic (rootSession filtering, bySession grouping,
computing permissionCount/questionCount/requiresInput/status/busy/priority)
unchanged so only the failing directory degrades without flipping the entire
modal to an error state.
packages/ui/src/components/message-part.tsx-209-218 (1)

209-218: ⚠️ Potential issue | 🟠 Major

Strip Windows separators before opening project-relative files.

On Windows, relativizeProjectPath() returns values like \src\file.ts; replace(/^\//, "") leaves that leading separator intact. The new path click handlers will then pass an invalid relative path to openFilePath().

Minimal fix
-  const file = relativizeProjectPath(path, directory).replace(/^\//, "")
+  const file = relativizeProjectPath(path, directory).replace(/^[\\/]+/, "")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/message-part.tsx` around lines 209 - 218,
openProjectFile currently strips only a leading forward slash from the
relativized path which leaves a leading backslash on Windows (e.g.
"\src\file.ts") and yields an invalid path for openFilePath; update the cleanup
on the value returned by relativizeProjectPath in openProjectFile to strip any
leading slash or backslash (use a regex that matches both characters, e.g.
/^[\\/]+/) so the resulting file string is normalized before calling
openFilePath({ path: file }).
🟡 Minor comments (11)
packages/util/src/session-transcript.ts-83-85 (1)

83-85: ⚠️ Potential issue | 🟡 Minor

Don't leave a dangling separator after the last message.

The current mapping appends --- to every row, so every non-empty transcript ends with a trailing separator. Build the separators between rows instead.

✂️ Proposed fix
-  const body = rows
-    .map((row) => `${formatMessage(row.info, row.parts)}---\n`)
-    .join("\n")
-    .trimEnd()
+  const body = rows
+    .map((row) => formatMessage(row.info, row.parts).trimEnd())
+    .join("\n\n---\n\n")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/util/src/session-transcript.ts` around lines 83 - 85, The code
building the transcript body currently appends a trailing separator because it
maps each row to `${formatMessage(row.info, row.parts)}---\n`; change this so
you map rows to just `formatMessage(row.info, row.parts)` and then join the
resulting array with the separator (e.g., `'\n---\n'`) to place separators
between messages only; update the `body` assignment that uses `rows` and
`formatMessage` accordingly and keep a final newline if the original output
relied on it.
packages/opencode/src/config/config.ts-701-703 (1)

701-703: ⚠️ Potential issue | 🟡 Minor

Windows path normalization is a no-op here.

replace(/\//g, "/") never rewrites \, so nested package agent IDs keep backslashes on Windows. That will produce mixed-separator names that do not match the slash-delimited format used by the rest of the config loaders.

Also applies to: 727-728

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

In `@packages/opencode/src/config/config.ts` around lines 701 - 703, The Windows
path normalization is ineffective because replace(/\//g, "/") doesn't convert
backslashes; update the normalization of relativePath to produce forward-slash
IDs by replacing backslashes with "/" (e.g., replace(/\\+/g, "/") or use
path.posix.normalize on a split) when computing agentName and prefixedName
(referencing relativePath, agentName, prefixedName and pkg.agentDir); apply the
same fix to the other occurrence that mirrors lines 727-728 so all agent IDs use
slash-delimited paths consistently.
packages/app/src/components/dialog-select-session.tsx-201-201 (1)

201-201: ⚠️ Potential issue | 🟡 Minor

Update the Tailwind v4+ important modifier syntax.

The workspace uses Tailwind v4.1.11. In Tailwind v4+, the important modifier must be placed at the end of the class name. Change !max-h-[480px] to max-h-[480px]! on line 201.

Suggested fix
-    <Dialog class="pt-3 pb-0 !max-h-[480px]" transition>
+    <Dialog class="pt-3 pb-0 max-h-[480px]!" transition>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app/src/components/dialog-select-session.tsx` at line 201, Update
the Tailwind important modifier syntax in the Dialog JSX: replace the deprecated
prefix form "!max-h-[480px]" with the v4+ suffix form "max-h-[480px]!" in the
Dialog element inside the dialog-select-session component (the Dialog with
class="pt-3 pb-0 !max-h-[480px]" in
packages/app/src/components/dialog-select-session.tsx) so the class becomes pt-3
pb-0 max-h-[480px]! while preserving the other classes and the transition prop.
packages/desktop/src/menu.ts-72-90 (1)

72-90: ⚠️ Potential issue | 🟡 Minor

Inconsistent i18n: Some menu items use hardcoded English strings.

  • Line 73: "Search All Sessions..." should use t("desktop.menu.file.searchAllSessions") or similar
  • Line 87: "Clone Project..." should use t("desktop.menu.file.cloneProject") or similar

Other items in the same block correctly use the t() function (e.g., Lines 78, 83).

🌐 Suggested fix for i18n consistency
 await MenuItem.new({
-  text: "Search All Sessions...",
+  text: t("desktop.menu.file.searchAllSessions"),
   accelerator: "Shift+Cmd+P",
   action: () => trigger("session.search.all"),
 }),
 await MenuItem.new({
-  text: "Clone Project...",
+  text: t("desktop.menu.file.cloneProject"),
   accelerator: "Shift+Cmd+O",
   action: () => trigger("project.clone"),
 }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop/src/menu.ts` around lines 72 - 90, Replace the hardcoded
English menu labels in the MenuItem.new entries with i18n calls: change the text
for the item that calls trigger("session.search.all") to
t("desktop.menu.file.searchAllSessions") and change the text for the item that
calls trigger("project.clone") to t("desktop.menu.file.cloneProject") so all
menu items use the t() function consistently alongside the existing MenuItem.new
and trigger(...) usages.
packages/ui/src/components/markdown-copy.test.ts-33-65 (1)

33-65: ⚠️ Potential issue | 🟡 Minor

Restore mocked globals with their original descriptors in finally.

Both tests capture only the VALUES of globalThis.navigator and globalThis.ClipboardItem (lines 34–35, 69–70) and restore them at the end of the test body (lines 64–65, 90–91). If an assertion fails or writeClipboardPayload() throws, the restoration code never runs, causing mocks to leak into subsequent tests. Additionally, restoring with Object.defineProperty() and configurable: true changes the original descriptor shape. Capture the full descriptors using Object.getOwnPropertyDescriptor() and restore them in a finally block or afterEach hook.

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

In `@packages/ui/src/components/markdown-copy.test.ts` around lines 33 - 65, The
test currently replaces globalThis.navigator and globalThis.ClipboardItem by
saving only their values and restoring them at the end of the test body; change
it to capture the full property descriptors via
Object.getOwnPropertyDescriptor(globalThis, "navigator") and
Object.getOwnPropertyDescriptor(globalThis, "ClipboardItem") before mocking,
then perform the mocks, and ensure restoration happens in a finally block (or an
afterEach) that uses Object.defineProperty to restore the original descriptors
exactly so mocks cannot leak if writeClipboardPayload or assertions throw;
reference the test's FakeClipboardItem, writeClipboardPayload, and the
globalThis.navigator/globalThis.ClipboardItem mocks when making these changes.
packages/app/src/context/server.tsx-255-257 (1)

255-257: ⚠️ Potential issue | 🟡 Minor

Potential inconsistency between normalized deduplication and exact-match operations.

The deduplication logic uses normalizeWorktree for comparison, but the original (non-normalized) directory is stored. Subsequent operations (close, expand, collapse, move) use exact string matching (x.worktree === directory).

This could cause issues if paths are passed inconsistently. For example:

  1. User opens /tmp/repo/ → stored as /tmp/repo/
  2. User later tries to close /tmp/repo (no trailing slash) → exact match fails

Consider normalizing the stored value as well:

 const target = normalizeWorktree(directory)
 if (current.find((x) => normalizeWorktree(x.worktree) === target)) return
-setStore("projects", key, [{ worktree: directory, expanded: true }, ...current])
+setStore("projects", key, [{ worktree: target, expanded: true }, ...current])

Or alternatively, apply normalization in the other operations too for consistency.

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

In `@packages/app/src/context/server.tsx` around lines 255 - 257, The dedupe uses
normalizeWorktree but you store the raw directory, causing later exact-match ops
(close/expand/collapse/move) on x.worktree === directory to fail; change the
stored value to the normalized form by passing the normalized target into
setStore (i.e., store { worktree: target, expanded: true } instead of {
worktree: directory, ... }), or alternatively ensure all operations (close,
expand, collapse, move) compare using normalizeWorktree(x.worktree) ===
normalizeWorktree(directory); update references to normalizeWorktree and the
setStore("projects", key, ...) call accordingly so storage and comparisons use
the same normalized representation.
packages/app/src/pages/session/message-timeline.tsx-1-12 (1)

1-12: ⚠️ Potential issue | 🟡 Minor

Remove unused startTransition import.

startTransition is imported on line 10 but never used in the file. Remove it from the import statement.

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

In `@packages/app/src/pages/session/message-timeline.tsx` around lines 1 - 12, The
import statement in message-timeline.tsx includes an unused symbol
startTransition; remove startTransition from the named imports (the import that
currently lists For, Index, createEffect, createMemo, createSignal, on,
onCleanup, Show, startTransition, type JSX) so the file only imports the
actually used symbols.
packages/app/src/pages/directory-layout.tsx-27-54 (1)

27-54: ⚠️ Potential issue | 🟡 Minor

Hardcoded error title should use i18n.

The error toast at line 38 uses a hardcoded string "Open failed" instead of a localized string via language.t(). This is inconsistent with error handling elsewhere in the codebase.

🌐 Proposed fix for i18n consistency

Note: You'll need to add useLanguage to the component and define an appropriate translation key.

 function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
   const navigate = useNavigate()
   const sync = useSync()
   const platform = usePlatform()
+  const language = useLanguage()
   const slug = createMemo(() => base64Encode(props.directory))
   
   // ... in onOpenFilePath handler:
           showToast({
             variant: "error",
-            title: "Open failed",
+            title: language.t("common.requestFailed"),
             description,
           })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app/src/pages/directory-layout.tsx` around lines 27 - 54, Replace
the hardcoded error title in the onOpenFilePath handler with a localized string:
import and call useLanguage to get language (or use existing language prop), add
a translation key (e.g. "errors.open_failed") in your i18n files, and change the
showToast title from "Open failed" to language.t("errors.open_failed"); keep the
rest of the error handling (description extraction, showToast call, and
dispatching the "opencode:open-file-path" event) intact and ensure the new
translation key is used where platform.openPath errors are handled.
packages/ui/src/components/markdown.tsx-267-279 (1)

267-279: ⚠️ Potential issue | 🟡 Minor

Strip file-link buttons from copied HTML.

This only removes the copy button. If the selection includes a rendered file reference, the clipboard HTML still contains button.file-link plus its data-file-* attributes. Rich paste targets commonly drop buttons, so the reference disappears, and the hidden file metadata is copied out of the app.

🧹 Suggested fix
     const wrap = document.createElement("div")
     wrap.appendChild(range.cloneContents())
     for (const item of wrap.querySelectorAll('[data-slot="markdown-copy-button"]')) {
       item.remove()
     }
+    for (const item of wrap.querySelectorAll("button.file-link")) {
+      const replacement =
+        item.querySelector("code")?.cloneNode(true) ?? document.createTextNode(item.textContent ?? "")
+      item.replaceWith(replacement)
+    }

     const html = serializeMarkdownClipboardHTML(wrap.innerHTML)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/markdown.tsx` around lines 267 - 279, The
clipboard HTML still contains rendered file references and their hidden
metadata; update the selection sanitization in the block around selection, wrap,
serializeMarkdownClipboardHTML and clip.setData to remove any button.file-link
elements (e.g., wrap.querySelectorAll('button.file-link') -> remove or replace
with plain text/filename node) and strip any data-file-* attributes from
remaining elements (iterate elements with attributes starting with "data-file-"
and remove those attributes) before calling serializeMarkdownClipboardHTML and
setting clip.setData.
packages/app/src/pages/layout/helpers.ts-182-187 (1)

182-187: ⚠️ Potential issue | 🟡 Minor

Don't hardcode the all-projects label in this helper.

"All projects" is user-visible but fixed to English here, so every non-English locale will still render this group name untranslated. Return a stable id and translate it where the label is rendered instead.

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

In `@packages/app/src/pages/layout/helpers.ts` around lines 182 - 187, The helper
currently hardcodes the user-visible label "All projects" in the returned group
object (id: "all", label: "All projects"), which prevents localization; change
the helper to return a stable id (keep id: "all") and remove the user-visible
label property (or set label to undefined) so the UI layer can translate it
where rendered; update the component that consumes this helper to map id "all"
to a translated string (e.g., via t('allProjects')) instead of relying on the
helper for the label.
packages/session-modal/src/main.tsx-247-283 (1)

247-283: ⚠️ Potential issue | 🟡 Minor

The “Needs attention” panel still renders idle sessions.

The header/count uses attention(), but the list below iterates over sessions(). That makes non-attention rows appear under a heading that says they need attention.

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

In `@packages/session-modal/src/main.tsx` around lines 247 - 283, The list under
the "Needs attention" panel is rendering all sessions (sessions()) while the
header/count uses attention(), so non-attention rows appear incorrectly; change
the UI to iterate and test the attention() array instead of sessions() — update
the Show that checks sessions().length > 0 to attention().length > 0 and the For
each to use attention() (and any inner uses of item still refer to
item.requiresInput/item.busy etc.), so only attention() items are listed and the
counts match the rendered rows.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 639bb7a4-9243-487f-847e-c76b022fceff

📥 Commits

Reviewing files that changed from the base of the PR and between 5ef0ad3 and 968dc99.

⛔ Files ignored due to path filters (3)
  • bun.lock is excluded by !**/*.lock
  • packages/sdk/js/src/v2/gen/sdk.gen.ts is excluded by !**/gen/**
  • packages/sdk/js/src/v2/gen/types.gen.ts is excluded by !**/gen/**
📒 Files selected for processing (98)
  • .github/pull_request_template.md
  • package.json
  • packages/app/e2e/actions.ts
  • packages/app/e2e/projects/projects-switch.spec.ts
  • packages/app/e2e/projects/workspaces.spec.ts
  • packages/app/e2e/selectors.ts
  • packages/app/src/app.tsx
  • packages/app/src/components/dialog-edit-project.tsx
  • packages/app/src/components/dialog-open-project.helpers.test.ts
  • packages/app/src/components/dialog-open-project.helpers.ts
  • packages/app/src/components/dialog-open-project.tsx
  • packages/app/src/components/dialog-select-model-unpaid.tsx
  • packages/app/src/components/dialog-select-model.tsx
  • packages/app/src/components/dialog-select-session.tsx
  • packages/app/src/components/dialog-settings.tsx
  • packages/app/src/components/session/session-header.tsx
  • packages/app/src/components/settings-general.tsx
  • packages/app/src/components/settings-keybinds.tsx
  • packages/app/src/components/settings-models.tsx
  • packages/app/src/context/command.tsx
  • packages/app/src/context/global-sync/bootstrap.ts
  • packages/app/src/context/layout.test.ts
  • packages/app/src/context/layout.tsx
  • packages/app/src/context/local.tsx
  • packages/app/src/context/models.tsx
  • packages/app/src/context/platform.tsx
  • packages/app/src/context/server.test.ts
  • packages/app/src/context/server.tsx
  • packages/app/src/context/settings.tsx
  • packages/app/src/i18n/en.ts
  • packages/app/src/index.css
  • packages/app/src/pages/directory-layout.tsx
  • packages/app/src/pages/layout.tsx
  • packages/app/src/pages/layout/deep-links.ts
  • packages/app/src/pages/layout/helpers.test.ts
  • packages/app/src/pages/layout/helpers.ts
  • packages/app/src/pages/layout/sidebar-items.tsx
  • packages/app/src/pages/layout/sidebar-project.tsx
  • packages/app/src/pages/layout/sidebar-shell.tsx
  • packages/app/src/pages/layout/sidebar-workspace.tsx
  • packages/app/src/pages/session.tsx
  • packages/app/src/pages/session/helpers.test.ts
  • packages/app/src/pages/session/helpers.ts
  • packages/app/src/pages/session/message-timeline.tsx
  • packages/app/src/pages/session/terminal-panel.tsx
  • packages/app/src/pages/session/use-session-commands.tsx
  • packages/desktop/package.json
  • packages/desktop/scripts/predev.ts
  • packages/desktop/src-tauri/capabilities/default.json
  • packages/desktop/src-tauri/src/constants.rs
  • packages/desktop/src-tauri/src/lib.rs
  • packages/desktop/src-tauri/src/server.rs
  • packages/desktop/src-tauri/tauri.session-modal.conf.json
  • packages/desktop/src/bindings.ts
  • packages/desktop/src/i18n/en.ts
  • packages/desktop/src/index.tsx
  • packages/desktop/src/menu.ts
  • packages/opencode/script/preload.js
  • packages/opencode/src/cli/cmd/desktop.ts
  • packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
  • packages/opencode/src/config/config.ts
  • packages/opencode/src/index.ts
  • packages/opencode/src/project/project.ts
  • packages/opencode/src/server/routes/session.ts
  • packages/opencode/src/session/index.ts
  • packages/opencode/src/util/which.ts
  • packages/opencode/test/config/config.test.ts
  • packages/opencode/test/project/project.test.ts
  • packages/script/src/index.ts
  • packages/script/src/version.test.ts
  • packages/script/src/version.ts
  • packages/session-modal/.gitignore
  • packages/session-modal/README.md
  • packages/session-modal/index.html
  • packages/session-modal/package.json
  • packages/session-modal/src/desktop-bindings.ts
  • packages/session-modal/src/main.tsx
  • packages/session-modal/src/styles.css
  • packages/session-modal/tsconfig.json
  • packages/session-modal/vite.config.ts
  • packages/ui/src/components/markdown-copy.test.ts
  • packages/ui/src/components/markdown-copy.ts
  • packages/ui/src/components/markdown-file-ref.test.ts
  • packages/ui/src/components/markdown-file-ref.ts
  • packages/ui/src/components/markdown.css
  • packages/ui/src/components/markdown.tsx
  • packages/ui/src/components/message-part.css
  • packages/ui/src/components/message-part.tsx
  • packages/ui/src/components/session-turn.tsx
  • packages/ui/src/context/data.tsx
  • packages/ui/src/context/dialog.tsx
  • packages/ui/src/hooks/filter-search.test.ts
  • packages/ui/src/hooks/filter-search.ts
  • packages/ui/src/hooks/use-filtered-list.tsx
  • packages/ui/src/i18n/en.ts
  • packages/util/src/session-transcript.ts
  • packages/web/src/content/docs/keybinds.mdx
  • script/check-preload.ts

Comment on lines +339 to +358
fn clone_with_git(url: &str, target: &str) -> Result<(), String> {
tracing::info!(%url, %target, "Running git clone");
let output = Command::new("git")
.args(["clone", "--", url, target])
.output()
.map_err(|e| format!("Failed to run git clone: {e}"))?;

if output.status.success() {
tracing::info!(%target, "git clone completed");
return Ok(());
}

let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
if !stderr.is_empty() {
tracing::warn!(%url, %target, stderr = %stderr, "git clone failed");
return Err(stderr);
}

tracing::warn!(%url, %target, "git clone failed without stderr");
Err("git clone failed".to_string())
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Redact clone URLs before logging them.

These logs persist the raw repository URL. If a user clones with embedded credentials or a PAT in the URL, the secret is written to disk.

🔒 Suggested fix
 fn clone_with_git(url: &str, target: &str) -> Result<(), String> {
-    tracing::info!(%url, %target, "Running git clone");
+    let repo = repo_name(url);
+    tracing::info!(repo = %repo, %target, "Running git clone");
@@
-        tracing::warn!(%url, %target, stderr = %stderr, "git clone failed");
+        tracing::warn!(repo = %repo, %target, stderr = %stderr, "git clone failed");
@@
-    tracing::info!(%url, ?directory, "clone_git_repository requested");
+    tracing::info!(repo = %repo_name(&url), ?directory, "clone_git_repository requested");

Also applies to: 425-431

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

In `@packages/desktop/src-tauri/src/lib.rs` around lines 339 - 358, The logs in
clone_with_git leak raw repository URLs (which may contain credentials);
implement a small helper like redact_url(url: &str) -> String that strips or
masks userinfo (username:password@) and any PATs from the URL, then replace uses
of %url in tracing::info! and tracing::warn! inside clone_with_git with the
redacted value; do the same replacement in the other git helper at the second
location (the function around lines 425-431) so all git-related log calls use
redact_url(url) instead of the raw url.

@github-actions
Copy link

This pull request has been automatically closed because it was not updated to meet our contributing guidelines within the 2-hour window.

Feel free to open a new pull request that follows our guidelines.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants