Skip to content

Desktop app#387

Open
jamiepine wants to merge 2 commits intomainfrom
desktop-app
Open

Desktop app#387
jamiepine wants to merge 2 commits intomainfrom
desktop-app

Conversation

@jamiepine
Copy link
Member

@jamiepine jamiepine commented Mar 10, 2026

Summary

  • Adds Tauri 2 desktop app wrapper (desktop/) with macOS native window chrome
  • Polishes embedded OpenCode UI: hides top bar, sidebar, session title, and border radius for seamless integration
  • Cleans up worker list: single-line task text, strips [opencode] prefix, OpenCode badge, neutral gray styling throughout
  • Moves OpenCode/Transcript tab toggle into worker detail header as a subtle pill control
  • Adds desktop/.gitignore for node_modules/ and target/

Note

This PR introduces a complete Tauri 2 desktop wrapper with macOS native window integration and significant UI polish for OpenCode embedding. The desktop/ directory contains the full Tauri configuration (47 new files including assets, Swift-based macOS window chrome, and Tauri source code). Key interface changes modernize the worker list display and seamlessly integrate the embedded OpenCode experience by removing visual chrome and moving controls into the header. Updates across interface routing and styling ensure smooth integration with the desktop wrapper.

Generated for commit 5509418.

- Add Tauri 2 desktop app wrapper (desktop/)
- Hide OpenCode top bar, sidebar, session title, and main border in embedded mode
- Strip [opencode] prefix from worker task display
- Collapse worker list cards to single-line task text
- Replace interactive badge with OpenCode badge for opencode workers
- Remove badge hover effects and color variants in worker list
- Add subtle OpenCode/Transcript toggle in worker detail header
- Neutralize accent colors in worker list (gray filter pills, selection state)
- Add desktop/.gitignore for node_modules and target
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 10, 2026

Walkthrough

Adds macOS Tauri desktop support and native Swift integration, introduces a TopBar system and simplified Sidebar in the frontend, and adds a live_worker_transcripts cache with transcript persistence on worker cancellation in the backend. Changes span build scripts, native crates, UI, routing, and channel state initialization.

Changes

Cohort / File(s) Summary
Desktop build & packaging
desktop/.gitignore, desktop/src-tauri/.gitignore, desktop/src-tauri/Info.plist, desktop/src-tauri/build.rs, desktop/src-tauri/src/main.rs
Adds gitignore entries, plist icon keys, a macOS build.rs step to run xcrun actool for Assets.car, and a Tauri main.rs that initializes the app and applies macOS-specific titlebar styling.
macOS native crate & Swift bindings
desktop/src-tauri/crates/macos/Package.swift, desktop/src-tauri/crates/macos/Package.resolved, desktop/src-tauri/crates/macos/build.rs, desktop/src-tauri/crates/macos/src-swift/window.swift, desktop/src-tauri/crates/macos/src/lib.rs
Adds SwiftPM manifest/resolution, enforces macOS 11.0 deployment target in build.rs, exposes C-callable Swift functions for titlebar style and theme locking, and provides swift-rs bindings.
Frontend architecture & runtime
interface/src/components/TopBar.tsx, interface/src/components/Sidebar.tsx, interface/src/main.tsx, interface/src/router.tsx
Introduces TopBar provider/hooks and component, simplifies Sidebar (removes collapsed/activity APIs), adjusts WKWebView zoom when in Tauri, and rewires routes/layout to use TopBar + Sidebar.
Frontend pages & components
interface/src/routes/Overview.tsx, interface/src/routes/Settings.tsx, interface/src/routes/AgentWorkers.tsx, interface/src/components/...
Migrates page headers to useSetTopBar, updates AgentWorkers UI (header-based OpenCode controls, styling changes), and adapts various components to the new top-bar model.
Frontend API & styles
interface/src/api/client.ts, interface/src/ui/style/style.scss
Adds exported IS_TAURI runtime flag and minor SCSS formatting change.
Backend channel transcripts & tests
src/agent/channel.rs, src/main.rs, tests/context_dump.rs
Adds ChannelState.live_worker_transcripts (Arc<RwLock<HashMap<...>>>) initialized at channel creation, drains/persists transcripts on worker cancellation, and updates test fixtures to initialize the new field.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Desktop app' is vague and generic, using non-descriptive language that fails to convey the specific nature of the changes (Tauri desktop wrapper with macOS native window integration and UI polish). Use a more descriptive title such as 'Add Tauri 2 desktop wrapper with macOS native window chrome' or 'Introduce desktop app and polish embedded OpenCode UI' to clarify the main changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly relates to the changeset, detailing the Tauri desktop wrapper, macOS native window chrome, embedded OpenCode UI polish, worker list cleanup, and tab toggle reorganization present in the files.

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

✨ 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 desktop-app

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

❤️ Share

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

const TopBarContext = createContext<TopBarStore | null>(null);

export function TopBarProvider({ children }: { children: ReactNode }) {
const storeRef = useRef<TopBarStore>(null);
Copy link
Contributor

Choose a reason for hiding this comment

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

useRef init is null, so the ref type should allow it.

Suggested change
const storeRef = useRef<TopBarStore>(null);
const storeRef = useRef<TopBarStore | null>(null);

* The component that calls this hook "owns" the topbar content for its lifetime.
* Uses a ref + effect to avoid re-render loops.
*/
export function useSetTopBar(node: ReactNode) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: useSetTopBar is doing a side-effect during render via store.setContent(node). Worth moving this into useLayoutEffect/useEffect to avoid any weirdness with StrictMode / future concurrent rendering.

const target = e.target as HTMLElement;
if (target.closest("a, button, input, select, textarea, [role=button]")) return;
e.preventDefault();
(window as any).__TAURI_INTERNALS__.invoke("plugin:window|start_dragging");
Copy link
Contributor

Choose a reason for hiding this comment

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

invoke() returns a promise here; it's easy to end up with an unhandled rejection if start-dragging fails.

Suggested change
(window as any).__TAURI_INTERNALS__.invoke("plugin:window|start_dragging");
void (window as any).__TAURI_INTERNALS__.invoke("plugin:window|start_dragging").catch(() => {});

}
],
"security": {
"csp": null
Copy link
Contributor

Choose a reason for hiding this comment

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

Worth avoiding csp: null in prod if possible (especially with tauri-plugin-shell enabled) — even a minimal CSP helps limit blast radius if the webview ever navigates somewhere unexpected.

Also: frontendDist being a URL looks suspicious for packaged builds; is the intent for the desktop app to always point at an external server rather than bundling static assets?

"permissions": [
"core:default",
"core:window:allow-start-dragging",
"shell:allow-open"
Copy link
Contributor

Choose a reason for hiding this comment

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

shell:allow-open is pretty broad. If we only need this for the OpenCode direct link, consider scoping it down to specific schemes/hosts (e.g. http(s) to localhost/127.0.0.1) to reduce the impact of any accidental/untrusted navigation in the webview.


// Show window after setup
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
Copy link
Contributor

Choose a reason for hiding this comment

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

Ignoring the show() result here makes it harder to debug startup issues.

Suggested change
let _ = window.show();
if let Err(error) = window.show() {
tracing::warn!(%error, "failed to show main window");
}

@@ -152,10 +162,70 @@ impl ChannelState {
return Err(format!("Worker {worker_id} not found"));
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Small ordering nit: I'd abort the worker handle before draining live_worker_transcripts so we don't drop any last-moment ToolStarted/ToolCompleted events between the drain and the abort.

Suggested change
if let Some(handle) = handle {
handle.abort();
}
// Drain the live transcript after aborting so we can persist it.
// The abort kills the worker future, so persist_transcript() inside the
// worker's run() method will never execute. We compensate here.
let live_steps = self
.live_worker_transcripts
.write()
.await
.remove(&worker_id.to_string());

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (3)
interface/src/components/Sidebar.tsx (1)

29-31: Consider removing the unused liveStates prop.

The liveStates prop is declared in SidebarProps but is immediately renamed to _liveStates and never used in the component body. If activity display has been intentionally removed from the sidebar, consider removing this prop from the interface entirely to keep the API clean.

♻️ Proposed cleanup
-interface SidebarProps {
-	liveStates: Record<string, ChannelLiveState>;
-}
+interface SidebarProps {}

And update the component signature:

-export function Sidebar({ liveStates: _liveStates }: SidebarProps) {
+export function Sidebar({}: SidebarProps) {

Note: This would also require updating the call site in interface/src/router.tsx.

Also applies to: 80-80

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

In `@interface/src/components/Sidebar.tsx` around lines 29 - 31, SidebarProps
declares liveStates which the Sidebar component receives as _liveStates but
never uses; remove the unused prop to clean the API. Update the SidebarProps
interface to drop the liveStates field and change the Sidebar component
signature to remove the _liveStates parameter (reference Sidebar and
SidebarProps in interface/src/components/Sidebar.tsx), then update any call
sites (e.g., where Sidebar is instantiated in interface/src/router.tsx) to stop
passing liveStates. Run TypeScript to catch any remaining references and remove
or refactor them accordingly.
interface/src/main.tsx (1)

10-13: Consider documenting the zoom factor rationale.

The 1.1 zoom factor is a magic number. Adding a brief note about why specifically 10% zoom corrects WKWebView's rendering scale would help future maintainers understand the choice.

Also note that document.body.style.zoom is non-standard CSS, but this should be fine since it's specifically targeting WKWebView in Tauri.

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

In `@interface/src/main.tsx` around lines 10 - 13, The magic zoom value 1.1 used
in the WKWebView detection block should be documented and made explicit: add a
short comment above the if ((window as any).__TAURI_INTERNALS__) check
explaining that WKWebView renders at ~10% smaller effective scale and therefore
we apply a 1.1 zoom correction, and note that document.body.style.zoom is
non-standard but acceptable here because the adjustment is scoped to
Tauri/WKWebView; optionally centralize the value as a named constant (e.g.,
WK_WEBVIEW_ZOOM = 1.1) referenced in the document.body.style.zoom assignment and
mention why 1.1 was chosen (empirical/visual correction) so future maintainers
understand the rationale.
interface/src/components/TopBar.tsx (1)

57-64: Avoid mutating the top-bar store during render.

store.setContent(node) publishes shared state before the route commit finishes. Under interrupted/concurrent renders that can surface header content for a page that never committed, and there is no cleanup path when the owner unmounts. Move the set/clear logic into a layout effect keyed by node.

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

In `@interface/src/components/TopBar.tsx` around lines 57 - 64, useSetTopBar
currently calls store.setContent(node) during render which can publish state
prematurely; change it to call store.setContent(node) inside a useLayoutEffect
keyed on node, and perform cleanup in the effect return to clear the top bar
when the component unmounts or node changes (e.g., call store.setContent(null)
or only clear if the store still holds the same node to avoid stomping other
updates). In short: move the mutation out of the render path in useSetTopBar,
invoke store.setContent(node) in useLayoutEffect([node]) and clear it in the
cleanup, referencing the existing useSetTopBar hook and store.setContent method.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@desktop/src-tauri/build.rs`:
- Around line 14-16: The build script currently only calls
println!("cargo:rerun-if-changed={}", icon_source) inside the existence check,
so Cargo won't watch Spacebot.icon if it didn't exist initially; move the
println! that emits the rerun-if-changed for the icon path (the one using
icon_source) out of the if Path::new(&icon_source).exists() block so the path is
always printed, and keep the warning/error logging about missing file
conditional; apply the same change for the second occurrence referenced around
the other Path::new(&icon_source) check (lines ~54-55).

In `@desktop/src-tauri/crates/macos/Package.resolved`:
- Around line 7-11: The SwiftRs dependency is pinned to the stale "specta"
branch and should be switched to a stable tag; update the Package.resolved entry
that currently shows "branch": "specta" and the associated "revision" value so
that it references the released semantic version (e.g., set "version": "1.0.7")
instead of a branch, and remove or clear the "branch" and old "revision" fields
accordingly so the package now resolves to the tagged release (ensure changes
correspond to the SwiftRs dependency block so tooling will pick up the 1.0.7
tag).

In `@desktop/src-tauri/src/main.rs`:
- Line 35: The call to window.show() in setup() is currently discarding its
Result with `let _ = window.show()`; change this to handle the Result by either
propagating the error from setup() (returning a Result and using the ? operator
on window.show()) or logging the failure (e.g., use process_logger or the app
logger to record the error) so the app won't silently boot without a visible
window; update the function signature of setup() if you choose propagation and
replace the `let _ = window.show()` with either `window.show()?` or an explicit
match/if let Err(e) => log_error!("window.show failed: {:?}", e)` referencing
the window.show() call and setup() function.

In `@interface/src/components/TopBar.tsx`:
- Around line 99-109: The Link in TopBar.tsx that renders the home button
currently contains only a decorative img (alt=""), so add an accessible name to
the Link (the Link element itself) by adding an aria-label (e.g.,
aria-label="Home") or include visually-hidden text inside the Link; keep the
image alt empty if it remains decorative and ensure the attribute is added on
the Link component used in the TopBar to provide a proper accessible name for
screen readers.
- Around line 78-86: Replace the private Tauri invoke call in handleMouseDown
with the public API: import getCurrentWindow from "@tauri-apps/api/window" and
call await getCurrentWindow().startDragging() instead of (window as
any).__TAURI_INTERNALS__.invoke(...); keep the existing guards (IS_TAURI,
e.buttons check, interactive element check) but wrap the await call in try/catch
to handle and log errors (don’t swallow the promise) so startDragging errors are
surfaced.

In `@interface/src/routes/AgentWorkers.tsx`:
- Around line 334-336: The Badge was hardcoded to variant="outline", leaving the
unused helper statusBadgeVariant() and causing TS6133; either restore usage of
statusBadgeVariant(...) as the Badge's variant prop (e.g.,
variant={statusBadgeVariant(status)}) so the helper is referenced, or delete the
unused statusBadgeVariant function and any related imports; update the Badge
declaration in AgentWorkers component (the Badge JSX where variant is currently
"outline") and ensure tests/compilation pass after removing or wiring the
helper.
- Around line 320-333: Replace the literal worker.worker_type === "opencode"
check with the shared OpenCode detection helper so rows that had only the legacy
"[opencode]" prefix still show the badge; import and use the isOpenCodeWorker
helper (from useChannelLiveState) or the hook-provided result when rendering the
Badge in AgentWorkers.tsx (the conditional rendering around Badge should use
isOpenCodeWorker(worker) or the hook value instead of worker.worker_type ===
"opencode") so the badge visibility matches the canonical logic.
- Around line 487-510: The segmented control for OpenCode/Transcript (rendered
when hasOpenCodeEmbed) currently toggles via setActiveTab and uses activeTab for
styling but lacks accessibility state; update the two buttons (the ones that
call setActiveTab("opencode") and setActiveTab("transcript")) to expose the
selected state by adding appropriate ARIA attributes (e.g., aria-pressed or
role="tab" with aria-selected) tied to activeTab (e.g., aria-pressed={activeTab
=== "opencode"} for the OpenCode button and similarly for Transcript), and
optionally add aria-controls pointing to the corresponding panel IDs so
assistive tech can determine which view is active.

In `@src/agent/channel.rs`:
- Around line 165-172: Draining and removing the entry from
live_worker_transcripts before aborting the worker can drop late-arriving
ToolStarted/ToolCompleted events; instead synchronize with the worker or use a
worker-owned transcript source: have the abort path request the worker to
flush/persist its transcript (call persist_transcript from the worker's run() or
expose a flush_transcript method) and only remove the live_worker_transcripts
entry after confirmation of that flush, or acquire a stronger synchronization
point (e.g., hold a per-worker lock or await a flush-complete notification)
before calling remove(&worker_id.to_string()); reference
live_worker_transcripts, persist_transcript, run, ToolStarted/ToolCompleted, and
worker_id to locate where to add the flush/notification and move the removal to
after confirmation.

In `@src/main.rs`:
- Around line 1770-1773: The ChannelControlHandle captured a clone of state
inside Channel::new before you replaced channel.state.live_worker_transcripts,
so control-plane cancels still reference the old map; modify Channel::new to
accept the shared Arc for live_worker_transcripts as an input (or accept a
pre-built State that already contains that Arc) and use that when constructing
the Channel and its ChannelControlHandle (instead of cloning and mutating after
construction), then update call sites that create Channel (the other location
noted) to pass api_state.live_worker_transcripts so both the Channel.state and
the ChannelControlHandle reference the same Arc-backed live_worker_transcripts.

---

Nitpick comments:
In `@interface/src/components/Sidebar.tsx`:
- Around line 29-31: SidebarProps declares liveStates which the Sidebar
component receives as _liveStates but never uses; remove the unused prop to
clean the API. Update the SidebarProps interface to drop the liveStates field
and change the Sidebar component signature to remove the _liveStates parameter
(reference Sidebar and SidebarProps in interface/src/components/Sidebar.tsx),
then update any call sites (e.g., where Sidebar is instantiated in
interface/src/router.tsx) to stop passing liveStates. Run TypeScript to catch
any remaining references and remove or refactor them accordingly.

In `@interface/src/components/TopBar.tsx`:
- Around line 57-64: useSetTopBar currently calls store.setContent(node) during
render which can publish state prematurely; change it to call
store.setContent(node) inside a useLayoutEffect keyed on node, and perform
cleanup in the effect return to clear the top bar when the component unmounts or
node changes (e.g., call store.setContent(null) or only clear if the store still
holds the same node to avoid stomping other updates). In short: move the
mutation out of the render path in useSetTopBar, invoke store.setContent(node)
in useLayoutEffect([node]) and clear it in the cleanup, referencing the existing
useSetTopBar hook and store.setContent method.

In `@interface/src/main.tsx`:
- Around line 10-13: The magic zoom value 1.1 used in the WKWebView detection
block should be documented and made explicit: add a short comment above the if
((window as any).__TAURI_INTERNALS__) check explaining that WKWebView renders at
~10% smaller effective scale and therefore we apply a 1.1 zoom correction, and
note that document.body.style.zoom is non-standard but acceptable here because
the adjustment is scoped to Tauri/WKWebView; optionally centralize the value as
a named constant (e.g., WK_WEBVIEW_ZOOM = 1.1) referenced in the
document.body.style.zoom assignment and mention why 1.1 was chosen
(empirical/visual correction) so future maintainers understand the rationale.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 29bcdd84-9b88-4aed-a7c3-aeadd5e8a049

📥 Commits

Reviewing files that changed from the base of the PR and between 57dc122 and 5509418.

⛔ Files ignored due to path filters (24)
  • desktop/assets/Spacebot.icon/Assets/sd 2.png is excluded by !**/*.png, !**/*.png
  • desktop/assets/Spacebot.icon/Assets/sd.png is excluded by !**/*.png, !**/*.png
  • desktop/assets/Spacebot.icon/icon.json is excluded by !**/*.json
  • desktop/assets/exports/Spacebot-iOS-ClearDark-1024x1024@1x.png is excluded by !**/*.png, !**/*.png
  • desktop/assets/exports/Spacebot-iOS-ClearLight-1024x1024@1x.png is excluded by !**/*.png, !**/*.png
  • desktop/assets/exports/Spacebot-iOS-Dark-1024x1024@1x.png is excluded by !**/*.png, !**/*.png
  • desktop/assets/exports/Spacebot-iOS-Default-1024x1024@1x.png is excluded by !**/*.png, !**/*.png
  • desktop/assets/exports/Spacebot-iOS-TintedDark-1024x1024@1x.png is excluded by !**/*.png, !**/*.png
  • desktop/assets/exports/Spacebot-iOS-TintedLight-1024x1024@1x.png is excluded by !**/*.png, !**/*.png
  • desktop/bun.lock is excluded by !**/*.lock, !**/*.lock
  • desktop/package.json is excluded by !**/*.json
  • desktop/src-tauri/Cargo.lock is excluded by !**/*.lock, !**/*.lock
  • desktop/src-tauri/Cargo.toml is excluded by !**/*.toml
  • desktop/src-tauri/capabilities/default.json is excluded by !**/*.json
  • desktop/src-tauri/crates/macos/Cargo.lock is excluded by !**/*.lock, !**/*.lock
  • desktop/src-tauri/crates/macos/Cargo.toml is excluded by !**/*.toml
  • desktop/src-tauri/gen/schemas/acl-manifests.json is excluded by !**/gen/**, !**/*.json, !**/gen/**
  • desktop/src-tauri/gen/schemas/capabilities.json is excluded by !**/gen/**, !**/*.json, !**/gen/**
  • desktop/src-tauri/gen/schemas/desktop-schema.json is excluded by !**/gen/**, !**/*.json, !**/gen/**
  • desktop/src-tauri/gen/schemas/macOS-schema.json is excluded by !**/gen/**, !**/*.json, !**/gen/**
  • desktop/src-tauri/icons/128x128.png is excluded by !**/*.png, !**/*.png
  • desktop/src-tauri/icons/128x128@2x.png is excluded by !**/*.png, !**/*.png
  • desktop/src-tauri/icons/32x32.png is excluded by !**/*.png, !**/*.png
  • desktop/src-tauri/tauri.conf.json is excluded by !**/*.json
📒 Files selected for processing (23)
  • desktop/.gitignore
  • desktop/src-tauri/.gitignore
  • desktop/src-tauri/Info.plist
  • desktop/src-tauri/build.rs
  • desktop/src-tauri/crates/macos/Package.resolved
  • desktop/src-tauri/crates/macos/Package.swift
  • desktop/src-tauri/crates/macos/build.rs
  • desktop/src-tauri/crates/macos/src-swift/window.swift
  • desktop/src-tauri/crates/macos/src/lib.rs
  • desktop/src-tauri/icons/icon.icns
  • desktop/src-tauri/src/main.rs
  • interface/src/api/client.ts
  • interface/src/components/Sidebar.tsx
  • interface/src/components/TopBar.tsx
  • interface/src/main.tsx
  • interface/src/router.tsx
  • interface/src/routes/AgentWorkers.tsx
  • interface/src/routes/Overview.tsx
  • interface/src/routes/Settings.tsx
  • interface/src/ui/style/style.scss
  • src/agent/channel.rs
  • src/main.rs
  • tests/context_dump.rs

Comment on lines +14 to +16
if std::path::Path::new(&icon_source).exists() {
println!("cargo:rerun-if-changed={}", icon_source);

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Always emit cargo:rerun-if-changed for the icon path.

If Spacebot.icon is absent on the first build, this script only logs a warning and Cargo never watches that path. Adding the file later will not re-run the build script until some unrelated change forces a rebuild.

Suggested fix
         let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
         let icon_source = format!("{}/../assets/Spacebot.icon", manifest_dir);
         let gen_dir = format!("{}/gen/icon", manifest_dir);

+        println!("cargo:rerun-if-changed={}", icon_source);
         std::fs::create_dir_all(&gen_dir).expect("Failed to create gen/icon directory");

         if std::path::Path::new(&icon_source).exists() {
-            println!("cargo:rerun-if-changed={}", icon_source);
-
             let output = Command::new("xcrun")

Also applies to: 54-55

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

In `@desktop/src-tauri/build.rs` around lines 14 - 16, The build script currently
only calls println!("cargo:rerun-if-changed={}", icon_source) inside the
existence check, so Cargo won't watch Spacebot.icon if it didn't exist
initially; move the println! that emits the rerun-if-changed for the icon path
(the one using icon_source) out of the if Path::new(&icon_source).exists() block
so the path is always printed, and keep the warning/error logging about missing
file conditional; apply the same change for the second occurrence referenced
around the other Path::new(&icon_source) check (lines ~54-55).

Comment on lines +7 to +11
"state": {
"branch": "specta",
"revision": "e0b4a5f444a4204efa8e8270468318bc7836fcce",
"version": null
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

swift-rs latest version and specta branch status

💡 Result:

  • swift-rs latest published crate version: 1.0.7 (docs.rs “latest” points to 1.0.7). (docs.rs)
  • specta branch status (Brendonovich/swift-rs):
    • The repo has a specta branch, last updated May 8, 2023. (github.com)
    • It appears stale vs main: specta shows 126 commits, while main shows 154 commits (so specta is ~28 commits behind, assuming mostly linear history). (github.com)

Citations:


Switch from the stale specta branch to a stable, tagged release.

The SwiftRs package is pinned to the specta branch, which has been unmaintained since May 2023 and is now 28+ commits behind the main branch. A stable released version (1.0.7) is available and should be used instead. While the current revision pinning provides reproducibility, relying on an abandoned branch diverges from best practices and creates long-term maintenance risk.

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

In `@desktop/src-tauri/crates/macos/Package.resolved` around lines 7 - 11, The
SwiftRs dependency is pinned to the stale "specta" branch and should be switched
to a stable tag; update the Package.resolved entry that currently shows
"branch": "specta" and the associated "revision" value so that it references the
released semantic version (e.g., set "version": "1.0.7") instead of a branch,
and remove or clear the "branch" and old "revision" fields accordingly so the
package now resolves to the tagged release (ensure changes correspond to the
SwiftRs dependency block so tooling will pick up the 1.0.7 tag).


// Show window after setup
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
Copy link
Contributor

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 drop window.show() errors.

If show() fails here, the app can boot with no visible window and no clue why. Please log or propagate the failure from setup(). As per coding guidelines, "Don't silently discard errors. No let _ = on Results. Handle them, log them, or propagate them. The only exception is .ok() on channel sends where the receiver may be dropped"

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

In `@desktop/src-tauri/src/main.rs` at line 35, The call to window.show() in
setup() is currently discarding its Result with `let _ = window.show()`; change
this to handle the Result by either propagating the error from setup()
(returning a Result and using the ? operator on window.show()) or logging the
failure (e.g., use process_logger or the app logger to record the error) so the
app won't silently boot without a visible window; update the function signature
of setup() if you choose propagation and replace the `let _ = window.show()`
with either `window.show()?` or an explicit match/if let Err(e) =>
log_error!("window.show failed: {:?}", e)` referencing the window.show() call
and setup() function.

Comment on lines +78 to +86
const handleMouseDown = useCallback((e: MouseEvent) => {
if (!IS_TAURI) return;
// Only drag on primary button, and not when clicking interactive elements
if (e.buttons !== 1) return;
const target = e.target as HTMLElement;
if (target.closest("a, button, input, select, textarea, [role=button]")) return;
e.preventDefault();
(window as any).__TAURI_INTERNALS__.invoke("plugin:window|start_dragging");
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's explore the repository structure to understand what we're working with
git ls-files | head -20

Repository: spacedriveapp/spacebot

Length of output: 603


🏁 Script executed:

# Check if there's a Tauri configuration file
fd -name "tauri.conf.json" -o -name "Cargo.toml" -o -name "package.json" | head -10

Repository: spacedriveapp/spacebot

Length of output: 296


🏁 Script executed:

# Examine the TopBar.tsx file in context
cat -n interface/src/components/TopBar.tsx | sed -n '70,95p'

Repository: spacedriveapp/spacebot

Length of output: 1123


🏁 Script executed:

# Search for other uses of __TAURI_INTERNALS__ in the codebase to understand scope
rg "__TAURI_INTERNALS__" -n

Repository: spacedriveapp/spacebot

Length of output: 353


🏁 Script executed:

# Search for Tauri window API usage patterns
rg "window\.__TAURI__" -n -A 2

Repository: spacedriveapp/spacebot

Length of output: 48


🏁 Script executed:

# Check for any comments about this decision or error handling
rg "start_dragging\|dragging\|window.*drag" -n -B 2 -A 2

Repository: spacedriveapp/spacebot

Length of output: 48


🏁 Script executed:

# Check package.json for Tauri version
fd "package.json" -path "*/interface/*" | head -5

Repository: spacedriveapp/spacebot

Length of output: 234


🏁 Script executed:

# Look at the Tauri dependencies
cat interface/package.json 2>/dev/null | grep -A 10 -B 2 "@tauri"

Repository: spacedriveapp/spacebot

Length of output: 48


🏁 Script executed:

# Check for any imports from Tauri APIs in the file
cat interface/src/components/TopBar.tsx | grep -i "import.*tauri\|from.*tauri"

Repository: spacedriveapp/spacebot

Length of output: 117


🏁 Script executed:

# Check the main.tsx to see how Tauri is initialized
cat interface/src/main.tsx | head -50

Repository: spacedriveapp/spacebot

Length of output: 642


🏁 Script executed:

# Find package.json in the interface directory
find interface -name "package.json" -type f

Repository: spacedriveapp/spacebot

Length of output: 88


🏁 Script executed:

# Read the package.json
cat interface/package.json 2>/dev/null | grep -E "\"@tauri-apps|version" | head -20

Repository: spacedriveapp/spacebot

Length of output: 86


🏁 Script executed:

# If package.json not found in interface, search the whole repo
find . -maxdepth 3 -name "package.json" -type f | xargs grep "@tauri-apps" 2>/dev/null | head -10

Repository: spacedriveapp/spacebot

Length of output: 116


🏁 Script executed:

# Read the full interface/package.json to see all dependencies
cat interface/package.json

Repository: spacedriveapp/spacebot

Length of output: 2398


🏁 Script executed:

# Read desktop/package.json to see Tauri setup
cat desktop/package.json | head -50

Repository: spacedriveapp/spacebot

Length of output: 278


🏁 Script executed:

# Check if `@tauri-apps/api` is imported anywhere in the codebase
rg "@tauri-apps/api" -n

Repository: spacedriveapp/spacebot

Length of output: 48


🏁 Script executed:

# Look for tauri window references more broadly
rg "tauri.*window\|window.*tauri" -i

Repository: spacedriveapp/spacebot

Length of output: 48


🏁 Script executed:

# Check the full TopBar.tsx context around the drag handler
cat interface/src/components/TopBar.tsx

Repository: spacedriveapp/spacebot

Length of output: 3514


🌐 Web query:

Tauri v2 window dragging API public documentation

💡 Result:

  • JavaScript API (Tauri v2): getCurrentWindow().startDragging() is documented in the window namespace reference (method startDragging(): Promise). [1]
  • Guide (custom titlebar / drag regions): Shows using data-tauri-drag-region, and a manual alternative calling appWindow.startDragging() on mousedown. [2]
  • Rust API: tauri::window::Window::start_dragging(&self) -> Result<()> (desktop only). [3]
  • Tauri v2 permissions / capabilities: permission string core:window:allow-start-dragging (enables the start_dragging command). [4]

Sources: [1] (v2.tauri.app) [2] (v2.tauri.app) [3] (docs.rs) [4] (v2.tauri.org.cn)

Citations:


🌐 Web query:

@tauri-apps/api window module start_dragging

💡 Result:

In Tauri, the Rust API is window.start_dragging(), but the JavaScript/TypeScript API in @tauri-apps/api/window is startDragging() (camelCase).

Tauri v2 (recommended)

import { getCurrentWindow } from '@tauri-apps/api/window'

await getCurrentWindow().startDragging()

This calls the window “startDragging()” API. [1]

You also need the permission in your capability (ACL), e.g.:

{
  "permissions": ["core:window:default", "core:window:allow-start-dragging"]
}

[2]

Tauri v1

import { appWindow } from '@tauri-apps/api/window'

await appWindow.startDragging()

[3]

And enable it in tauri.conf.json allowlist (v1):

  • allowlist.window.startDragging: true [3]

Platform notes (v2 docs): Linux is listed as unsupported for startDragging(). [1]

Sources:
[1] Tauri v2 JS window API (startDragging) (v2.tauri.app)
[2] Tauri v2 window customization / permissions (core:window:allow-start-dragging) (v2.tauri.app)
[3] Tauri v1 JS window API (appWindow.startDragging) (v1.tauri.app)

Citations:


🌐 Web query:

Tauri window manager start_dragging public API alternative

💡 Result:

If you’re currently calling an internal WindowManager::start_dragging, the public alternatives are:

  • JS (frontend): appWindow.startDragging() (useful for custom titlebars / drag handles). (v1.tauri.app)
  • HTML (no JS needed): add data-tauri-drag-region to the element(s) you want to behave as a draggable region. (v2.tauri.app)
  • Rust (backend): call the window API Window::start_dragging(&self) -> Result<()>. (docs.rs)

Also make sure the feature/permission for startDragging is enabled in your Tauri config/permissions (Tauri v2 docs show it under “Permissions” for window customization). (v2.tauri.app)

Citations:


Replace with Tauri's public getCurrentWindow().startDragging() API.

The current code uses window.__TAURI_INTERNALS__.invoke("plugin:window|start_dragging"), which is a private, untyped API. Tauri v2 provides a public alternative: getCurrentWindow().startDragging() (requires importing from @tauri-apps/api/window). Additionally, the current invoke is not awaited, so errors are silently ignored—the promise should be awaited and errors handled appropriately.

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

In `@interface/src/components/TopBar.tsx` around lines 78 - 86, Replace the
private Tauri invoke call in handleMouseDown with the public API: import
getCurrentWindow from "@tauri-apps/api/window" and call await
getCurrentWindow().startDragging() instead of (window as
any).__TAURI_INTERNALS__.invoke(...); keep the existing guards (IS_TAURI,
e.buttons check, interactive element check) but wrap the await call in try/catch
to handle and log errors (don’t swallow the promise) so startDragging errors are
surfaced.

Comment on lines +99 to +109
<Link
to="/"
className="flex w-14 shrink-0 items-center justify-center border-r border-sidebar-line bg-sidebar"
>
<img
src={`${BASE_PATH}/ball.png`}
alt=""
className="h-6 w-6 transition-transform duration-150 ease-out hover:scale-110 active:scale-95"
draggable={false}
/>
</Link>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Give the home link an accessible name.

The link only contains an image with alt="", so assistive tech lands on an unlabeled interactive control. Add an aria-label on the Link or visible text.

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

In `@interface/src/components/TopBar.tsx` around lines 99 - 109, The Link in
TopBar.tsx that renders the home button currently contains only a decorative img
(alt=""), so add an accessible name to the Link (the Link element itself) by
adding an aria-label (e.g., aria-label="Home") or include visually-hidden text
inside the Link; keep the image alt empty if it remains decorative and ensure
the attribute is added on the Link component used in the TopBar to provide a
proper accessible name for screen readers.

Comment on lines +320 to +333
<div className="flex items-center justify-between gap-2">
<p className={cx("min-w-0 flex-1 truncate text-xs font-medium", selected ? "text-ink" : "text-ink-dull")}>
{worker.task.replace(/^\[opencode]\s*/, "")}
</p>
<div className="flex items-center gap-1.5">
{isInteractive && (
<div className="flex shrink-0 items-center gap-1.5 pointer-events-none">
{worker.worker_type === "opencode" ? (
<Badge variant="outline" size="sm">
OpenCode
</Badge>
) : isInteractive ? (
<Badge variant="outline" size="sm">
interactive
</Badge>
)}
) : null}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Reuse the existing OpenCode fallback here.

After Line 322 strips the [opencode] prefix, rows that only have the legacy task prefix lose the only visible OpenCode signal because this badge now keys solely off worker.worker_type === "opencode". interface/src/hooks/useChannelLiveState.ts:21-39 already has isOpenCodeWorker() for this case; reusing it here will also keep the detail surface aligned.

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

In `@interface/src/routes/AgentWorkers.tsx` around lines 320 - 333, Replace the
literal worker.worker_type === "opencode" check with the shared OpenCode
detection helper so rows that had only the legacy "[opencode]" prefix still show
the badge; import and use the isOpenCodeWorker helper (from useChannelLiveState)
or the hook-provided result when rendering the Badge in AgentWorkers.tsx (the
conditional rendering around Badge should use isOpenCodeWorker(worker) or the
hook value instead of worker.worker_type === "opencode") so the badge visibility
matches the canonical logic.

Comment on lines 334 to 336
<Badge
variant={statusBadgeVariant(displayStatus)}
variant="outline"
size="sm"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

This hardcoded badge variant leaves dead code behind and is currently breaking CI.

bunx tsc --noEmit is failing with TS6133 because statusBadgeVariant() is no longer referenced after forcing this badge to outline. Either remove the helper or keep the variant selection wired through here.

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

In `@interface/src/routes/AgentWorkers.tsx` around lines 334 - 336, The Badge was
hardcoded to variant="outline", leaving the unused helper statusBadgeVariant()
and causing TS6133; either restore usage of statusBadgeVariant(...) as the
Badge's variant prop (e.g., variant={statusBadgeVariant(status)}) so the helper
is referenced, or delete the unused statusBadgeVariant function and any related
imports; update the Badge declaration in AgentWorkers component (the Badge JSX
where variant is currently "outline") and ensure tests/compilation pass after
removing or wiring the helper.

Comment on lines +487 to +510
{hasOpenCodeEmbed && (
<div className="flex items-center gap-1 rounded-full border border-app-line/50 p-0.5">
<button
onClick={() => setActiveTab("opencode")}
className={cx(
"rounded-full px-2 py-0.5 text-tiny font-medium transition-colors",
activeTab === "opencode"
? "bg-app-hover/50 text-ink"
: "text-ink-faint hover:text-ink-dull",
)}
>
OpenCode
</button>
<button
onClick={() => setActiveTab("transcript")}
className={cx(
"rounded-full px-2 py-0.5 text-tiny font-medium transition-colors",
activeTab === "transcript"
? "bg-app-hover/50 text-ink"
: "text-ink-faint hover:text-ink-dull",
)}
>
Transcript
</button>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Expose the selected state on this OpenCode/Transcript toggle.

These buttons behave like a segmented control, but there is no aria-pressed or aria-selected signal for the active view. Keyboard users can still activate them, but assistive tech cannot tell which panel is currently shown.

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

In `@interface/src/routes/AgentWorkers.tsx` around lines 487 - 510, The segmented
control for OpenCode/Transcript (rendered when hasOpenCodeEmbed) currently
toggles via setActiveTab and uses activeTab for styling but lacks accessibility
state; update the two buttons (the ones that call setActiveTab("opencode") and
setActiveTab("transcript")) to expose the selected state by adding appropriate
ARIA attributes (e.g., aria-pressed or role="tab" with aria-selected) tied to
activeTab (e.g., aria-pressed={activeTab === "opencode"} for the OpenCode button
and similarly for Transcript), and optionally add aria-controls pointing to the
corresponding panel IDs so assistive tech can determine which view is active.

Comment on lines +165 to +172
// Drain the live transcript before aborting so we can persist it.
// The abort kills the worker future, so persist_transcript() inside the
// worker's run() method will never execute. We compensate here.
let live_steps = self
.live_worker_transcripts
.write()
.await
.remove(&worker_id.to_string());
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Draining the transcript cache before abort can lose late tool events.

live_worker_transcripts is filled asynchronously in src/api/state.rs from ToolStarted/ToolCompleted. Removing the worker entry first means any event already emitted by the worker but not yet applied to the map is dropped, so cancelled workers will intermittently persist incomplete transcripts. This needs a synchronization point or a worker-owned transcript source instead of draining the API-side cache mid-flight.

Also applies to: 178-226

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

In `@src/agent/channel.rs` around lines 165 - 172, Draining and removing the entry
from live_worker_transcripts before aborting the worker can drop late-arriving
ToolStarted/ToolCompleted events; instead synchronize with the worker or use a
worker-owned transcript source: have the abort path request the worker to
flush/persist its transcript (call persist_transcript from the worker's run() or
expose a flush_transcript method) and only remove the live_worker_transcripts
entry after confirmation of that flush, or acquire a stronger synchronization
point (e.g., hold a per-worker lock or await a flush-complete notification)
before calling remove(&worker_id.to_string()); reference
live_worker_transcripts, persist_transcript, run, ToolStarted/ToolCompleted, and
worker_id to locate where to add the flush/notification and move the removal to
after confirmation.

Comment on lines +1770 to +1773
// Share the live transcript cache so cancelled workers can
// persist their accumulated transcript before abort.
channel.state.live_worker_transcripts =
api_state.live_worker_transcripts.clone();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This post-construction swap leaves ChannelControlHandle on the old cache.

Channel::new builds control_handle from state.clone() before these assignments happen. Replacing only channel.state.live_worker_transcripts here does not update the copy already captured inside ChannelControlHandle, so control-plane cancellations still use the original empty map and won't persist cancelled-worker transcripts. Pass the shared Arc into Channel::new instead of mutating it afterwards.

Also applies to: 2062-2065

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

In `@src/main.rs` around lines 1770 - 1773, The ChannelControlHandle captured a
clone of state inside Channel::new before you replaced
channel.state.live_worker_transcripts, so control-plane cancels still reference
the old map; modify Channel::new to accept the shared Arc for
live_worker_transcripts as an input (or accept a pre-built State that already
contains that Arc) and use that when constructing the Channel and its
ChannelControlHandle (instead of cloning and mutating after construction), then
update call sites that create Channel (the other location noted) to pass
api_state.live_worker_transcripts so both the Channel.state and the
ChannelControlHandle reference the same Arc-backed live_worker_transcripts.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/agent/channel.rs (1)

165-172: ⚠️ Potential issue | 🟠 Major

Abort/drain ordering can still lose the tail of a cancelled transcript.

Line 165 removes the cache entry before Line 175 aborts the worker. Any ToolStarted/ToolCompleted event already emitted by the worker but not yet applied to live_worker_transcripts gets dropped, so the persisted snapshot can be truncated. This needs a real flush/ack from the transcript producer or a worker-owned transcript handoff instead of an optimistic remove() here.

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

In `@src/agent/channel.rs` around lines 165 - 172, Currently you remove the entry
from live_worker_transcripts via remove(&worker_id.to_string()) before aborting
the worker, which can drop in-flight ToolStarted/ToolCompleted events; instead
implement a coordinated transcript handoff/flush: have the abort path request a
flush/ack from the worker (or call a worker-owned method to transfer its
transcript) and only remove the live entry after receiving that acknowledgement,
or atomically swap the transcript ownership to the aborting controller (e.g.,
via a oneshot/handshake) and then call the abort; update the code around
live_worker_transcripts.remove(...) and the worker abort call so the removal
happens after a confirmed handoff/flush rather than optimistically before
aborting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/agent/channel.rs`:
- Around line 165-172: Currently you remove the entry from
live_worker_transcripts via remove(&worker_id.to_string()) before aborting the
worker, which can drop in-flight ToolStarted/ToolCompleted events; instead
implement a coordinated transcript handoff/flush: have the abort path request a
flush/ack from the worker (or call a worker-owned method to transfer its
transcript) and only remove the live entry after receiving that acknowledgement,
or atomically swap the transcript ownership to the aborting controller (e.g.,
via a oneshot/handshake) and then call the abort; update the code around
live_worker_transcripts.remove(...) and the worker abort call so the removal
happens after a confirmed handoff/flush rather than optimistically before
aborting.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e6aa0b33-76d9-4a72-89ab-9013096b5708

📥 Commits

Reviewing files that changed from the base of the PR and between 5509418 and 7e9e24a.

📒 Files selected for processing (3)
  • src/agent/channel.rs
  • src/main.rs
  • tests/context_dump.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main.rs

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant