Skip to content

chore: desktop release alignment + CI fixes#1

Merged
guilhermexp merged 21 commits intomainfrom
chore/desktop-release-alignment
Apr 1, 2026
Merged

chore: desktop release alignment + CI fixes#1
guilhermexp merged 21 commits intomainfrom
chore/desktop-release-alignment

Conversation

@guilhermexp
Copy link
Copy Markdown
Owner

@guilhermexp guilhermexp commented Apr 1, 2026

Summary

  • Align desktop release pipeline: matrix build for macOS (zip) + Windows (NSIS exe)
  • Fix prepare-openclaw-bundle to copy capability source dirs (resolves music-metadata missing error)
  • Add permissions: contents: write to sync-openclaw workflow
  • Split CI into verify (push/PR) and release (tags v*) jobs
  • New features: audio attachments, artifact preview, voice recording, UI palette refresh

What this enables

Push a v* tag → CI builds both macOS zip and Windows exe installer → publishes as draft GitHub Release → download on any machine.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Voice reply mode and voice-message controls (record, send, transcription).
    • Right-side artifact preview panel with Markdown/code/image/audio/pdf/video renderers and resizing.
    • Inline audio playback for TTS results and richer tool-result attachments.
  • Bug Fixes

    • Normalize stored API tokens to avoid authentication failures.
  • Improvements

    • OpenClaw runtime info & update action in settings.
    • Analytics disabled (no telemetry sent).
    • Chat enhancements: provenance receipt support, UI tweaks, auto-collapse action log.
  • Documentation

    • Updated desktop docs and a release secrets checklist.

guilhermexp and others added 15 commits March 29, 2026 12:21
This commit introduces a new section in the "Other" settings tab to
display information about the OpenClaw runtime.

It now shows:
- The current OpenClaw version fetched from the gateway.
- Whether OpenClaw updates are managed by the app (bundled runtime) or
  if direct updates are supported (dev-checkout).
- An "Update now" button is available when direct updates are supported.
  Add OpenClaw runtime information

Fetch and display the current OpenClaw version from the gateway. Show
whether OpenClaw updates are managed by the app or can be performed
directly, and provide a button to trigger updates if supported.
Add a new function `copyBundledCapabilitySourceDirs` that copies source
files for capabilities that have a `runtime-api` and do not have an
`openclaw.plugin.json` manifest. This ensures that runtime code for
capabilities is included in the final bundle.

Also, add a fallback mechanism to `collectDistSubdirPackages` to use
regex-based dependency analysis if `esbuild` fails, along with a
warning. Introduce an environment variable
`OPENCLAW_BUNDLE_SKIP_ESBUILD` to allow skipping the `esbuild` bundling
step entirely.
- Extract audioPath from tool result details in chat-utils
- Render <audio controls> inline when TTS generates audio
- Handle audio content blocks in attachments pipeline
- Add "Text to speech" label for tts tool in ActionLog
- Dark theme styling with color-scheme: dark
- Unit tests for audioPath extraction and component rendering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ndering

- ArtifactContext manages panel state, file loading via IPC bridge
- ArtifactPanel renders markdown, code, image, PDF, video, HTML, text
- ArtifactDivider enables drag-to-resize between chat and preview
- IPC readFileText handler with path security validation and 2MB limit
- Clickable file paths in read/write tool results open artifact panel
- Markdown links with file:// URLs open in artifact panel
- Responsive: panel hides below 960px viewport width
- Keyboard: Escape closes panel, slide-in animation on open

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit updates various UI elements to use a new color palette. Key
changes include:
- Updating accent colors for dividers and toggles.
- Modifying link and text colors for better contrast and consistency.
- Adjusting focus ring colors.
- Replacing deprecated color variables like `--lime` and `--info`.
This commit changes the analytics implementation in both the main and
renderer processes.
Previously, `posthog-js` and `posthog-node` were directly imported and
initialized.
Now, these libraries are only imported as dev dependencies.

The `initPosthogMain` and `initPosthogRenderer` functions no longer
initialize PostHog.
Instead, they simply set the `currentUserId`.
The `captureMain`, `optInMain`, `optOutMain`, `captureRenderer`,
`optInRenderer`, and `optOutRenderer` functions are now no-ops.
This change is intended to simplify the analytics setup and ensure that
analytics are not accidentally enabled in development or testing
environments.

Additionally, the `switchToSubscription` and `switchToSelfManaged`
thunks in `authSlice` have been updated to call `finalizeModeSwitch`.
This function now handles reloading secrets, resetting session model
selections, and reloading the configuration.
A new utility function `resetSessionModelSelection` has been introduced
to handle the session model reset logic.
This commit introduces logic to reclaim the default port if it's being
used by the global gateway launch agent on macOS in development
environments.

The `reclaimDefaultPortFromGlobalGatewayForDev` function checks if the
default port is occupied. If it is, and the application is running in
development on macOS, it attempts to unload the global gateway launch
agent to free up the port. This prevents conflicts when the application
tries to bind to its preferred port.

Additionally, this commit refactors the handling of API keys to
normalize token values by removing all whitespace. This ensures
consistency and prevents issues caused by extraneous whitespace in API
tokens.
This commit introduces several enhancements to the chat interface and
underlying functionality related to handling media and audio:

- **Audio Attachments:** The chat system now properly handles audio
  attachments, allowing for playback and display of audio files. This
  includes improvements to `UiMessageAttachment` and the
  `extractToolResult` function to recognize and process audio file
  paths.

- **Inline Media Playback:** A new `useInlineMediaSrc` hook and
  associated utilities enable the direct playback of media files (audio,
  video, images) within the chat interface using their file paths.

- **Voice Reply Mode:** The chat composer and message list have been
  updated to support a "voice reply mode." When active, this mode
  prioritizes spoken replies, suppressing plain text output if an audio
  TTS response is available for the turn. It also introduces specific UI
  elements for toggling and indicating this mode.

- **Tool Result Enhancements:** The `ToolCallCard` and `ChatMessageList`
  components have been updated to correctly display audio results from
  TTS tools and to handle image attachments derived from tool outputs.

- **OpenAI TTS Integration:**
    - The `useVoiceConfig` hook now includes settings for OpenAI TTS,
      allowing users to select a voice.
    - Configuration logic in `VoiceRecognitionTab` has been extended to
      detect and configure OpenAI TTS providers.
    - The desktop gateway now correctly injects the OpenAI API key
      environment variable when available in the state directory,
      enabling TTS generation via OpenAI.

- **Gateway and Core Functionality:**
    - Various internal gateway and utility functions have been
      refactored and updated to support the new media and voice
      features. This includes improvements to file reading IPC handlers,
      configuration migrations, and the spawning of the gateway process.
    - OpenAI API key resolution has been isolated into a dedicated
      module.
    - `MediaRecorder` usage in `useVoiceInput` has been replaced with
      `wavRecorder` for local transcription, simplifying the audio
      recording process.

These changes significantly enhance the multimedia capabilities of the
chat interface, particularly for voice interactions and rich media
content.
The voice message recording button now correctly toggles between
starting and sending recordings. The click handler has been updated to
manage this state change, and the associated test has been improved to
cover this new functionality.
The voice input hook was cancelling recording immediately upon starting,
preventing actual recording. This commit ensures that recording is not
cancelled until explicitly requested. Additionally, the mock for
`useWavRecorder` has been updated to create a new object for each call,
preventing state leakage between tests.
Introduces an `autoCollapse` prop to the `ActionLog` component. When set
to true, the action log will collapse by default and can be expanded
manually. This is primarily used in voice reply mode to keep the
interface clean.

A corresponding `useEffect` hook has been added to ensure the action log
collapses when `autoCollapse` becomes true, such as when entering voice
reply mode.
The github-actions[bot] needs explicit write permission to push
submodule pointer updates back to the repo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

Warning

Rate limit exceeded

@guilhermexp has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 20 minutes and 33 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 20 minutes and 33 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: be35e138-66f8-4780-adae-84d18278801d

📥 Commits

Reviewing files that changed from the base of the PR and between 0e6058e and 29080d3.

📒 Files selected for processing (1)
  • .github/workflows/build-desktop.yml
📝 Walkthrough

Walkthrough

Restructures desktop app layout to "OpenSpace", adds artifact preview UI and file-reader IPC, voice reply/recording with OpenAI TTS path, sanitizes tokens, removes PostHog, updates CI/release workflows and build scripts, and introduces multiple UI/IPC/main-process integrations and tests.

Changes

Cohort / File(s) Summary
CI / Workflows
/.github/workflows/build-desktop.yml, /.github/workflows/sync-openclaw.yml
Renamed workflow to "Desktop CI"; replaced mac job with verify/release matrix (mac/win), hardened installs (pnpm --frozen-lockfile, npm ci), changed packaging args, expanded artifact globs, and added contents: write permission.
Release scripts & electron hooks
desktop/scripts/..., desktop/scripts/electron-builder*.cjs, desktop/scripts/configure-github-release-secrets.sh
Moved references from apps/electron-desktopdesktop/, added script to configure GitHub release secrets, adjusted repo-root resolution and notarize hook paths, and modified DMG naming.
Prepare / bundling
desktop/scripts/prepare-*.mjs, desktop/scripts/lib/openclaw-bundle-utils.mjs, openclaw-bundle-verify.mjs
Added esbuild-skip flag, esbuild failure fallback, copyBundledCapabilitySourceDirs, and verification of runtime source presence.
Package scripts
desktop/package.json
Swapped prepare scripts to prepare:all, use --env-file-if-exists, and adjusted dist scripts to run prepare step.
Docs & README
desktop/README.md, desktop/docs/*
Renamed to OpenSpace Desktop, updated paths and quickstart, added release/secrets checklist, OpenClaw runtime notes (port 1515), PostHog disabled, and token sanitization docs.
Analytics removal (renderer & main)
desktop/renderer/src/analytics/posthog-client.ts, .../posthog-client.test.ts, desktop/src/main/analytics/posthog-main.ts, .../posthog-main.test.ts
Removed PostHog initialization and event plumbing; replaced with no-op implementations and added tests ensuring no init.
File Reader IPC & preload/bridge
desktop/src/main/ipc/file-reader.ts, desktop/src/preload.ts, desktop/src/shared/*, desktop/src/main/ipc/register.ts
New safe file-reader utilities and IPC handlers (resolveFilePath, readFileText, readFileDataUrl) with path restrictions, size limits, mime inference, and registration; preload and bridge contract extended.
Artifact system (UI + context + helpers)
desktop/renderer/src/ui/chat/components/ArtifactPanel.tsx, ArtifactDivider.tsx, artifact-preview.ts, inline-media.tsx, context/ArtifactContext.tsx, +tests & CSS
Added ArtifactProvider, panel rendering for markdown/code/image/pdf/video/audio/html/text, resizable divider, classification helpers, inline-media hook, file resolution/loading via IPC, Escape-to-close and tests.
Chat UI: voice, audio, tool results
ChatPage.tsx, ChatMessageList.tsx, ToolCallCard.tsx, AudioPlayer additions, chat-utils.ts, chat-types.ts, chatSlice.ts, chat-thunks.ts (+tests)
Integrated voice-reply mode, TTS/attachment audioPath handling, audio player rendering, attachments from filePath/dataUrl, systemProvenanceReceipt passing, and extended types and reducers to carry audio/attachments.
Voice input & transcription
useVoiceInput.ts, useVoiceConfig.ts, useChatStream.ts, desktop/src/main/whisper/ipc.ts (+tests)
Refactored to use WAV recorder, call whisperTranscribe IPC; added OpenAI transcription path (multipart upload) when model is "openai"; added mime/fileName params; useVoiceConfig supports onTranscript callback.
Other UI changes & settings
many files under desktop/renderer/src/ui/settings/* and shared styles
Added OpenAI TTS voice selection, OpenClaw runtime info + update action, mode-switch flow improvements, CSS/theme color updates (blue→gray), SVG attribute fixes, and multiple tests.
Auth/token handling & keys
desktop/src/main/keys/authProfilesStore.ts, apiKeys.ts, openai-api-key.ts (+tests)
Added normalizeTokenValue (removes all whitespace) and applied during read/write; added resolver for OpenAI TTS API key from auth-profiles and injected into gateway spawn env var.
Gateway/bootstrap dev-mode
desktop/src/main/bootstrap/dev-global-gateway.ts, app-bootstrap.ts, spawn.ts (+tests)
Added reclaimDefaultPortFromGlobalGatewayForDev to unload macOS global LaunchAgent if it blocks default port; invoked before port selection. Injected OpenAI TTS key into spawned gateway env.
Config migrations
desktop/src/main/gateway/config-migrations.ts
Added migration v5 to move/merge legacy tts into messages.tts with mergeMissing helper.
Tests & minor formatting
many new/updated tests across renderer and main
Extensive new tests for artifact features, audio/tts, file-reader handlers, bundle utils, voice flows, and modifications to existing tests; plus minor formatting/cleanup changes.
Submodule
openclaw
Updated submodule commit pointer.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Renderer (ChatPage)
    participant ArtifactContext
    participant Preload/DesktopAPI
    participant Main (FileReader IPC)
    participant Filesystem

    User->>Renderer: Click artifact link
    Renderer->>ArtifactContext: openArtifact(filePath)
    ArtifactContext->>Preload/DesktopAPI: resolveFilePath(filePath)
    Preload/DesktopAPI->>Main: IPC.resolveFilePath
    Main->>Filesystem: normalize/validate path
    Filesystem-->>Main: resolvedPath
    Main-->>Preload/DesktopAPI: { path }
    Preload/DesktopAPI-->>ArtifactContext: { path }

    alt text file
        ArtifactContext->>Preload/DesktopAPI: readFileText(path)
        Preload/DesktopAPI->>Main: IPC.readFileText
        Main->>Filesystem: stat/read with size checks
        Filesystem-->>Main: content + mime
        Main-->>Preload/DesktopAPI: { content, mimeType }
        Preload/DesktopAPI-->>ArtifactContext: { content, mimeType }
    else binary
        ArtifactContext->>ArtifactContext: set filePath (no text load)
    end

    ArtifactContext-->>Renderer: update state
    Renderer->>User: render ArtifactPanel
    User->>Renderer: Press Escape
    Renderer->>ArtifactContext: closeArtifact()
    ArtifactContext-->>Renderer: cleared state
Loading
sequenceDiagram
    participant User
    participant ChatComposer
    participant useVoiceInput
    participant Preload/DesktopAPI
    participant Main (whisper IPC)
    participant OpenAI

    User->>ChatComposer: Start voice recording
    ChatComposer->>useVoiceInput: startRecording()
    useVoiceInput->>useWavRecorder: startRecording()

    User->>ChatComposer: Stop recording
    ChatComposer->>useVoiceInput: stopRecording()
    useVoiceInput->>useWavRecorder: stop -> wav bytes
    useVoiceInput->>Preload/DesktopAPI: whisperTranscribe({audio, model, mime, fileName})
    Preload/DesktopAPI->>Main: IPC.whisperTranscribe

    alt model == "openai"
        Main->>OpenAI: POST /v1/audio/transcriptions (multipart with file)
        OpenAI-->>Main: { text }
        Main-->>Preload/DesktopAPI: { ok: true, text }
    else
        Main->>whisper-cli: spawn and parse output
        whisper-cli-->>Main: { text }
        Main-->>Preload/DesktopAPI: { ok: true, text }
    end

    Preload/DesktopAPI-->>useVoiceInput: { text }
    useVoiceInput-->>ChatComposer: onTranscript(text)
    ChatComposer->>store: sendChatMessage({ text, systemProvenanceReceipt })
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 I nibbled logs and chased a file,

swapped bright blues for softer style.
PostHog hopped off — silence sweet,
voices streaming, TTS to meet,
artifacts bloom on OpenSpace's aisle.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/desktop-release-alignment

guilhermexp and others added 2 commits April 1, 2026 12:30
Remove unused React imports (JSX transform handles it),
prefix unused params with _, clean up no-op analytics module.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 19

Caution

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

⚠️ Outside diff range comments (2)
desktop/scripts/electron-builder.afterAllArtifactBuild-notarize-dmg.cjs (1)

173-179: ⚠️ Potential issue | 🔴 Critical

The notarization script is completely missing from the repository.

The script notarize-mac-artifact.sh does not exist at openclaw/scripts/notarize-mac-artifact.sh or anywhere else in the repository. The openclaw directory exists but is empty with no scripts subdirectory. The code will throw an error at runtime (lines 175–179) because the file check will fail. This notarization functionality is broken and needs either the missing script to be created or the code to be removed if this feature is not yet implemented.

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

In `@desktop/scripts/electron-builder.afterAllArtifactBuild-notarize-dmg.cjs`
around lines 173 - 179, The code currently throws an Error when the notarization
helper is missing (the block using repoRootFromHere(), notarizeScript =
path.join(...), and fs.existsSync(notarizeScript)); either add the missing
script at openclaw/scripts/notarize-mac-artifact.sh to restore notarization, or
change this runtime check to gracefully skip notarization when the script is
absent: replace the throw with a clear warning/log (e.g., processLogger.warn or
console.warn) and exit the notarization flow early so afterAllArtifactBuild
continues without failing; update any references to notarizeScript and keep the
path.join(repoRoot, "openclaw", "scripts", "notarize-mac-artifact.sh") logic
intact so the check still detects the script if later added.
desktop/renderer/src/ui/chat/ChatPage.tsx (1)

85-92: ⚠️ Potential issue | 🟡 Minor

Remove debug console.log statement.

The console.log(behavior, "behavior") on line 86 appears to be a leftover debug statement.

🧹 Remove debug log
   const scrollToBottom = React.useCallback((behavior: ScrollBehavior = "smooth") => {
-    console.log(behavior, "behavior");
     const el = scrollRef.current;
     if (!el) {
       return;
     }
     el.scrollTo({ top: el.scrollHeight, behavior });
   }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/renderer/src/ui/chat/ChatPage.tsx` around lines 85 - 92, The function
scrollToBottom contains a leftover debug console.log call; remove the
console.log(behavior, "behavior") statement from inside scrollToBottom (which
references scrollRef.current and calls el.scrollTo) so the callback performs
only the scroll logic and preserves the existing default parameter and behavior
handling.
🧹 Nitpick comments (33)
desktop/renderer/src/ui/settings/account-models/AccountModelsTab.module.css (1)

127-134: Add a reduced-motion fallback for the loader spinner.

The infinite spin animation should respect user motion preferences.

Optional accessibility tweak
 .modeSwitchSpinner {
   width: 18px;
   height: 18px;
   border-radius: 999px;
   border: 2px solid rgba(255, 255, 255, 0.18);
   border-top-color: rgba(255, 255, 255, 0.9);
   animation: account-models-spin 0.8s linear infinite;
   flex-shrink: 0;
 }
+
+@media (prefers-reduced-motion: reduce) {
+  .modeSwitchSpinner {
+    animation: none;
+  }
+}

Also applies to: 144-152

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

In `@desktop/renderer/src/ui/settings/account-models/AccountModelsTab.module.css`
around lines 127 - 134, The spinner CSS (.modeSwitchSpinner using animation
account-models-spin) doesn’t respect reduced-motion preferences; add a
prefers-reduced-motion media query that disables or shortens the animation
(e.g., set animation: none or reduce duration) for .modeSwitchSpinner and any
other spinner classes at lines 144-152 so users who prefer reduced motion won’t
see the infinite spin. Ensure you target the same animation name
(account-models-spin) and apply the fallback inside `@media`
(prefers-reduced-motion: reduce) for consistency.
desktop/renderer/src/ui/chat/components/ChatComposer.module.css (1)

324-328: Consider consistent keyframe naming convention.

chatComposerSpin uses camelCase, which matches other keyframes in this file (micPulse, micSpin). However, Stylelint flags this as a violation of keyframes-name-pattern (expecting kebab-case). Either rename all keyframes to kebab-case for consistency with typical CSS conventions, or configure Stylelint to allow camelCase in this project.

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

In `@desktop/renderer/src/ui/chat/components/ChatComposer.module.css` around lines
324 - 328, Keyframe names use camelCase (chatComposerSpin, micPulse, micSpin)
which violates the project's keyframes-name-pattern; rename these to kebab-case
(e.g., chat-composer-spin, mic-pulse, mic-spin) and update every reference
(animation, animation-name, `@keyframes` declarations, and any shorthand uses) so
names match, or alternatively update the Stylelint config to allow camelCase if
you prefer that convention; ensure the `@keyframes` declaration for
chatComposerSpin and all uses of micPulse/micSpin are changed consistently.
desktop/src/main/gateway/config-migrations.ts (1)

244-248: Consider simplifying the return expression for clarity.

After delete cfg.tts on line 246, the expression !("tts" in cfg) is always true, making changed || !("tts" in cfg) equivalent to true. Since reaching this branch means legacy tts existed and was deleted, the config was always modified.

♻️ Simplified return statement
       const changed = mergeMissing(currentTts, legacyTts);
       delete cfg.tts;
-      return changed || !("tts" in cfg);
+      // Legacy tts was merged and deleted, so config always changed
+      return true;
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/main/gateway/config-migrations.ts` around lines 244 - 248, The
final return expression after deleting cfg.tts is redundant because once this
branch runs the legacy tts existed and was removed, so !("tts" in cfg) is always
true; replace the expression `return changed || !("tts" in cfg)` with a simple
`return true` (while keeping the mergeMissing call that uses currentTts and
legacyTts) to make intent clear and remove the dead check.
desktop/renderer/src/ui/chat/components/ToolCallCard.module.css (1)

81-97: Add explicit keyboard focus styles for new interactive controls.

These buttons currently define hover/active states but no explicit :focus-visible state.

🎯 Suggested CSS addition
+.ToolCallArgValueButton:focus-visible,
+.AudioModeButton:focus-visible,
+.AudioPlayButton:focus-visible {
+  outline: 2px solid rgba(164, 207, 126, 0.9);
+  outline-offset: 2px;
+}

Also applies to: 249-297

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

In `@desktop/renderer/src/ui/chat/components/ToolCallCard.module.css` around lines
81 - 97, The .ToolCallArgValueButton class adds interactive styling but lacks
keyboard focus styling; update the CSS to include an explicit :focus-visible
rule for .ToolCallArgValueButton (and the other matching button classes noted)
that provides a visible focus indicator—e.g., a high-contrast outline or
box-shadow, adjusted color/offset to match hover state, and preserved
keyboard-only visibility—so keyboard users can clearly see focus without
changing hover behavior.
desktop/scripts/prepare-openclaw-bundle.mjs (1)

109-119: Scope node_modules externalization to vendor dist only.

The current resolver matches any importer path containing node_modules, which is broader than the intended vendor-dist scope and can mask bundling regressions.

♻️ Suggested refactor
-function createExtensionNodeModulesExternalPlugin() {
+function createExtensionNodeModulesExternalPlugin(vendorDir) {
+  const vendorDistDir = path.join(vendorDir, "dist");
   return {
     name: "extension-node-modules-external",
     setup(build) {
       build.onResolve({ filter: /^[^.]/ }, (args) => {
         if (!args.importer) return null;
-        if (args.importer.includes("node_modules")) {
+        if (
+          args.importer.startsWith(vendorDistDir + path.sep) &&
+          args.importer.includes("node_modules")
+        ) {
           return { path: args.path, external: true };
         }
         return null;
       });
     },
   };
 }
-    const extNmPlugin = createExtensionNodeModulesExternalPlugin();
+    const extNmPlugin = createExtensionNodeModulesExternalPlugin(outDir);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/scripts/prepare-openclaw-bundle.mjs` around lines 109 - 119, The
resolver in createExtensionNodeModulesExternalPlugin currently marks imports as
external whenever the importer path contains "node_modules", which is too broad;
change the onResolve logic so it only returns { path: args.path, external: true
} when the importer is inside the vendor distribution (e.g., matches
"vendor/dist") and references node_modules—update the condition in
build.onResolve (the closure using args.importer) to check both the vendor/dist
scope (e.g., args.importer.includes("vendor/dist") or a vendor/dist regex) and
the presence of "node_modules" before marking the import external.
desktop/renderer/src/ui/settings/voice/VoiceRecognitionTab.tsx (2)

136-137: Condition may be redundant but is defensive.

The showOpenAiVoiceSection check includes both hasOpenAiTts (set from effect) and isOpenAiTtsConfigured(props.configSnap?.config). This handles the case where props.configSnap updates before the async effect runs. If this is intentional, a brief comment would clarify.

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

In `@desktop/renderer/src/ui/settings/voice/VoiceRecognitionTab.tsx` around lines
136 - 137, The condition for showOpenAiVoiceSection redundantly includes both
hasOpenAiTts and isOpenAiTtsConfigured(props.configSnap?.config) as a defensive
measure to handle props.configSnap updating before the async effect sets
hasOpenAiTts; add a brief inline comment next to the showOpenAiVoiceSection
declaration (referencing showOpenAiVoiceSection, hasOpenAiTts,
isOpenAiTtsConfigured, props.configSnap?.config, and provider) explaining this
intentional redundancy so future readers understand why both checks are present.

184-214: Dependency array is overly broad — use specific props.

Using [props] as the dependency will recreate handleOpenAiVoiceChange whenever any prop changes. Extract the specific dependencies used in the callback.

♻️ Suggested fix
   const handleOpenAiVoiceChange = React.useCallback(
     async (nextVoice: string) => {
       setOpenAiTtsVoice(nextVoice);
       setVoiceBusy(true);
       setStatus(null);
       props.onError(null);
       try {
         await props.gw.request("config.patch", {
           patch: {
             messages: {
               tts: {
                 provider: "openai",
                 providers: {
                   openai: {
                     voice: nextVoice,
                   },
                 },
               },
             },
           },
         });
         await props.reload();
         setStatus(`Voice replies now use ${nextVoice}.`);
       } catch (err) {
         props.onError(errorToMessage(err));
       } finally {
         setVoiceBusy(false);
       }
     },
-    [props]
+    [props.gw, props.reload, props.onError]
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/renderer/src/ui/settings/voice/VoiceRecognitionTab.tsx` around lines
184 - 214, The callback handleOpenAiVoiceChange has an overly broad dependency
array [props]; replace it with the specific dependencies used inside the
callback so it only re-creates when needed. Update the dependency array to
include props.gw (used for props.gw.request), props.reload, props.onError and
any local helpers referenced (e.g. errorToMessage) — you can omit React state
setters (setOpenAiTtsVoice, setVoiceBusy, setStatus) since they are stable, but
include them if your linting requires it; keep the rest of the function
unchanged.
desktop/src/main/keys/authProfilesStore.ts (1)

183-199: Consider extracting normalization to a single point.

Tokens are normalized both on read (line 92) and on write (here). While this double-normalization is safe and defensive, it adds slight complexity. If this is intentional for handling tokens that bypass the read path, a brief comment documenting this would help future maintainers.

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

In `@desktop/src/main/keys/authProfilesStore.ts` around lines 183 - 199, The code
double-normalizes tokens by calling normalizeTokenValue both when reading and
again in the sanitizedStore creation; either centralize normalization into one
place (e.g., normalize in the AuthProfilesStore write/save function or in the
single deserialization/constructor for AuthProfilesStore) and remove the
duplicate call inside the sanitizedStore mapping, or if double-normalization is
intentional to catch writes that bypass the read path, add a concise inline
comment at the sanitizedStore construction explaining that normalizeTokenValue
is applied on both read and write for defensive coverage; look for
normalizeTokenValue, sanitizedStore, and params.store.profiles to update the
normalization location or to add the explanatory comment.
desktop/renderer/src/ui/chat/components/ArtifactPanel.module.css (1)

215-224: Consider renaming keyframe to kebab-case for consistency with stylelint rules.

The keyframe artifactPanelEnter violates the keyframes-name-pattern linting rule expecting kebab-case. If your project enforces this rule, rename to artifact-panel-enter and update the reference on line 9.

♻️ Proposed fix
-@keyframes artifactPanelEnter {
+@keyframes artifact-panel-enter {
   from {
     opacity: 0;
     transform: translateX(14px);
   }
   to {
     opacity: 1;
     transform: translateX(0);
   }
 }

Also update the reference:

-  animation: artifactPanelEnter 200ms ease;
+  animation: artifact-panel-enter 200ms ease;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/renderer/src/ui/chat/components/ArtifactPanel.module.css` around
lines 215 - 224, The keyframes name artifactPanelEnter violates the kebab-case
lint rule; rename the `@keyframes` identifier to artifact-panel-enter and update
every reference that uses artifactPanelEnter (e.g., the animation property
elsewhere in the stylesheet referenced on line 9) to the new
artifact-panel-enter name so the keyframes and usage remain consistent and pass
stylelint.
desktop/renderer/src/ui/settings/OtherTab.test.tsx (2)

163-165: Use fireEvent.click() for consistency with other tests.

Line 164 uses the native button.click() method, whereas similar tests in this PR (e.g., ChatComposer.test.tsx) use fireEvent.click(). For consistency and to ensure React's synthetic event system is properly exercised, prefer the Testing Library utilities.

♻️ Suggested fix
     const button = await screen.findByRole("button", { name: "Update now" });
-    button.click();
+    fireEvent.click(button);

Add fireEvent to the imports:

-import { cleanup, render, screen, waitFor } from "@testing-library/react";
+import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/renderer/src/ui/settings/OtherTab.test.tsx` around lines 163 - 165,
Replace the native DOM call button.click() with Testing Library's
fireEvent.click(button) to exercise React's synthetic event system; update the
import list in OtherTab.test.tsx to include fireEvent (so that
screen.findByRole(...), the button variable, and fireEvent.click(button) are
used consistently with other tests like ChatComposer.test.tsx).

69-71: Redundant cleanup() call in beforeEach.

React Testing Library automatically calls cleanup() after each test when using Vitest. The explicit cleanup() call in beforeEach (Line 70) is unnecessary and redundant with the afterEach cleanup (Line 100).

♻️ Suggested simplification
 describe("OtherTab", () => {
   beforeEach(() => {
-    cleanup();
     vi.clearAllMocks();
     vi.stubGlobal("fetch", mockFetch);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/renderer/src/ui/settings/OtherTab.test.tsx` around lines 69 - 71,
Remove the redundant cleanup() call from the beforeEach block: locate the
beforeEach that currently calls cleanup() and vi.clearAllMocks(), keep
vi.clearAllMocks() to reset mocks, and rely on the existing afterEach cleanup()
already present; ensure only vi.clearAllMocks() remains in the beforeEach so
tests still reset mocks but do not call cleanup twice.
desktop/renderer/src/ui/chat/components/ToolCallCard.test.tsx (2)

16-32: Consider cleaning up window.openclawDesktop after each test.

Object.defineProperty on window.openclawDesktop persists across tests. While the current tests may work, this could cause unexpected behavior if future tests expect different mock configurations or if the global state leaks.

♻️ Suggested improvement
 afterEach(() => {
   cleanup();
+  // Reset window.openclawDesktop to avoid test pollution
+  delete (window as unknown as Record<string, unknown>).openclawDesktop;
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/renderer/src/ui/chat/components/ToolCallCard.test.tsx` around lines
16 - 32, The tests define a persistent global mock on window.openclawDesktop
inside renderWithArtifactProvider which can leak between tests; add teardown to
remove or restore that property after each test (e.g., add an afterEach that
deletes window.openclawDesktop or resets it to undefined), and ensure the mock
created in renderWithArtifactProvider (the object with
resolveFilePath/readFileText/readFileDataUrl/openExternal) is the one being
cleaned up so subsequent tests start with a clean window state.

13-13: Consider restoring the spy in afterEach.

The global spy on HTMLMediaElement.prototype.pause is set once and never restored. While Vitest typically isolates tests, explicitly restoring spies is a good practice to prevent test pollution.

♻️ Suggested improvement
+const pauseSpy = vi.spyOn(HTMLMediaElement.prototype, "pause").mockImplementation(() => {});
+
 afterEach(() => {
   cleanup();
+  pauseSpy.mockClear();
 });
-
-vi.spyOn(HTMLMediaElement.prototype, "pause").mockImplementation(() => {});

Or use mockRestore() in a global afterAll if the spy should be fully restored after all tests.

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

In `@desktop/renderer/src/ui/chat/components/ToolCallCard.test.tsx` at line 13, A
global spy on HTMLMediaElement.prototype.pause is created with vi.spyOn but
never restored, risking test pollution; update the test file to restore the spy
after tests by calling mockRestore on the spy (or restoring in an
afterEach/afterAll hook) — locate the vi.spyOn(HTMLMediaElement.prototype,
"pause") call and ensure you keep a reference to the returned spy and call
spy.mockRestore() in an afterEach (or call spy.mockRestore() in afterAll if you
intend to restore once for the whole suite).
desktop/renderer/src/ui/settings/OtherTab.tsx (1)

142-178: Consider adding AbortController and a timeout for the fetch call.

The cancelled flag prevents stale state updates, but the fetch request itself continues even after the component unmounts. Additionally, there's no timeout, so if the gateway is slow or unresponsive, the UI won't show any feedback.

♻️ Proposed improvement with AbortController and timeout
   React.useEffect(() => {
     const api = getDesktopApiOrNull();
     if (!api?.getGatewayInfo) {
       return;
     }
     let cancelled = false;
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 10000);
     void api
       .getGatewayInfo()
       .then(async (info) => {
         const gatewayState = info.state;
         if (cancelled || gatewayState?.kind !== "ready") {
           return;
         }
         const baseUrl = gatewayState.url.endsWith("/") ? gatewayState.url : `${gatewayState.url}/`;
         const bootstrapUrl = new URL("__openclaw/control-ui-config.json", baseUrl).toString();
-        const response = await fetch(bootstrapUrl);
+        const response = await fetch(bootstrapUrl, { signal: controller.signal });
         if (!response.ok) {
           throw new Error(`Failed to load OpenClaw version (${response.status})`);
         }
         // ...rest of the logic
       })
       .catch(() => {
         if (!cancelled) {
           setOpenclawVersion(null);
         }
       });
     return () => {
       cancelled = true;
+      clearTimeout(timeoutId);
+      controller.abort();
     };
   }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/renderer/src/ui/settings/OtherTab.tsx` around lines 142 - 178, The
effect using getGatewayInfo should create an AbortController and a timeout
(e.g., via setTimeout) before calling fetch and pass controller.signal into
fetch; store the timeout id and clear it when the effect cleans up, and call
controller.abort() in the cleanup (instead of only toggling cancelled) so the
network request is cancelled when the component unmounts; update the .catch
handler to ignore AbortError (do not call setOpenclawVersion on abort) and still
setOpenclawVersion(null) on other errors; reference the existing useEffect,
getGatewayInfo, fetch(bootstrapUrl), and setOpenclawVersion to locate where to
add controller, timeout and adjusted error handling.
desktop/renderer/src/ui/chat/hooks/useVoiceInput.ts (2)

131-132: Potential state inconsistency between isRecording and wavIsRecording.

The combined state isRecording || wavIsRecording could briefly be inconsistent. For example, in startRecording, local isRecording is set synchronously (Line 76) but wavIsRecording depends on the async behavior of startWavRecording. Similarly, stopRecording clears local state immediately (Line 80) before stopWavRecording completes.

While this likely won't cause user-visible issues, consider using only wavIsRecording as the source of truth or ensuring the local state mirrors the WAV recorder's state consistently.

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

In `@desktop/renderer/src/ui/chat/hooks/useVoiceInput.ts` around lines 131 - 132,
combinedIsRecording can be momentarily inconsistent because isRecording is set
synchronously in startRecording and cleared in stopRecording while
wavIsRecording follows async startWavRecording/stopWavRecording; fix by making
WAV recorder the single source of truth (remove or stop using isRecording in
combinedIsRecording) or by updating isRecording only after
startWavRecording/stopWavRecording resolves (i.e., in startRecording await
startWavRecording then set isRecording, and in stopRecording await
stopWavRecording then clear isRecording); update references to
combinedIsRecording to use wavIsRecording (and similarly consolidate error
handling to use wavError) and ensure startRecording/stopRecording functions
reflect the chosen approach.

49-49: The _gwRequest parameter is unused but properly marked with the underscore convention.

The parameter is correctly prefixed with an underscore (TypeScript convention), and the implementation confirms it's never referenced—transcription now goes through the desktop IPC API (api.whisperTranscribe) instead. While the parameter could be removed, it would be a breaking change for the three call sites in StartChatPage.tsx, ChatPage.tsx, and useVoiceConfig.ts that currently pass gw.request or gwRequest. If this parameter is kept for backward compatibility or legacy reasons, the current approach of marking it as intentionally unused is correct. If removing it is desired, update all call sites accordingly.

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

In `@desktop/renderer/src/ui/chat/hooks/useVoiceInput.ts` at line 49, Leave the
unused GatewayRequest parameter in the function signature of useVoiceInput as
_gwRequest for backward compatibility; explicitly document why it's unused by
adding a short comment above the function (e.g., "kept for legacy callers;
transcription now uses api.whisperTranscribe") so reviewers understand it's
intentional, and do not change the three callers StartChatPage.tsx,
ChatPage.tsx, and useVoiceConfig.ts now—if you instead choose to remove it,
update those three call sites to stop passing gw.request/gwRequest and then
remove the parameter from useVoiceInput.
desktop/src/main/analytics/posthog-main.ts (1)

1-19: PostHog disabled with no-op stubs — API compatibility preserved.

The implementation correctly disables PostHog while maintaining function signatures for compatibility. The underscore prefix convention for unused parameters is appropriate.

Consider removing currentUserId tracking entirely if it's no longer read anywhere, or add a comment explaining its retention (e.g., for future re-enablement or debugging).

🧹 If currentUserId is unused, simplify further
-let currentUserId: string | null = null;
-
-export function initPosthogMain(userId: string, _enabled: boolean): void {
-  currentUserId = userId;
-}
+export function initPosthogMain(_userId: string, _enabled: boolean): void {}

 export function captureMain(_event: string, _properties?: Record<string, unknown>): void {}

-export function optInMain(userId: string): void {
-  currentUserId = userId;
-}
+export function optInMain(_userId: string): void {}

-export function optOutMain(): void {
-  currentUserId = null;
-}
+export function optOutMain(): void {}

-export async function shutdownPosthogMain(): Promise<void> {
-  currentUserId = null;
-}
+export async function shutdownPosthogMain(): Promise<void> {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/main/analytics/posthog-main.ts` around lines 1 - 19, The file
contains a no-op PostHog stub that still maintains a module-level variable
currentUserId; either remove currentUserId entirely if it's not read anywhere,
or add a brief comment above its declaration explaining why it is retained
(e.g., "kept for API compatibility / future re-enablement / debugging") to avoid
confusion; update or remove assignments in initPosthogMain, optInMain,
optOutMain, and shutdownPosthogMain accordingly so they match the chosen
approach (no state kept vs. retained-for-compatibility).
desktop/renderer/src/store/slices/chat/chat-utils.ts (1)

149-164: Consider expanding MIME type coverage.

The extension-to-MIME mapping covers common formats but misses a few that users might encounter:

  • .aacaudio/aac
  • .flacaudio/flac
  • .tiff/.tifimage/tiff
  • .bmpimage/bmp

This is a nice-to-have improvement that can be deferred.

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

In `@desktop/renderer/src/store/slices/chat/chat-utils.ts` around lines 149 - 164,
The inferMimeTypeFromPath function currently misses some common extensions;
update inferMimeTypeFromPath to handle .aac (audio/aac), .flac (audio/flac),
.tiff and .tif (image/tiff), and .bmp (image/bmp) by adding corresponding
endsWith checks (e.g., normalized.endsWith(".aac") => "audio/aac") alongside the
existing extension checks so those file types return correct MIME types instead
of the default application/octet-stream.
desktop/renderer/src/ui/chat/components/ChatMessageList.audio.test.tsx (2)

12-22: Consider extracting repeated mock setup to a shared helper.

The window.openclawDesktop mock is defined identically in 5 tests. Extracting this to a beforeEach or helper function would reduce duplication and make maintenance easier.

♻️ Proposed refactor
function setupOpenclawDesktopMock() {
  Object.defineProperty(window, "openclawDesktop", {
    value: {
      readFileDataUrl: vi.fn(async (filePath: string) => ({
        dataUrl: `data:application/octet-stream;base64,${btoa(filePath)}`,
        mimeType: "application/octet-stream",
      })),
      openExternal: vi.fn(async () => {}),
    } as unknown as NonNullable<Window["openclawDesktop"]>,
    writable: true,
    configurable: true,
  });
}

// Then in each test:
it("renders inline audio player...", async () => {
  setupOpenclawDesktopMock();
  // ... rest of test
});

Also applies to: 71-81, 137-147, 203-213, 264-274

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

In `@desktop/renderer/src/ui/chat/components/ChatMessageList.audio.test.tsx`
around lines 12 - 22, Multiple tests in ChatMessageList.audio.test.tsx repeat
the same window.openclawDesktop mock; extract that mock into a shared helper
function (e.g., setupOpenclawDesktopMock) or move it into a beforeEach block so
tests call the helper instead of duplicating code. Locate the
Object.defineProperty block that sets window.openclawDesktop (the
readFileDataUrl and openExternal vi.fn definitions) and replace repeated copies
with a single exported/setup function or a beforeEach that invokes it; ensure
the helper returns or sets the same NonNullable<Window["openclawDesktop"]>
behavior so existing tests using readFileDataUrl and openExternal continue to
work.

136-200: Hardcoded absolute path in test data.

Line 173-174 uses a specific user's filesystem path (/Users/guilhermevarela/Library/Application Support/...). While functional for testing, consider using a generic path pattern to avoid confusion.

🧹 Suggested path
-                        "/Users/guilhermevarela/Library/Application Support/openspace-desktop/openclaw/media/tool-image-generation/image-1---8e376867-b20a-494c-8b12-ada5ebb3d5e1.png",
+                        "/tmp/openclaw/media/tool-image-generation/image-1---test-uuid.png",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/renderer/src/ui/chat/components/ChatMessageList.audio.test.tsx`
around lines 136 - 200, The test "renders historical generated images from tool
result file paths" hardcodes a user-specific absolute path in the tool result
attachment filePath and expectation; update the test within
ChatMessageList.audio.test.tsx to use a generic, cross-platform fixture path
(e.g., "/path/to/media/tool-image.png" or build it via path.join with __dirname)
and update the mocked readFileDataUrl return and the expect(...) btoa(...)
assertion to use that same generic variable; ensure references include the
attachment object (filePath) and the mocked
window.openclawDesktop.readFileDataUrl so both the input and expected dataUrl
remain consistent.
desktop/scripts/configure-github-release-secrets.sh (1)

19-20: Hardcoded repository default may need adjustment for forks.

REPO="${REPO:-guilhermexp/openspace}" defaults to a specific repository. For wider use or forks, consider documenting this prominently or deriving from gh repo view --json nameWithOwner -q .nameWithOwner when run from a repo clone.

🔧 Alternative: auto-detect repo
-REPO="${REPO:-guilhermexp/openspace}"
+REPO="${REPO:-$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || echo "guilhermexp/openspace")}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/scripts/configure-github-release-secrets.sh` around lines 19 - 20,
The script configure-github-release-secrets.sh currently hardcodes REPO with
REPO="${REPO:-guilhermexp/openspace}"; change this to auto-detect the repository
when REPO is unset by invoking the GitHub CLI (e.g., use gh repo view --json
nameWithOwner -q .nameWithOwner) and assign its output to REPO, falling back to
an explicit prompt or failing with a clear message if gh is unavailable; update
references to REPO accordingly and ensure the script documents that REPO can
still be overridden via environment variable.
desktop/renderer/src/ui/chat/hooks/useVoiceInput.test.ts (1)

79-85: Unused test helpers detected.

mockTrackStop (line 79) and flushMedia (lines 81-85) are defined and reset but never used in the visible test cases. If these were intended for future tests, consider adding a TODO comment; otherwise, they can be removed to reduce noise.

🧹 Proposed cleanup
-  const mockTrackStop = vi.fn();
-
-  async function flushMedia() {
-    await act(async () => {
-      await new Promise<void>((resolve) => setTimeout(resolve, 0));
-    });
-  }

And remove the reset on line 96:

-    mockTrackStop.mockReset();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/renderer/src/ui/chat/hooks/useVoiceInput.test.ts` around lines 79 -
85, The test declares unused helpers mockTrackStop and flushMedia in
useVoiceInput.test (symbols: mockTrackStop, flushMedia) which should be removed
to reduce noise; either delete their declarations and any related resets (e.g.,
the reset call that references mockTrackStop) or, if you plan to use them later,
add a TODO comment above each declaration explaining intended future use so they
are not treated as dead code during reviews. Ensure no other tests reference
these symbols before removing them.
desktop/renderer/src/ui/chat/components/ArtifactDivider.tsx (1)

23-23: Clarify the useEffect cleanup intent.

The pattern useEffect(() => stopDragging, [stopDragging]) returns stopDragging as the cleanup function, which runs on unmount and when stopDragging changes. Since stopDragging is memoized with an empty dependency array, it's stable across renders, so this effectively only runs on unmount. This is correct but subtle — a brief comment would aid readability.

📝 Suggested comment
-  React.useEffect(() => stopDragging, [stopDragging]);
+  // Cleanup drag listeners on unmount
+  React.useEffect(() => stopDragging, [stopDragging]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/renderer/src/ui/chat/components/ArtifactDivider.tsx` at line 23, Add
a brief inline comment above the React.useEffect(() => stopDragging,
[stopDragging]) call explaining that stopDragging is a memoized cleanup function
(it’s created with an empty dependency array) and that returning it here
intentionally registers it to run on unmount (and only on unmount because
stopDragging is stable); reference the useEffect and stopDragging symbols so
future readers understand the subtle cleanup intent.
desktop/renderer/src/ui/chat/components/ArtifactPanel.tsx (2)

154-163: Consider using an icon instead of "X" text for close button.

The close button uses literal "X" text (line 161). For consistency with the rest of the UI (which appears to use SVG icons), consider using an SVG close icon.

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

In `@desktop/renderer/src/ui/chat/components/ArtifactPanel.tsx` around lines 154 -
163, The close button currently renders literal "X" text which is inconsistent
with the app's SVG icon usage; replace the "X" inside the button (className
styles.ArtifactIconButton) with the project's close SVG (either an inline <svg>
or the shared CloseIcon component) while keeping the onClick handler
closeArtifact and preserving aria-label/title; ensure the SVG is accessible (use
aria-hidden="true" for decorative icon and keep the button's aria-label) and
style/sizing match other icon buttons.

65-73: Consider adding meaningful alt text for images.

The image uses an empty alt="" attribute. While the filename is available, providing it as alt text would improve accessibility.

♿ Accessibility improvement
-          <img src={fileUrl} alt={getArtifactFileName(filePath)} className={styles.ArtifactImage} />
+          <img 
+            src={fileUrl} 
+            alt={`Preview of ${getArtifactFileName(filePath)}`} 
+            className={styles.ArtifactImage} 
+          />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/renderer/src/ui/chat/components/ArtifactPanel.tsx` around lines 65 -
73, The image currently renders with an empty alt; update the img in
ArtifactPanel (when renderKind === "image") to set alt to a meaningful string
using getArtifactFileName(filePath) (e.g., alt={getArtifactFileName(filePath)})
and guard against missing names by falling back to a generic label like
"Artifact image" so the img (fileUrl, filePath, styles.ArtifactImage) always has
accessible alt text.
desktop/renderer/src/ui/chat/ChatPage.tsx (2)

355-364: Potential unnecessary re-renders from artifact panel width clamping.

This useEffect includes artifact.panelWidth and artifact.setPanelWidth in dependencies, and calls setPanelWidth inside. While the conditional check (nextPanelWidth !== artifact.panelWidth) prevents infinite loops, consider if this logic could be moved to the ArtifactContext itself or triggered only on explicit resize events to avoid running on every viewportWidth change.

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

In `@desktop/renderer/src/ui/chat/ChatPage.tsx` around lines 355 - 364, The effect
in ChatPage (React.useEffect) repeatedly runs because it depends on
artifact.panelWidth and artifact.setPanelWidth and then may call
artifact.setPanelWidth after computing clampArtifactPanelWidth using
shellRef.current.clientWidth and viewportWidth; move the clamping responsibility
into the ArtifactContext (so the context ensures any setPanelWidth is clamped
against the current container width) or change this effect to only run on
explicit resize/show events (depend only on showArtifactPanel,
viewportWidth/shellRef change or a resize listener) to avoid calling
artifact.setPanelWidth from within an effect that lists artifact.panelWidth in
its deps; update references to clampArtifactPanelWidth, artifact.panelWidth,
artifact.setPanelWidth, showArtifactPanel, viewportWidth and shellRef
accordingly.

304-350: Consider extracting voice message handling into a custom hook.

The handleVoiceMessageStop callback contains substantial logic: stopping recording, checking paywall, toggling voice mode, and sending the message. Consider extracting this into useVoiceInput or a dedicated hook for better separation of concerns and testability.

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

In `@desktop/renderer/src/ui/chat/ChatPage.tsx` around lines 304 - 350, Extract
the logic inside handleVoiceMessageStop into a custom hook (e.g., useVoiceInput)
that encapsulates stopping recording (voiceMessageInput.stopRecording), paywall
check (needsUpgradePaywall -> dispatch(upgradePaywallActions.open)), toggling
voice mode via gw.request("sessions.patch") and setVoiceReplyMode, dispatching
sendChatMessage({ request: gw.request, sessionKey, message,
systemProvenanceReceipt: VOICE_REPLY_RECEIPT }), and focusing the composer
(composerRef.current?.focusInput()); replace the inline callback with a short
wrapper that calls the hook's exposed stop handler and move all dependencies
(gw, dispatch, sessionKey, voiceMessageInput, voiceReplyMode, hasActiveStream,
sending, needsUpgradePaywall, addToastError) into the hook so the component only
wires props/state and invokes the hook's stop function.
.github/workflows/build-desktop.yml (2)

135-141: Artifact path patterns may not match Windows outputs.

The artifact upload includes *.dmg which is macOS-specific, and *.exe which is Windows-specific. This is fine since if-no-files-found: warn is set, but consider documenting this or splitting artifact uploads per OS for cleaner results.

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

In @.github/workflows/build-desktop.yml around lines 135 - 141, The artifact
path patterns currently mix OS-specific files (*.dmg, *.exe) under the same
upload step which can produce noisy warnings; update the workflow to either
document that mix or split uploads by OS: create separate upload
steps/conditions that use OS-specific patterns (e.g., one step for macOS with
*.dmg and *.zip, one for Windows with *.exe and *.zip) or keep the single step
but add a comment and retain if-no-files-found: warn; refer to the existing
patterns (*.dmg, *.exe, *.zip, *.blockmap, *.yml) and the if-no-files-found
setting when implementing the change.

12-14: Consider adding timeout-minutes to prevent runaway builds.

The verify and release jobs don't specify a timeout. Consider adding timeout-minutes to prevent indefinite hangs, especially for the release job which involves network operations (publishing).

⏱️ Suggested timeout addition
   verify:
     if: github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/v')
     runs-on: macos-latest
+    timeout-minutes: 30
     steps:

And for the release job:

   release:
     if: startsWith(github.ref, 'refs/tags/v')
+    timeout-minutes: 45
     strategy:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/build-desktop.yml around lines 12 - 14, The verify and
release workflow jobs (the verify job shown and the release job) lack a timeout
and can hang indefinitely; add a timeout-minutes setting to each job definition
(e.g., under the verify and release job blocks, add timeout-minutes: 30 or an
appropriate value) to ensure GitHub Actions cancels runaway builds, and ensure
any long-running network/publish steps in the release job have sufficient but
bounded time.
desktop/renderer/src/ui/chat/components/ChatMessageList.tsx (1)

57-92: Consider extracting the heuristics regex patterns for maintainability.

The hasStructuredVoiceTranscript function uses multiple regex patterns inline. Consider extracting these to named constants for better readability and easier maintenance/testing.

♻️ Optional refactor to extract patterns
+const STRUCTURED_PATTERNS = {
+  url: /(https?:\/\/|www\.)/i,
+  codeBlock: /```|`[^`]+`/,
+  list: /^\s*(\d+\.\s+|[-*]\s+)/m,
+  command: /^\s*(pnpm|npm|bun|git|cd|ls|cat|openclaw|curl|uv|python|node)\b/m,
+  path: /(^|\s)(~\/|\/[A-Za-z0-9._-]+\/[A-Za-z0-9._/-]*)/,
+  json: /^\s*[{[][\s\S]*[}\]]\s*$/,
+  table: /^\s*\|.+\|\s*$/m,
+};
+
 function hasStructuredVoiceTranscript(text: string): boolean {
   const trimmed = text.trim();
   if (!trimmed) {
     return false;
   }
-
-  if (/(https?:\/\/|www\.)/i.test(trimmed)) {
-    return true;
-  }
-  // ... etc
+
+  return Object.values(STRUCTURED_PATTERNS).some((pattern) => pattern.test(trimmed));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/renderer/src/ui/chat/components/ChatMessageList.tsx` around lines 57
- 92, Extract each inline regex in hasStructuredVoiceTranscript into named
constants (e.g., URL_PATTERN, CODE_FENCE_PATTERN, INLINE_CODE_PATTERN,
LIST_PATTERN, COMMAND_PATTERN, PATH_PATTERN, JSON_PATTERN, TABLE_PATTERN) or a
single STRUCTURED_PATTERNS object; keep the initial trimmed empty check, then
replace the multiple if-tests with a single check like
Object.values(STRUCTURED_PATTERNS).some(p => p.test(trimmed)); ensure flags
(e.g., /i, /m) are preserved and include both fenced and inline code patterns
and the exact command and path regexes from the original function.
desktop/src/main/ipc/file-reader.ts (2)

122-143: Minor TOCTOU window between stat and read operations.

The file size is checked via stat() before reading, but the file could theoretically be modified (grown) between these calls. For a preview feature this is a very low-risk concern, but worth noting. An alternative would be to read with a size limit using streams, though the current approach is simpler and acceptable for the use case.

Also applies to: 145-166

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

In `@desktop/src/main/ipc/file-reader.ts` around lines 122 - 143, The
readFileTextFromDisk function has a TOCTOU race between fsp.stat(resolvedPath)
and fsp.readFile(resolvedPath); replace the two-step stat+read with a single
limited read using a stream or reading from an opened file descriptor to enforce
MAX_TEXT_FILE_BYTES atomically. Specifically, in readFileTextFromDisk (and the
similar block at lines 145-166) open the file (or create a fs.createReadStream)
and read up to MAX_TEXT_FILE_BYTES+1 bytes, aborting and throwing the "File is
larger than the 2MB preview limit." error if more data is seen; keep the
existing checks using resolvePreviewFilePath and inferTextMimeType but remove
the separate stat() call so size is validated during the read.

79-95: Consider adding Windows-specific restricted paths.

The restricted paths currently cover Unix/macOS sensitive directories but not Windows equivalents. On Windows, users might have sensitive data in locations like:

  • %USERPROFILE%\.ssh
  • %APPDATA%\gnupg
  • %USERPROFILE%\.aws
Proposed enhancement
 export function isRestrictedFilePath(filePath: string): boolean {
   const resolvedPath = path.resolve(filePath);
   const homeDir = os.homedir();
   const restrictedRoots = [
     "/etc",
     "/private/etc",
     "/dev",
     "/proc",
     "/sys",
     path.join(homeDir, ".ssh"),
     path.join(homeDir, ".gnupg"),
     path.join(homeDir, ".aws"),
     path.join(homeDir, "Library", "Keychains"),
+    // Windows-specific paths (these resolve correctly on all platforms)
+    path.join(homeDir, "AppData", "Roaming", "gnupg"),
   ].map((entry) => path.resolve(entry));
 
   return restrictedRoots.some((restrictedRoot) => isPathInside(resolvedPath, restrictedRoot));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/main/ipc/file-reader.ts` around lines 79 - 95, The
isRestrictedFilePath function currently checks Unix/macOS sensitive directories
but omits Windows-specific paths; update the restrictedRoots array inside
isRestrictedFilePath to include Windows equivalents when process.platform ===
"win32" (using os.homedir(), process.env.USERPROFILE and process.env.APPDATA)
such as path.join(userProfile, ".ssh"), path.join(userProfile, ".aws"),
path.join(appData, "gnupg") (resolve each entry like the existing .map call) so
resolvedPath comparisons via isPathInside will correctly block Windows sensitive
locations.
desktop/renderer/src/store/slices/auth/authSlice.test.ts (1)

598-647: Assert that the clean session stays untouched.

Both fixtures include a clean session, but the tests never assert that it is not patched. A regression that sends sessions.patch for every session would still pass; add a negative assertion or exact patch-call count here.

Example assertion to add in both tests
expect(mockRequest).not.toHaveBeenCalledWith("sessions.patch", { key: "clean", model: null });
expect(mockRequest.mock.calls.filter(([method]) => method === "sessions.patch")).toHaveLength(3);

Also applies to: 786-835

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

In `@desktop/renderer/src/store/slices/auth/authSlice.test.ts` around lines 598 -
647, The test "reloads secrets, clears session model overrides, and refreshes
config" currently doesn't assert that the "clean" session is left untouched;
update the assertions after dispatching switchToSubscription({ request:
mockRequest }) to include a negative assertion that mockRequest was NOT called
with ("sessions.patch", { key: "clean", model: null }) and assert the exact
number of "sessions.patch" calls (e.g., filter mockRequest.mock.calls for method
=== "sessions.patch" and expect length 3) so only the intended sessions were
patched; reference mockRequest, switchToSubscription, and the
"sessions.patch"/"sessions.list" calls when adding these checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 69acd055-3c91-4bf6-be08-d11a49ed8506

📥 Commits

Reviewing files that changed from the base of the PR and between cc91da4 and 37d896a.

📒 Files selected for processing (126)
  • .github/workflows/build-desktop.yml
  • .github/workflows/sync-openclaw.yml
  • desktop/README.md
  • desktop/docs/gateway-message-metadata.md
  • desktop/docs/plans/2026-03-31-desktop-openclaw-current-state.md
  • desktop/docs/release-secrets-checklist.md
  • desktop/docs/telegram-manual-setup-electron.md
  • desktop/package.json
  • desktop/renderer/index.html
  • desktop/renderer/src/analytics/posthog-client.test.ts
  • desktop/renderer/src/analytics/posthog-client.ts
  • desktop/renderer/src/gateway/client.ts
  • desktop/renderer/src/store/slices/auth/authSlice.test.ts
  • desktop/renderer/src/store/slices/auth/authSlice.ts
  • desktop/renderer/src/store/slices/chat/chat-thunks.ts
  • desktop/renderer/src/store/slices/chat/chat-types.ts
  • desktop/renderer/src/store/slices/chat/chat-utils.test.ts
  • desktop/renderer/src/store/slices/chat/chat-utils.ts
  • desktop/renderer/src/store/slices/chat/chatSlice.test.ts
  • desktop/renderer/src/store/slices/chat/chatSlice.ts
  • desktop/renderer/src/store/slices/session-model-reset.ts
  • desktop/renderer/src/ui/chat/ChatPage.tsx
  • desktop/renderer/src/ui/chat/ChatTranscript.module.css
  • desktop/renderer/src/ui/chat/components/ActionLog.test.tsx
  • desktop/renderer/src/ui/chat/components/ActionLog.tsx
  • desktop/renderer/src/ui/chat/components/ArtifactDivider.tsx
  • desktop/renderer/src/ui/chat/components/ArtifactPanel.module.css
  • desktop/renderer/src/ui/chat/components/ArtifactPanel.test.tsx
  • desktop/renderer/src/ui/chat/components/ArtifactPanel.tsx
  • desktop/renderer/src/ui/chat/components/ChatComposer.module.css
  • desktop/renderer/src/ui/chat/components/ChatComposer.test.tsx
  • desktop/renderer/src/ui/chat/components/ChatComposer.tsx
  • desktop/renderer/src/ui/chat/components/ChatMessageList.audio.test.tsx
  • desktop/renderer/src/ui/chat/components/ChatMessageList.tsx
  • desktop/renderer/src/ui/chat/components/MessageMeta.test.tsx
  • desktop/renderer/src/ui/chat/components/ToolCallCard.module.css
  • desktop/renderer/src/ui/chat/components/ToolCallCard.test.tsx
  • desktop/renderer/src/ui/chat/components/ToolCallCard.tsx
  • desktop/renderer/src/ui/chat/components/artifact-preview.ts
  • desktop/renderer/src/ui/chat/components/inline-media.tsx
  • desktop/renderer/src/ui/chat/context/ArtifactContext.test.tsx
  • desktop/renderer/src/ui/chat/context/ArtifactContext.tsx
  • desktop/renderer/src/ui/chat/hooks/useChatStream.test.tsx
  • desktop/renderer/src/ui/chat/hooks/useChatStream.ts
  • desktop/renderer/src/ui/chat/hooks/useMarkdownComponents.tsx
  • desktop/renderer/src/ui/chat/hooks/useVoiceConfig.test.tsx
  • desktop/renderer/src/ui/chat/hooks/useVoiceConfig.ts
  • desktop/renderer/src/ui/chat/hooks/useVoiceInput.test.ts
  • desktop/renderer/src/ui/chat/hooks/useVoiceInput.ts
  • desktop/renderer/src/ui/onboarding/RestoreFilePage.module.css
  • desktop/renderer/src/ui/settings/OtherTab.test.tsx
  • desktop/renderer/src/ui/settings/OtherTab.tsx
  • desktop/renderer/src/ui/settings/RestoreBackupModal.module.css
  • desktop/renderer/src/ui/settings/account-models/AccountModelsTab.module.css
  • desktop/renderer/src/ui/settings/account-models/AccountModelsTab.test.tsx
  • desktop/renderer/src/ui/settings/account-models/AccountModelsTab.tsx
  • desktop/renderer/src/ui/settings/account-models/InlineApiKey.tsx
  • desktop/renderer/src/ui/settings/account/AccountTab.module.css
  • desktop/renderer/src/ui/settings/connectors/ConnectorsTab.tsx
  • desktop/renderer/src/ui/settings/providers/useModelProvidersState.ts
  • desktop/renderer/src/ui/settings/settings-visual-standardization.test.ts
  • desktop/renderer/src/ui/settings/skills/CustomSkillUpload.module.css
  • desktop/renderer/src/ui/settings/skills/SkillsGrid.tsx
  • desktop/renderer/src/ui/settings/voice/VoiceRecognitionTab.module.css
  • desktop/renderer/src/ui/settings/voice/VoiceRecognitionTab.test.tsx
  • desktop/renderer/src/ui/settings/voice/VoiceRecognitionTab.tsx
  • desktop/renderer/src/ui/shared/billing/AutoTopUpControl.module.css
  • desktop/renderer/src/ui/shared/kit/icons.tsx
  • desktop/renderer/src/ui/shared/models/modelPresentation.ts
  • desktop/renderer/src/ui/sidebar/session-sidebar-activity.test.tsx
  • desktop/renderer/src/ui/sidebar/useSessionActivity.test.ts
  • desktop/renderer/src/ui/styles/base.css
  • desktop/renderer/src/ui/styles/providers-shared.css
  • desktop/scripts/build-dmg-from-app.sh
  • desktop/scripts/build-memo-runtime.mjs
  • desktop/scripts/configure-github-release-secrets.sh
  • desktop/scripts/electron-builder.afterAllArtifactBuild-notarize-dmg.cjs
  • desktop/scripts/electron-builder.afterSign-notarize.cjs
  • desktop/scripts/lib/openclaw-bundle-utils.mjs
  • desktop/scripts/lib/openclaw-bundle-verify.mjs
  • desktop/scripts/prepare-gh-runtime.mjs
  • desktop/scripts/prepare-gog-credentials.mjs
  • desktop/scripts/prepare-gog-runtime.mjs
  • desktop/scripts/prepare-jq-runtime.mjs
  • desktop/scripts/prepare-memo-runtime.mjs
  • desktop/scripts/prepare-obsidian-cli-runtime.mjs
  • desktop/scripts/prepare-openclaw-bundle.mjs
  • desktop/scripts/prepare-remindctl-runtime.mjs
  • desktop/scripts/prepare-whisper-cli-runtime.mjs
  • desktop/scripts/release.sh
  • desktop/src/main/analytics/posthog-main.test.ts
  • desktop/src/main/analytics/posthog-main.ts
  • desktop/src/main/bootstrap/app-bootstrap.test.ts
  • desktop/src/main/bootstrap/app-bootstrap.ts
  • desktop/src/main/bootstrap/dev-global-gateway.test.ts
  • desktop/src/main/bootstrap/dev-global-gateway.ts
  • desktop/src/main/gateway/config-migrations.test.ts
  • desktop/src/main/gateway/config-migrations.ts
  • desktop/src/main/gateway/extra-models.test.ts
  • desktop/src/main/gateway/spawn.test.ts
  • desktop/src/main/gateway/spawn.ts
  • desktop/src/main/gog/ipc.ts
  • desktop/src/main/ipc/config-ipc.ts
  • desktop/src/main/ipc/contracts.test.ts
  • desktop/src/main/ipc/file-reader.test.ts
  • desktop/src/main/ipc/file-reader.ts
  • desktop/src/main/ipc/gh-ipc.ts
  • desktop/src/main/ipc/memo-ipc.ts
  • desktop/src/main/ipc/obsidian-ipc.ts
  • desktop/src/main/ipc/register.ts
  • desktop/src/main/ipc/remindctl-ipc.ts
  • desktop/src/main/ipc/types.ts
  • desktop/src/main/keys/apiKeys.ts
  • desktop/src/main/keys/authProfilesStore.test.ts
  • desktop/src/main/keys/authProfilesStore.ts
  • desktop/src/main/keys/openai-api-key.test.ts
  • desktop/src/main/keys/openai-api-key.ts
  • desktop/src/main/openclaw/bundle-capability-sources.test.ts
  • desktop/src/main/openclaw/paths.ts
  • desktop/src/main/whisper/ipc.test.ts
  • desktop/src/main/whisper/ipc.ts
  • desktop/src/main/window/mainWindow.ts
  • desktop/src/preload.ts
  • desktop/src/shared/desktop-bridge-contract.ts
  • desktop/src/shared/ipc-channels.ts
  • openclaw
💤 Files with no reviewable changes (1)
  • desktop/renderer/src/ui/sidebar/session-sidebar-activity.test.tsx

Comment on lines 189 to 191
```
Atomic Bot 13:40 ↑4k ↓371 R26k W212 claude-opus-4-6
OpenSpace 13:40 ↑4k ↓371 R26k W212 claude-opus-4-6
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add language specifier to fenced code block.

The code block at line 189 is missing a language specifier. Since it shows rendered UI output text, consider using text or plaintext.

📝 Suggested fix
-```
+```text
 OpenSpace  13:40  ↑4k  ↓371  R26k  W212  claude-opus-4-6
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

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

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 189-189: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

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

In `@desktop/docs/gateway-message-metadata.md` around lines 189 - 191, The fenced
code block containing the UI output line "OpenSpace  13:40  ↑4k  ↓371  R26k 
W212  claude-opus-4-6" is missing a language specifier; update that fence (the
triple-backtick block around that string) to include a language like text or
plaintext (e.g., change ``` to ```text) so the block is treated as plain text in
rendered docs.

Comment on lines +3 to +8
Este guia cobre os valores esperados pela pipeline de release do Electron em:

- [.github/workflows/build-desktop.yml](/Users/guilhermevarela/Documents/Projetos/openspace/.github/workflows/build-desktop.yml)
- [desktop/package.json](/Users/guilhermevarela/Documents/Projetos/openspace/desktop/package.json)
- [desktop/scripts/electron-builder.afterSign-notarize.cjs](/Users/guilhermevarela/Documents/Projetos/openspace/desktop/scripts/electron-builder.afterSign-notarize.cjs)
- [desktop/scripts/electron-builder.afterAllArtifactBuild-notarize-dmg.cjs](/Users/guilhermevarela/Documents/Projetos/openspace/desktop/scripts/electron-builder.afterAllArtifactBuild-notarize-dmg.cjs)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Replace absolute paths with relative paths.

The absolute paths (e.g., /Users/guilhermevarela/Documents/Projetos/openspace/...) are specific to your local machine and won't work for other contributors. Use relative paths from the repository root instead.

📝 Suggested fix
 Este guia cobre os valores esperados pela pipeline de release do Electron em:
 
-- [.github/workflows/build-desktop.yml](/Users/guilhermevarela/Documents/Projetos/openspace/.github/workflows/build-desktop.yml)
-- [desktop/package.json](/Users/guilhermevarela/Documents/Projetos/openspace/desktop/package.json)
-- [desktop/scripts/electron-builder.afterSign-notarize.cjs](/Users/guilhermevarela/Documents/Projetos/openspace/desktop/scripts/electron-builder.afterSign-notarize.cjs)
-- [desktop/scripts/electron-builder.afterAllArtifactBuild-notarize-dmg.cjs](/Users/guilhermevarela/Documents/Projetos/openspace/desktop/scripts/electron-builder.afterAllArtifactBuild-notarize-dmg.cjs)
+- [.github/workflows/build-desktop.yml](/.github/workflows/build-desktop.yml)
+- [desktop/package.json](/desktop/package.json)
+- [desktop/scripts/electron-builder.afterSign-notarize.cjs](/desktop/scripts/electron-builder.afterSign-notarize.cjs)
+- [desktop/scripts/electron-builder.afterAllArtifactBuild-notarize-dmg.cjs](/desktop/scripts/electron-builder.afterAllArtifactBuild-notarize-dmg.cjs)
📝 Committable suggestion

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

Suggested change
Este guia cobre os valores esperados pela pipeline de release do Electron em:
- [.github/workflows/build-desktop.yml](/Users/guilhermevarela/Documents/Projetos/openspace/.github/workflows/build-desktop.yml)
- [desktop/package.json](/Users/guilhermevarela/Documents/Projetos/openspace/desktop/package.json)
- [desktop/scripts/electron-builder.afterSign-notarize.cjs](/Users/guilhermevarela/Documents/Projetos/openspace/desktop/scripts/electron-builder.afterSign-notarize.cjs)
- [desktop/scripts/electron-builder.afterAllArtifactBuild-notarize-dmg.cjs](/Users/guilhermevarela/Documents/Projetos/openspace/desktop/scripts/electron-builder.afterAllArtifactBuild-notarize-dmg.cjs)
Este guia cobre os valores esperados pela pipeline de release do Electron em:
- [.github/workflows/build-desktop.yml](/.github/workflows/build-desktop.yml)
- [desktop/package.json](/desktop/package.json)
- [desktop/scripts/electron-builder.afterSign-notarize.cjs](/desktop/scripts/electron-builder.afterSign-notarize.cjs)
- [desktop/scripts/electron-builder.afterAllArtifactBuild-notarize-dmg.cjs](/desktop/scripts/electron-builder.afterAllArtifactBuild-notarize-dmg.cjs)
🧰 Tools
🪛 LanguageTool

[grammar] ~3-~3: Possível erro de concordância.
Context: ...t Este guia cobre os valores esperados pela pipeline de release do Electron em: - [.github/...

(GENERAL_GENDER_AGREEMENT_ERRORS)


[locale-violation] ~6-~6: “package” é um estrangeirismo. É preferível dizer “embalagem” ou “pacote”.
Context: ...workflows/build-desktop.yml) - [desktop/package.json](/Users/guilhermevarela/Documents/...

(PT_BARBARISMS_REPLACE_PACKAGE)

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

In `@desktop/docs/release-secrets-checklist.md` around lines 3 - 8, The
documentation uses absolute local file paths in the link list (e.g., entries
referencing /Users/guilhermevarela/... for build-desktop.yml,
desktop/package.json, desktop/scripts/electron-builder.afterSign-notarize.cjs,
and desktop/scripts/electron-builder.afterAllArtifactBuild-notarize-dmg.cjs);
update these to repository-relative paths (for example use
.github/workflows/build-desktop.yml, desktop/package.json, and the two
desktop/scripts/*.cjs files) so links resolve for all contributors—edit the
lines in release-secrets-checklist.md that list those files and replace each
absolute path with the corresponding relative path from the repo root.

Comment on lines +215 to +218
Script:

- [configure-github-release-secrets.sh](/Users/guilhermevarela/Documents/Projetos/openspace/desktop/scripts/configure-github-release-secrets.sh)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Replace absolute path with relative path.

Same issue as above — the script reference uses an absolute path.

📝 Suggested fix
 Script:
 
-- [configure-github-release-secrets.sh](/Users/guilhermevarela/Documents/Projetos/openspace/desktop/scripts/configure-github-release-secrets.sh)
+- [configure-github-release-secrets.sh](/desktop/scripts/configure-github-release-secrets.sh)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/docs/release-secrets-checklist.md` around lines 215 - 218, The
checklist uses an absolute filesystem path for the script reference; replace the
absolute path with a project-relative path (e.g.,
scripts/configure-github-release-secrets.sh) so the documentation is portable.
Update the entry that currently shows
"/Users/guilhermevarela/.../configure-github-release-secrets.sh" to the relative
path "scripts/configure-github-release-secrets.sh" and verify the link target
matches the repository file name (configure-github-release-secrets.sh).

"fetch:whisper-cli": "node scripts/fetch-whisper-cli-runtime.mjs",
"build:memo": "node scripts/build-memo-runtime.mjs",
"prepare:gog:credentials": "node --env-file=.env scripts/prepare-gog-credentials.mjs",
"prepare:gog:credentials": "node --env-file-if-exists=.env scripts/prepare-gog-credentials.mjs",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package scripts using --env-file-if-exists =="
rg -n -- '--env-file-if-exists' --glob '**/package.json'

echo "== declared Node engine constraints =="
rg -nP '"engines"\s*:|"node"\s*:' --glob '**/package.json'

echo "== GitHub workflow Node versions =="
if [ -d .github/workflows ]; then
  fd -t f -e yml -e yaml . .github/workflows | while read -r wf; do
    echo "## $wf"
    rg -n 'actions/setup-node|node-version' "$wf" || true
  done
fi

Repository: guilhermexp/openspace

Length of output: 233


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package scripts using --env-file-if-exists =="
rg -n -- '--env-file-if-exists' -g '*.json' || echo "No matches found"

echo ""
echo "== declared Node engine constraints =="
find . -name 'package.json' -type f | head -20 | while read f; do
  echo "File: $f"
  rg -P '"engines"\s*:|"node"\s*:' "$f" || true
done

echo ""
echo "== GitHub workflow Node versions =="
if [ -d .github/workflows ]; then
  find .github/workflows -type f \( -name '*.yml' -o -name '*.yaml' \) | while read wf; do
    echo "## $wf"
    rg -n 'node-version|actions/setup-node' "$wf" || true
  done
else
  echo "No .github/workflows directory found"
fi

Repository: guilhermexp/openspace

Length of output: 584


🌐 Web query:

Node.js --env-file-if-exists flag introduction version

💡 Result:

The Node.js --env-file-if-exists CLI flag was introduced in version 22.9.0, released on 2024-09-17.

Citations:


🏁 Script executed:

#!/bin/bash
echo "=== Checking desktop/package.json ==="
wc -l ./desktop/package.json
echo ""
echo "=== Lines around 39 ==="
sed -n '35,45p' ./desktop/package.json
echo ""
echo "=== Lines around 61 ==="
sed -n '57,67p' ./desktop/package.json
echo ""
echo "=== Node engine constraint in desktop/package.json ==="
cat ./desktop/package.json | jq '.engines' 2>/dev/null || grep -A2 '"engines"' ./desktop/package.json || echo "No engines field found"

Repository: guilhermexp/openspace

Length of output: 2032


🏁 Script executed:

#!/bin/bash
echo "=== .nvmrc file ==="
cat .nvmrc 2>/dev/null || echo "No .nvmrc file"

echo ""
echo "=== Root package.json engines ==="
cat ./package.json | jq '.engines' 2>/dev/null || echo "No engines field"

echo ""
echo "=== All package.json files with engines ==="
find . -name 'package.json' -type f -exec grep -l '"engines"' {} \;

echo ""
echo "=== Check if Node 22 defaults to 22.9.0+ in GitHub Actions ==="
echo "Note: actions/setup-node@v4 with 'node-version: 22' uses latest 22.x"

Repository: guilhermexp/openspace

Length of output: 317


🌐 Web query:

GitHub Actions setup-node@v4 node-version 22 which version resolves

💡 Result:

In GitHub Actions using actions/setup-node@v4 with node-version: 22, it resolves to the latest available Node.js 22.x version from the actions/node-versions repository. As of early 2026, the latest release for Node 22 is 22.22.0 (from https://github.com/actions/node-versions/releases/tag/22.22.0-20981702931, result 13). Newer releases like 22.22.2 may be available in cached tools (result 60 shows 22.22.2 in ubuntu22 image), but the action first checks runner cache and falls back to the latest matching semver from node-versions if not found. With check-latest: false (default), it uses the highest cached or pre-released version satisfying ^22.0.0, typically the latest minor/patch like 22.22.x. Exact version depends on runner image and cache state, but sources confirm support for major version "22" resolving to latest 22.x (results 1,4,5,12,13). Node 22 is LTS 'Jod' (results 11,14).

Citations:


Add engines.node constraint to require Node.js 22.9.0 or later.

The scripts at lines 39 and 61 use --env-file-if-exists, a Node CLI flag introduced in version 22.9.0. While CI workflows using GitHub Actions resolve node-version: "22" to the latest 22.x (currently 22.22.0+), which supports this flag, contributors running local builds with older Node 22.x versions (22.0–22.8.x) will encounter an "unknown option" error. Add an explicit engines.node constraint in desktop/package.json to prevent this:

"engines": {
  "node": ">=22.9.0"
}

Or create a .nvmrc file at the repository root with 22.9.0 to enforce the minimum version.

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

In `@desktop/package.json` at line 39, The package.json must declare a minimum
Node version because scripts like the "prepare:gog:credentials" npm script rely
on the Node CLI flag --env-file-if-exists (added in Node v22.9.0); update
package.json by adding an "engines" field with "node": ">=22.9.0" (e.g., add an
"engines" object containing that constraint) so local installs warn or fail on
older Node, or alternatively add a top-level .nvmrc file containing "22.9.0" at
the repo root to enforce the minimum version for contributors.

Comment on lines +148 to +150
Guia de secrets e variables:

- [release-secrets-checklist.md](/Users/guilhermevarela/Documents/Projetos/openspace/desktop/docs/release-secrets-checklist.md)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix the absolute path and translate to English.

Line 148 contains Portuguese text, and line 150 uses an absolute local filesystem path that won't work for other users.

🔧 Proposed fix
-Guia de secrets e variables:
+Secrets and variables guide:

-- [release-secrets-checklist.md](/Users/guilhermevarela/Documents/Projetos/openspace/desktop/docs/release-secrets-checklist.md)
+- [release-secrets-checklist.md](docs/release-secrets-checklist.md)
📝 Committable suggestion

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

Suggested change
Guia de secrets e variables:
- [release-secrets-checklist.md](/Users/guilhermevarela/Documents/Projetos/openspace/desktop/docs/release-secrets-checklist.md)
Secrets and variables guide:
- [release-secrets-checklist.md](docs/release-secrets-checklist.md)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/README.md` around lines 148 - 150, Translate the Portuguese heading
and surrounding text to English and replace the absolute local path with a
repo-relative link; specifically, change the heading "Guia de secrets e
variables:" (and any Portuguese lines) to e.g. "Secrets and variables guide:"
and update the link from
"/Users/guilhermevarela/Documents/Projetos/openspace/desktop/docs/release-secrets-checklist.md"
to a relative path like "docs/release-secrets-checklist.md" so the README
references a repository file rather than a local filesystem location.

Comment on lines +129 to +135
ensureDir(outDir);
if (!hasConfiguredSource) {
console.log(
`[electron-desktop] gog OAuth client secret not configured; leaving ${outDir} empty`
);
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clear stale credential file on the “not configured” path.

If a previous run already wrote gog-client-secret.json, the current early return leaves that secret in place, so the directory is not actually empty.

🔧 Suggested fix
   ensureDir(outDir);
   if (!hasConfiguredSource) {
+    try {
+      fs.rmSync(outPath, { force: true });
+    } catch {
+      // ignore cleanup failures for missing/unreadable stale file
+    }
     console.log(
       `[electron-desktop] gog OAuth client secret not configured; leaving ${outDir} empty`
     );
     return;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/scripts/prepare-gog-credentials.mjs` around lines 129 - 135, When
hasConfiguredSource is false we currently return after ensureDir(outDir) but
leave any existing credential file behind; modify the "not configured" branch to
remove the stale credential file (e.g. the gog-client-secret.json under outDir)
before returning. Locate the block using ensureDir(outDir), hasConfiguredSource
and outDir and add logic to check for and delete the existing
gog-client-secret.json file (using fs.existsSync/fs.unlinkSync or the async
fs.promises equivalents) so the directory is actually empty on the early return.

Comment on lines +17 to +25
const originalApiKey = process.env.POSTHOG_API_KEY;

beforeEach(() => {
process.env.POSTHOG_API_KEY = "phc_test_key";
});

afterEach(() => {
process.env.POSTHOG_API_KEY = originalApiKey;
vi.resetModules();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Node.js, if you run process.env.MY_VAR = undefined, does it unset the variable or assign a string value? What is the recommended way to remove an env var in tests?

💡 Result:

Assigning process.env.MY_VAR = undefined sets the value to the string "undefined", not unsetting it. The recommended way to remove an env var is delete process.env.MY_VAR. In tests, use this directly, and clean up with afterEach( => { delete process.env.MY_VAR; }) or save/restore the original environment to avoid leaks between tests.

Citations:


🏁 Script executed:

cat -n desktop/src/main/analytics/posthog-main.test.ts | head -30

Repository: guilhermexp/openspace

Length of output: 1064


Restore POSTHOG_API_KEY with delete when originally unset.

If originalApiKey is undefined, assigning it back sets the variable to the string "undefined" rather than removing it, polluting subsequent tests.

Proposed fix
 describe("posthog-main", () => {
+  const hadOriginalApiKey = Object.prototype.hasOwnProperty.call(process.env, "POSTHOG_API_KEY");
   const originalApiKey = process.env.POSTHOG_API_KEY;

   beforeEach(() => {
     process.env.POSTHOG_API_KEY = "phc_test_key";
   });

   afterEach(() => {
-    process.env.POSTHOG_API_KEY = originalApiKey;
+    if (hadOriginalApiKey) {
+      process.env.POSTHOG_API_KEY = originalApiKey;
+    } else {
+      delete process.env.POSTHOG_API_KEY;
+    }
     vi.resetModules();
     vi.clearAllMocks();
   });
📝 Committable suggestion

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

Suggested change
const originalApiKey = process.env.POSTHOG_API_KEY;
beforeEach(() => {
process.env.POSTHOG_API_KEY = "phc_test_key";
});
afterEach(() => {
process.env.POSTHOG_API_KEY = originalApiKey;
vi.resetModules();
const hadOriginalApiKey = Object.prototype.hasOwnProperty.call(process.env, "POSTHOG_API_KEY");
const originalApiKey = process.env.POSTHOG_API_KEY;
beforeEach(() => {
process.env.POSTHOG_API_KEY = "phc_test_key";
});
afterEach(() => {
if (hadOriginalApiKey) {
process.env.POSTHOG_API_KEY = originalApiKey;
} else {
delete process.env.POSTHOG_API_KEY;
}
vi.resetModules();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/main/analytics/posthog-main.test.ts` around lines 17 - 25, The
afterEach currently restores process.env.POSTHOG_API_KEY by assigning
originalApiKey which will set the string "undefined" when it was originally
unset; change the restore logic in the afterEach block (the test file's
beforeEach/afterEach around originalApiKey and vi.resetModules) to check if
originalApiKey is undefined and, if so, delete process.env.POSTHOG_API_KEY,
otherwise set process.env.POSTHOG_API_KEY = originalApiKey, then call
vi.resetModules().

Comment on lines +46 to +56
launchctl("launchctl", ["print", target], { stdio: "ignore" });
} catch {
return false;
}

deps.log?.(
`[bootstrap] preferred port ${params.preferredPort} is occupied; unloading global ${GLOBAL_GATEWAY_LAUNCH_AGENT_LABEL}`,
);

try {
launchctl("launchctl", ["bootout", target], { stdio: "ignore" });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify both launchctl execFileSync call sites and their options in this file.
rg -n -C2 'launchctl\("launchctl", \["(print|bootout)", target\]' desktop/src/main/bootstrap/dev-global-gateway.ts

Repository: guilhermexp/openspace

Length of output: 387


🏁 Script executed:

#!/bin/bash
# Read the complete file to understand context and find all launchctl calls
cat -n desktop/src/main/bootstrap/dev-global-gateway.ts

Repository: guilhermexp/openspace

Length of output: 2852


🌐 Web query:

Node.js execFileSync timeout option

💡 Result:

Yes, Node.js child_process.execFileSync supports a timeout option in its options object. The timeout specifies the maximum time in milliseconds the child process is allowed to run. If exceeded, the parent sends a killSignal (default 'SIGTERM') to terminate it. The method blocks until the process fully exits, even after timeout. If the process times out or exits with a non-zero code, it throws an Error containing the spawnSync result. Example from official docs: const { execFileSync } = require('node:child_process'); try { const stdout = execFileSync('my-script.sh', ['my-arg'], { timeout: 5000, // 5 seconds killSignal: 'SIGTERM', stdio: 'pipe', encoding: 'utf8', }); console.log(stdout.toString); } catch (error) { console.error('Error:', error); } Key details: - Default timeout: 0 (no timeout). - Added in older versions, available in current Node.js v25+. - Use with caution in synchronous code as it blocks the event loop.

Citations:


Add timeouts to synchronous launchctl calls to avoid startup hangs.

Lines 46 and 56 call execFileSync without a timeout. If launchctl stalls or becomes unresponsive, bootstrap will block indefinitely.

🔧 Suggested fix
   const launchctl = deps.execFileSync ?? execFileSync;
   const target = `gui/${uid}/${GLOBAL_GATEWAY_LAUNCH_AGENT_LABEL}`;
+  const launchctlOpts = { stdio: "ignore" as const, timeout: 3_000 };
   try {
-    launchctl("launchctl", ["print", target], { stdio: "ignore" });
+    launchctl("launchctl", ["print", target], launchctlOpts);
   } catch {
     return false;
   }
@@
   try {
-    launchctl("launchctl", ["bootout", target], { stdio: "ignore" });
+    launchctl("launchctl", ["bootout", target], launchctlOpts);
   } catch (error) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/main/bootstrap/dev-global-gateway.ts` around lines 46 - 56, The
synchronous launchctl invocations (the launchctl helper used in the try/catch
blocks calling execFileSync via the launchctl function when checking and booting
out the GLOBAL_GATEWAY_LAUNCH_AGENT_LABEL) can hang indefinitely; update those
calls to supply a timeout option to execFileSync (via the launchctl wrapper) so
they fail-fast (e.g., use a small sensible timeout) and handle the exception as
before; modify the launchctl function or its call sites that pass { stdio:
"ignore" } to include a timeout value and ensure deps.log still runs when
params.preferredPort is occupied.

import type { GogExecResult } from "./types";

const PREPARE_CMD = "cd apps/electron-desktop && npm run fetch:gog";
const PREPARE_CMD = "cd desktop && npm run fetch:gog";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== registerGogIpcHandlers call sites =="
rg -nP --type=ts -C4 '\bregisterGogIpcHandlers\s*\('

echo "== openclawDir producers/assignments =="
rg -nP --type=ts -C3 '\bopenclawDir\b'

echo "== credential path references =="
rg -nP --type=ts -C2 '\.gog-runtime/credentials/gog-client-secret\.json'

Repository: guilhermexp/openspace

Length of output: 32849


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== resolveRepoRoot function =="
rg -nP --type=ts 'function resolveRepoRoot|const resolveRepoRoot' -A 5

echo "== resolveBundledOpenClawDir function =="
rg -nP --type=ts 'function resolveBundledOpenClawDir|const resolveBundledOpenClawDir' -A 8

echo "== app-bootstrap.ts around openclawDir assignment =="
sed -n '70,90p' desktop/src/main/bootstrap/app-bootstrap.ts

echo "== Check mainDir parameter source =="
rg -nP --type=ts 'mainDir' desktop/src/main/bootstrap/app-bootstrap.ts | head -20

Repository: guilhermexp/openspace

Length of output: 2517


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Full resolveRepoRoot and related functions =="
cat desktop/src/main/openclaw/paths.ts

echo -e "\n== How mainDir is determined =="
rg -nP --type=ts 'mainDir.*__dirname' desktop/src/main/ -B 2 -A 2

echo -e "\n== Entry point file =="
head -20 desktop/src/main/index.ts 2>/dev/null || echo "File not found, checking electron entry..."

Repository: guilhermexp/openspace

Length of output: 3192


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Complete ipc.ts context around line 14-32 =="
sed -n '14,35p' desktop/src/main/gog/ipc.ts

echo -e "\n== Check if resolveDownloadedGogCredentialsPath is used elsewhere =="
rg -nP --type=ts 'resolveDownloadedGogCredentialsPath'

echo -e "\n== Check GogHandlerParams and how mainDir is available =="
sed -n '59,62p' desktop/src/main/ipc/types.ts
rg -nP --type=ts 'type RegisterParams' -A 30 desktop/src/main/ipc/types.ts | head -50

Repository: guilhermexp/openspace

Length of output: 3143


🏁 Script executed:

#!/bin/bash
set -euorypipeline

echo "== How mainDir gets its value in app-bootstrap.ts =="
sed -n '30,50p' desktop/src/main/bootstrap/app-bootstrap.ts

echo -e "\n== Check what the actual entry point is =="
find desktop -name "main.ts" -o -name "index.ts" | head -5
grep -n "__dirname" desktop/src/main/index.ts 2>/dev/null || echo "Not in index.ts"
git ls-files desktop/src/main/ | grep -E "main|index|entry" | head -10

echo -e "\n== Verify the paths logic by checking an actual path test =="
sed -n '113,135p' desktop/src/main/openclaw/paths.test.ts

Repository: guilhermexp/openspace

Length of output: 739


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== How mainDir gets its value in app-bootstrap.ts =="
sed -n '30,50p' desktop/src/main/bootstrap/app-bootstrap.ts

echo -e "\n== Check what the actual entry point is =="
find desktop -name "main.ts" -o -name "index.ts" | head -5
git ls-files desktop/src/main/ | grep -E "main|index|entry" | head -10

echo -e "\n== Verify the paths logic by checking an actual path test =="
sed -n '113,135p' desktop/src/main/openclaw/paths.test.ts

Repository: guilhermexp/openspace

Length of output: 2215


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check the actual entry point and how mainDir is passed =="
sed -n '1,50p' desktop/src/main.ts

echo -e "\n== Check how bootstrapApp is called =="
rg -nP --type=ts 'bootstrapApp\(' -B 5 -A 2 desktop/src/main.ts

echo -e "\n== Check if mainDir is available in RegisterParams downstream =="
rg -nP --type=ts 'mainDir' desktop/src/main/bootstrap/app-bootstrap.ts | grep -E 'params\.|registerGog|ipc|register'

echo -e "\n== Comparison: existing resolveDownloadedGogCredentialsPath vs current usage =="
echo "Function in paths.ts:"
sed -n '57,61p' desktop/src/main/openclaw/paths.ts
echo -e "\nUsage in ipc.ts (lines 27-31):"
sed -n '27,31p' desktop/src/main/gog/ipc.ts

Repository: guilhermexp/openspace

Length of output: 2891


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check if mainDir is part of RegisterParams and available to GogHandlerParams =="
sed -n '9,24p' desktop/src/main/ipc/types.ts

echo -e "\n== Check if there are any E2E_BUNDLE_DIR usages that might affect paths =="
rg -nP --type=ts 'OPENCLAW_E2E_BUNDLE_DIR'

echo -e "\n== Verify the path derivation in practice - what if openclawDir is bundled? =="
echo "In production, openclawDir would be: path.join(process.resourcesPath, 'openclaw')"
echo "Then devCredentialsPath becomes: path.resolve(..., '..', 'desktop')"
echo "Which would resolve to: path.join(process.resourcesPath, 'desktop')"
echo "This path likely doesn't exist in the bundled app structure."

echo -e "\n== Check how the credentials path is actually used =="
sed -n '35,45p' desktop/src/main/gog/ipc.ts

echo -e "\n== See if mainDir could be added to GogHandlerParams =="
rg -nP --type=ts 'GogHandlerParams' -B 3 -A 3 desktop/src/main/ipc/types.ts

Repository: guilhermexp/openspace

Length of output: 2141


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Trace the actual path for E2E case with vendor =="
node <<'EOF'
const path = require('path');

const repoRoot = '/home/user/openspace';

// Normal dev case
const devOpenclawDir = path.join(repoRoot, 'openclaw');
const devCredPath1 = path.resolve(devOpenclawDir, '..', 'desktop');
console.log('Dev (normal):');
console.log('  openclawDir:', devOpenclawDir);
console.log('  devCredentialsPath:', devCredPath1);

// E2E vendored case
const e2eOpenclawDir = path.join(repoRoot, 'vendor', 'openclaw');
const e2eCredPath = path.resolve(e2eOpenclawDir, '..', 'desktop');
console.log('\nE2E (vendored):');
console.log('  openclawDir:', e2eOpenclawDir);
console.log('  devCredentialsPath:', e2eCredPath);
console.log('  Paths match?', devCredPath1 === e2eCredPath);

// Production case
const prodOpenclawDir = '/Applications/openspace.app/Contents/Resources/openclaw';
const prodCredPath = path.resolve(prodOpenclawDir, '..', 'desktop');
console.log('\nProduction (bundled):');
console.log('  openclawDir:', prodOpenclawDir);
console.log('  devCredentialsPath:', prodCredPath);
console.log('  Problem: This path likely does not exist in bundled app!');
EOF

echo -e "\n== Check if mainDir could be added to RegisterParams =="
echo "Currently, mainDir is available in bootstrapApp but not passed through RegisterParams."
echo "The dedicated function resolveDownloadedGogCredentialsPath already exists and is tested."
rg -nP --type=ts 'resolveDownloadedGogCredentialsPath' desktop/src/main/openclaw/paths.test.ts -A 5

Repository: guilhermexp/openspace

Length of output: 1429


Refactor devCredentialsPath derivation to be vendor/bundle-aware, or pass mainDir through handler params.

The assumption that openclawDir reliably points to the repository root is invalid. Path resolution breaks in two scenarios:

  1. E2E tests with vendored openclaw: path.resolve(openclawDir, "..", "desktop") resolves to vendor/desktop instead of the actual desktop directory.
  2. Production bundles: Resolves to Resources/desktop which doesn't exist in the bundled app structure.

A dedicated function resolveDownloadedGogCredentialsPath(mainDir) already exists in paths.ts and is properly tested. Either:

  • Add mainDir to GogHandlerParams and use the existing function, or
  • Implement explicit path resolution logic that accounts for packaged vs. dev states (similar to other binary path resolution in the codebase).

Applies to lines 14, 21-32.

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

In `@desktop/src/main/gog/ipc.ts` at line 14, The devCredentialsPath derivation
assumes openclawDir is repo root and breaks for vendored/bundled apps; update
the Gog handler to use the tested resolver by adding mainDir to GogHandlerParams
(or otherwise obtain mainDir) and replace the current path.resolve(openclawDir,
"..", "desktop") logic with a call to
resolveDownloadedGogCredentialsPath(mainDir) from paths.ts (or replicate its
packaged-vs-dev resolution behavior), updating any usages in the Gog handler
(e.g., where devCredentialsPath is computed near PREPARE_CMD and within the Gog
handler functions) to use the resolved path.

@@ -1 +1 @@
Subproject commit bd2c208689ba50310b90c276f29e02c1deec407f
Subproject commit c3ac023d93b8cb9f2970ac0248e15fe4bde48428
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Read submodule URL for openclaw from .gitmodules
url="$(git config -f .gitmodules --get submodule.openclaw.url)"
sha="c3ac023d93b8cb9f2970ac0248e15fe4bde48428"

echo "Submodule URL: $url"
echo "Target SHA:    $sha"

# 2) Verify the SHA exists on the remote
if git ls-remote "$url" | awk '{print $1}' | grep -qi "^$sha$"; then
  echo "OK: SHA exists on remote."
else
  echo "ERROR: SHA not found on remote refs."
  exit 1
fi

# 3) Verify recursive checkout can resolve the submodule SHA
git submodule sync -- openclaw
git submodule update --init --recursive -- openclaw
echo "OK: submodule update completed."

Repository: guilhermexp/openspace

Length of output: 216


The submodule SHA c3ac023d93b8cb9f2970ac0248e15fe4bde48428 does not exist on the remote and will cause CI/release jobs to fail.

The commit referenced in Line 1 is not reachable from https://github.com/guilhermexp/openclaw.git. Any attempt to run git submodule update --init --recursive will fail with a "reference not found" error, blocking all deployments and CI pipelines that depend on submodule checkout.

Verify the correct SHA exists on the remote or update the submodule pointer to a valid commit.

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

In `@openclaw` at line 1, The repository references a submodule commit SHA
c3ac023d93b8cb9f2970ac0248e15fe4bde48428 that doesn't exist on the remote; open
.gitmodules and the submodule entry in the index, verify the intended remote
(https://github.com/guilhermexp/openclaw.git) contains the correct commit, and
if not update the submodule pointer to a valid commit by checking out the
submodule to the correct reachable commit and committing the updated pointer in
the parent repo (or change the submodule URL in .gitmodules if it should point
elsewhere), then push the parent repo update so CI can successfully run git
submodule update --init --recursive.

guilhermexp and others added 2 commits April 1, 2026 12:42
The esbuild bundling step crashes on CI runners due to memory
constraints. Skip it in the verify job (smoke check) while keeping
the full bundle in the release job.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When esbuild bundling is skipped, the package resolve check fails
because packages aren't bundled into the vendor. Skip verification
in the verify job — the release job does the full validation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
desktop/renderer/src/ui/chat/components/ArtifactPanel.tsx (1)

101-105: ⚠️ Potential issue | 🟠 Major

Untrusted HTML preview still executes scripts.

srcDoc content is sandboxed, but sandbox="allow-scripts" still runs JavaScript from artifact content (Line 104). For local/untrusted artifacts, this keeps an avoidable attack surface; prefer removing scripts by default or gating with explicit user consent.

🔒 Suggested hardening
-        <iframe
-          className={styles.ArtifactHtml}
-          srcDoc={fileContent ?? ""}
-          sandbox="allow-scripts"
-        />
+        <iframe
+          className={styles.ArtifactHtml}
+          srcDoc={fileContent ?? ""}
+          sandbox=""
+          title={getArtifactFileName(filePath)}
+        />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/renderer/src/ui/chat/components/ArtifactPanel.tsx` around lines 101 -
105, The iframe in ArtifactPanel.tsx currently sets sandbox="allow-scripts"
which still permits execution of JavaScript from untrusted srcDoc content;
remove the allow-scripts token and instead use a fully isolated sandbox (e.g.,
no tokens) and/or sanitize fileContent before passing to srcDoc, and gate
re-enabling scripts behind explicit user consent (e.g., a boolean like
allowArtifactScripts toggled by the UI) in the ArtifactPanel component; update
references to the iframe/props (styles.ArtifactHtml, srcDoc, sandbox attribute)
and ensure any sanitizeHtml utility or consent check is used before enabling
scripts.
🧹 Nitpick comments (4)
desktop/renderer/src/ui/settings/account-models/AccountModelsTab.tsx (1)

96-98: Redundant fallback logic in state initialization.

Since isPaidMode is derived from authMode === "paid", when authMode is nullish, isPaidMode will always be false, making the ternary always resolve to "self-managed".

♻️ Simplify initialization
-  const [selectedMode, setSelectedMode] = useState<SetupMode | null>(
-    authMode ?? (isPaidMode ? "paid" : "self-managed")
-  );
+  const [selectedMode, setSelectedMode] = useState<SetupMode | null>(
+    authMode ?? "self-managed"
+  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/renderer/src/ui/settings/account-models/AccountModelsTab.tsx` around
lines 96 - 98, The state initialization for selectedMode uses a redundant
fallback that checks isPaidMode (derived from authMode === "paid"), so simplify
by initializing selectedMode directly from authMode with a default of
"self-managed" (i.e., replace the ternary fallback logic with authMode ??
"self-managed"), updating the useState call that declares selectedMode and
setSelectedMode (typed as SetupMode | null) accordingly.
.github/workflows/build-desktop.yml (1)

69-79: Consider making release publication all-or-nothing.

Because Line 131 publishes from each matrix leg directly, the first successful platform can create/upload to the draft release before the other platform finishes. If the second leg fails, you end up with a partial draft that has to be cleaned up manually. A small follow-up job that publishes only after both matrix builds succeed would make this path a lot more predictable.

Also applies to: 116-131

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

In @.github/workflows/build-desktop.yml around lines 69 - 79, The publish step
currently runs inside each matrix leg (using matrix.os, electron_args,
artifact_name and runs-on), causing partial draft releases if one leg fails; fix
it by removing the release publication from the per-matrix job(s) and adding a
single follow-up job (e.g., "publish-release") that has needs: [the matrix build
job name] so it only runs after all matrix matrix builds succeed, then have that
job download artifacts from each matrix run and perform the release/publish
once; update references to artifact_name/electron_args only for artifact
production in the matrix and centralize the publication logic in the new job.
desktop/src/preload.ts (1)

90-90: Register getOpenclawRuntimeInfo with the shared IPC constant on both sides.

preload.ts invokes IPC.getOpenclawRuntimeInfo, but desktop/src/main/ipc/config-ipc.ts still registers the handler with a raw "get-openclaw-runtime-info" literal. That split contract is easy to break during channel renames.

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

In `@desktop/src/preload.ts` at line 90, The preload call uses
IPC.getOpenclawRuntimeInfo but the main-process registration in config-ipc.ts
still uses the string literal "get-openclaw-runtime-info"; update the main-side
registration to import and use the same IPC constant
(IPC.getOpenclawRuntimeInfo) so both sides reference the identical channel
symbol, and verify the IPC enum/object defining getOpenclawRuntimeInfo is
exported and imported by both desktop/src/preload.ts and
desktop/src/main/ipc/config-ipc.ts (adjust the import if necessary) to keep the
contract consistent.
desktop/renderer/src/ui/chat/components/ToolCallCard.test.tsx (1)

9-13: Restore the global media spy after each test.

Mocking HTMLMediaElement.prototype.pause at module scope leaks into unrelated jsdom suites and can hide media regressions.

Suggested fix
-import { afterEach, describe, expect, it, vi } from "vitest";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@
-afterEach(() => {
-  cleanup();
-});
-
-vi.spyOn(HTMLMediaElement.prototype, "pause").mockImplementation(() => {});
+beforeEach(() => {
+  vi.spyOn(HTMLMediaElement.prototype, "pause").mockImplementation(() => {});
+});
+
+afterEach(() => {
+  cleanup();
+  vi.restoreAllMocks();
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/renderer/src/ui/chat/components/ToolCallCard.test.tsx` around lines 9
- 13, The global spy on HTMLMediaElement.prototype.pause is created at module
scope via vi.spyOn and currently leaks between tests; update the test file to
keep a reference to that spy (e.g., pauseSpy) and restore it in the afterEach
cleanup by calling pauseSpy.mockRestore() (or call vi.restoreAllMocks() in
afterEach) so the pause mock is removed after each test; ensure you still call
cleanup() inside the same afterEach block.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/build-desktop.yml:
- Around line 12-14: The verify job is only running on macOS and only building
the mac zip, while the Windows NSIS packaging is gated to release tags; update
the verify job and its build matrix so it runs on both macos-latest and
windows-latest (or add a separate verify job for windows) and include the
Windows NSIS/installer build target in the non-tagged verify matrix (remove or
replicate the NSIS target from the tag-only release matrix) so Windows
packaging, path, and signing are exercised on normal pushes; target identifiers
to change: the verify job name "verify", the "runs-on: macos-latest" value, the
build matrix entries that currently only include "--mac zip", and the release
matrix block that introduces the NSIS target.

In `@desktop/docs/plans/2026-03-31-desktop-openclaw-current-state.md`:
- Around line 3-6: Proofread and normalize Portuguese orthography in the plan
document: replace non-accented words like "codigo", "esta ativo", "ate",
"performance" with "código", "está ativo", "até", "desempenho" and scan the
entire file for similar accent/spelling issues; ensure consistent formal tone
and complete sentences throughout (e.g., revise fragments that read like session
notes), run a spellchecker or language linter, and apply the same corrections to
the other occurrences referenced (lines indicated around the occurrences at 197,
255, 272, 356) so the document reads as durable repository documentation rather
than raw notes.

In `@desktop/renderer/src/ui/chat/components/ArtifactPanel.tsx`:
- Around line 101-105: The iframe in ArtifactPanel.tsx (the <iframe
className={styles.ArtifactHtml} srcDoc={fileContent ?? ""}
sandbox="allow-scripts" />) is missing a title which hurts screen-reader
accessibility; update the iframe to include a descriptive title attribute (e.g.,
use the file name prop/variable if available, otherwise a sensible fallback like
"HTML preview") so the iframe has a clear, accessible label for assistive
technologies.

In `@desktop/renderer/src/ui/chat/components/ToolCallCard.tsx`:
- Around line 154-180: The component currently returns null when resolvedSrc is
falsy, which unmounts file-backed audio while useInlineMediaSrc is still
resolving or has produced an error; instead keep the audio element mounted for
file-backed sources so loading and bridgeError are visible. Remove the early `if
(!resolvedSrc) return null` and conditionally render the audio UI based on
whether a filePath was provided (use the `audioPath`/`filePath` signal from
`useInlineMediaSrc`), ensure `isLoading` is set when `resolvedSrc` is empty but
`filePath` exists, and surface `bridgeError` via `error` state so the player
shows loading/error instead of disappearing; refer to `useInlineMediaSrc`,
`resolvedSrc`, `bridgeError`, `audioRef`, and the useEffect that resets state to
implement this behavior.

---

Duplicate comments:
In `@desktop/renderer/src/ui/chat/components/ArtifactPanel.tsx`:
- Around line 101-105: The iframe in ArtifactPanel.tsx currently sets
sandbox="allow-scripts" which still permits execution of JavaScript from
untrusted srcDoc content; remove the allow-scripts token and instead use a fully
isolated sandbox (e.g., no tokens) and/or sanitize fileContent before passing to
srcDoc, and gate re-enabling scripts behind explicit user consent (e.g., a
boolean like allowArtifactScripts toggled by the UI) in the ArtifactPanel
component; update references to the iframe/props (styles.ArtifactHtml, srcDoc,
sandbox attribute) and ensure any sanitizeHtml utility or consent check is used
before enabling scripts.

---

Nitpick comments:
In @.github/workflows/build-desktop.yml:
- Around line 69-79: The publish step currently runs inside each matrix leg
(using matrix.os, electron_args, artifact_name and runs-on), causing partial
draft releases if one leg fails; fix it by removing the release publication from
the per-matrix job(s) and adding a single follow-up job (e.g.,
"publish-release") that has needs: [the matrix build job name] so it only runs
after all matrix matrix builds succeed, then have that job download artifacts
from each matrix run and perform the release/publish once; update references to
artifact_name/electron_args only for artifact production in the matrix and
centralize the publication logic in the new job.

In `@desktop/renderer/src/ui/chat/components/ToolCallCard.test.tsx`:
- Around line 9-13: The global spy on HTMLMediaElement.prototype.pause is
created at module scope via vi.spyOn and currently leaks between tests; update
the test file to keep a reference to that spy (e.g., pauseSpy) and restore it in
the afterEach cleanup by calling pauseSpy.mockRestore() (or call
vi.restoreAllMocks() in afterEach) so the pause mock is removed after each test;
ensure you still call cleanup() inside the same afterEach block.

In `@desktop/renderer/src/ui/settings/account-models/AccountModelsTab.tsx`:
- Around line 96-98: The state initialization for selectedMode uses a redundant
fallback that checks isPaidMode (derived from authMode === "paid"), so simplify
by initializing selectedMode directly from authMode with a default of
"self-managed" (i.e., replace the ternary fallback logic with authMode ??
"self-managed"), updating the useState call that declares selectedMode and
setSelectedMode (typed as SetupMode | null) accordingly.

In `@desktop/src/preload.ts`:
- Line 90: The preload call uses IPC.getOpenclawRuntimeInfo but the main-process
registration in config-ipc.ts still uses the string literal
"get-openclaw-runtime-info"; update the main-side registration to import and use
the same IPC constant (IPC.getOpenclawRuntimeInfo) so both sides reference the
identical channel symbol, and verify the IPC enum/object defining
getOpenclawRuntimeInfo is exported and imported by both desktop/src/preload.ts
and desktop/src/main/ipc/config-ipc.ts (adjust the import if necessary) to keep
the contract consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 37d0fab1-9e8e-4d5f-b5f7-bf5762313a04

📥 Commits

Reviewing files that changed from the base of the PR and between 37d896a and 0e6058e.

📒 Files selected for processing (29)
  • .github/workflows/build-desktop.yml
  • desktop/docs/plans/2026-03-31-desktop-openclaw-current-state.md
  • desktop/renderer/src/store/slices/auth/authSlice.test.ts
  • desktop/renderer/src/store/slices/auth/authSlice.ts
  • desktop/renderer/src/store/slices/session-model-reset.ts
  • desktop/renderer/src/ui/chat/ChatPage.tsx
  • desktop/renderer/src/ui/chat/components/ActionLog.test.tsx
  • desktop/renderer/src/ui/chat/components/ArtifactPanel.tsx
  • desktop/renderer/src/ui/chat/components/ChatComposer.test.tsx
  • desktop/renderer/src/ui/chat/components/ChatMessageList.tsx
  • desktop/renderer/src/ui/chat/components/ToolCallCard.test.tsx
  • desktop/renderer/src/ui/chat/components/ToolCallCard.tsx
  • desktop/renderer/src/ui/chat/context/ArtifactContext.test.tsx
  • desktop/renderer/src/ui/chat/hooks/useChatStream.test.tsx
  • desktop/renderer/src/ui/chat/hooks/useVoiceConfig.test.tsx
  • desktop/renderer/src/ui/chat/hooks/useVoiceInput.test.ts
  • desktop/renderer/src/ui/settings/account-models/AccountModelsTab.tsx
  • desktop/renderer/src/ui/settings/voice/VoiceRecognitionTab.tsx
  • desktop/src/main/analytics/posthog-main.ts
  • desktop/src/main/bootstrap/dev-global-gateway.test.ts
  • desktop/src/main/bootstrap/dev-global-gateway.ts
  • desktop/src/main/ipc/file-reader.test.ts
  • desktop/src/main/keys/authProfilesStore.ts
  • desktop/src/main/keys/openai-api-key.test.ts
  • desktop/src/main/openclaw/bundle-capability-sources.test.ts
  • desktop/src/main/whisper/ipc.test.ts
  • desktop/src/main/whisper/ipc.ts
  • desktop/src/main/window/mainWindow.ts
  • desktop/src/preload.ts
✅ Files skipped from review due to trivial changes (4)
  • desktop/renderer/src/ui/chat/components/ChatComposer.test.tsx
  • desktop/src/main/keys/openai-api-key.test.ts
  • desktop/src/main/ipc/file-reader.test.ts
  • desktop/renderer/src/ui/chat/ChatPage.tsx
🚧 Files skipped from review as they are similar to previous changes (16)
  • desktop/src/main/openclaw/bundle-capability-sources.test.ts
  • desktop/renderer/src/ui/chat/hooks/useVoiceConfig.test.tsx
  • desktop/renderer/src/ui/chat/hooks/useChatStream.test.tsx
  • desktop/src/main/bootstrap/dev-global-gateway.ts
  • desktop/renderer/src/store/slices/auth/authSlice.ts
  • desktop/renderer/src/store/slices/session-model-reset.ts
  • desktop/src/main/window/mainWindow.ts
  • desktop/renderer/src/store/slices/auth/authSlice.test.ts
  • desktop/renderer/src/ui/settings/voice/VoiceRecognitionTab.tsx
  • desktop/src/main/whisper/ipc.test.ts
  • desktop/src/main/bootstrap/dev-global-gateway.test.ts
  • desktop/renderer/src/ui/chat/hooks/useVoiceInput.test.ts
  • desktop/renderer/src/ui/chat/components/ChatMessageList.tsx
  • desktop/src/main/whisper/ipc.ts
  • desktop/renderer/src/ui/chat/context/ArtifactContext.test.tsx
  • desktop/src/main/analytics/posthog-main.ts

Comment on lines +12 to 14
verify:
if: github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/v')
runs-on: macos-latest
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The new Windows installer path is only tested on release tags.

Line 14 still hardcodes verify to macos-latest, and Line 54 only runs --mac zip, while Line 77 introduces the Windows NSIS target only in the tag-gated release matrix. That makes a v* tag the first time the Windows packaging path runs, so NSIS/path/signing regressions can land on main unnoticed and fail only during release.

💡 Suggested smoke-build matrix for verify
   verify:
     if: github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/v')
-    runs-on: macos-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          - os: macos-latest
+            smoke_args: --mac zip
+            artifact_name: openspace-smoke-mac
+          - os: windows-latest
+            smoke_args: --win nsis
+            artifact_name: openspace-smoke-win
+    runs-on: ${{ matrix.os }}
@@
       - name: Package smoke build
         env:
           CSC_IDENTITY_AUTO_DISCOVERY: false
-        run: cd desktop && npx electron-builder --publish never --mac zip
+        run: cd desktop && npx electron-builder --publish never ${{ matrix.smoke_args }}
@@
       - name: Upload smoke artifacts
         uses: actions/upload-artifact@v4
         with:
-          name: openspace-smoke-mac-${{ github.sha }}
+          name: ${{ matrix.artifact_name }}-${{ github.sha }}

Also applies to: 51-55, 69-79

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

In @.github/workflows/build-desktop.yml around lines 12 - 14, The verify job is
only running on macOS and only building the mac zip, while the Windows NSIS
packaging is gated to release tags; update the verify job and its build matrix
so it runs on both macos-latest and windows-latest (or add a separate verify job
for windows) and include the Windows NSIS/installer build target in the
non-tagged verify matrix (remove or replicate the NSIS target from the tag-only
release matrix) so Windows packaging, path, and signing are exercised on normal
pushes; target identifiers to change: the verify job name "verify", the
"runs-on: macos-latest" value, the build matrix entries that currently only
include "--mac zip", and the release matrix block that introduces the NSIS
target.

Comment on lines +3 to +6
> Data: 2026-03-31
> Projeto: OpenSpace Desktop
> Status: documentado conforme estado atual do ambiente e do codigo

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Proofread this plan doc before merging it as a reference.

There are repeated spelling/orthography issues here (codigo, esta ativo, ate, performance) that make the file read like session notes instead of durable repository documentation.

Also applies to: 197-197, 255-255, 272-272, 356-356

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

In `@desktop/docs/plans/2026-03-31-desktop-openclaw-current-state.md` around lines
3 - 6, Proofread and normalize Portuguese orthography in the plan document:
replace non-accented words like "codigo", "esta ativo", "ate", "performance"
with "código", "está ativo", "até", "desempenho" and scan the entire file for
similar accent/spelling issues; ensure consistent formal tone and complete
sentences throughout (e.g., revise fragments that read like session notes), run
a spellchecker or language linter, and apply the same corrections to the other
occurrences referenced (lines indicated around the occurrences at 197, 255, 272,
356) so the document reads as durable repository documentation rather than raw
notes.

Comment on lines +101 to +105
<iframe
className={styles.ArtifactHtml}
srcDoc={fileContent ?? ""}
sandbox="allow-scripts"
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add iframe title for accessibility.

The HTML preview iframe is missing a title, which reduces screen-reader usability (Line 101). Add a descriptive title (e.g., filename).

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

In `@desktop/renderer/src/ui/chat/components/ArtifactPanel.tsx` around lines 101 -
105, The iframe in ArtifactPanel.tsx (the <iframe
className={styles.ArtifactHtml} srcDoc={fileContent ?? ""}
sandbox="allow-scripts" />) is missing a title which hurts screen-reader
accessibility; update the iframe to include a descriptive title attribute (e.g.,
use the file name prop/variable if available, otherwise a sensible fallback like
"HTML preview") so the iframe has a clear, accessible label for assistive
technologies.

Comment on lines +154 to +180
const { src: bridgedSrc, error: bridgeError } = useInlineMediaSrc({
dataUrl: src,
filePath: audioPath,
});
const resolvedSrc = bridgedSrc;
const audioRef = useRef<HTMLAudioElement | null>(null);
const [isLoading, setIsLoading] = useState(Boolean(resolvedSrc));
const [isPlaying, setIsPlaying] = useState(false);
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
setIsLoading(Boolean(resolvedSrc));
setIsPlaying(false);
setDuration(0);
setCurrentTime(0);
setError(bridgeError);
if (audioRef.current) {
audioRef.current.pause();
audioRef.current.currentTime = 0;
}
}, [bridgeError, resolvedSrc]);

if (!resolvedSrc) {
return null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep file-backed audio mounted while it resolves or fails.

useInlineMediaSrc starts filePath media with src="", so this early return hides both the loading state and any bridge error. On a failed read, the TTS/audio result just disappears.

Suggested fix
-  if (!resolvedSrc) {
+  if (!src && !audioPath) {
     return null;
   }

   const displayTitle = title ?? getAudioLabel(src, audioPath);
-  const statusText = isLoading
-    ? "Preparing audio"
+  const statusText = error
+    ? "Audio unavailable"
+    : isLoading || (!resolvedSrc && Boolean(audioPath))
+      ? "Preparing audio"
       : isPlaying
         ? `${formatAudioTime(currentTime)} / ${formatAudioTime(duration)}`
         : duration > 0
           ? `${formatAudioTime(duration)} ready`
           : "Voice reply";
@@
-      <audio
-        ref={audioRef}
-        className={s.AudioPlayerElement}
-        preload="metadata"
-        src={resolvedSrc}
-        aria-label={displayTitle}
-        onLoadedMetadata={(event) => {
-          setDuration(event.currentTarget.duration || 0);
-          setIsLoading(false);
-        }}
-        onCanPlay={() => setIsLoading(false)}
-        onTimeUpdate={(event) => setCurrentTime(event.currentTarget.currentTime || 0)}
-        onPlay={() => setIsPlaying(true)}
-        onPause={() => setIsPlaying(false)}
-        onEnded={() => {
-          setIsPlaying(false);
-          setCurrentTime(0);
-        }}
-        onError={() => {
-          setIsLoading(false);
-          setIsPlaying(false);
-          setError("Unable to load audio.");
-        }}
-      />
+      {resolvedSrc ? (
+        <audio
+          ref={audioRef}
+          className={s.AudioPlayerElement}
+          preload="metadata"
+          src={resolvedSrc}
+          aria-label={displayTitle}
+          onLoadedMetadata={(event) => {
+            setDuration(event.currentTarget.duration || 0);
+            setIsLoading(false);
+          }}
+          onCanPlay={() => setIsLoading(false)}
+          onTimeUpdate={(event) => setCurrentTime(event.currentTarget.currentTime || 0)}
+          onPlay={() => setIsPlaying(true)}
+          onPause={() => setIsPlaying(false)}
+          onEnded={() => {
+            setIsPlaying(false);
+            setCurrentTime(0);
+          }}
+          onError={() => {
+            setIsLoading(false);
+            setIsPlaying(false);
+            setError("Unable to load audio.");
+          }}
+        />
+      ) : null}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/renderer/src/ui/chat/components/ToolCallCard.tsx` around lines 154 -
180, The component currently returns null when resolvedSrc is falsy, which
unmounts file-backed audio while useInlineMediaSrc is still resolving or has
produced an error; instead keep the audio element mounted for file-backed
sources so loading and bridgeError are visible. Remove the early `if
(!resolvedSrc) return null` and conditionally render the audio UI based on
whether a filePath was provided (use the `audioPath`/`filePath` signal from
`useInlineMediaSrc`), ensure `isLoading` is set when `resolvedSrc` is empty but
`filePath` exists, and surface `bridgeError` via `error` state so the player
shows loading/error instead of disappearing; refer to `useInlineMediaSrc`,
`resolvedSrc`, `bridgeError`, `audioRef`, and the useEffect that resets state to
implement this behavior.

guilhermexp and others added 2 commits April 1, 2026 12:57
The fetch scripts already read GITHUB_TOKEN for authenticated
GitHub API calls. Without it, CI runners hit the 60 req/hour
unauthenticated rate limit. Authenticated gets 5000/hour.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The verify job doesn't need to prepare openclaw bundle, fetch
runtimes, or package. Those steps hit rate limits, private repos,
and OOM issues. The release job does the full pipeline.

Verify now checks: lint, prettier, typecheck, and build compilation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@guilhermexp guilhermexp merged commit 7eca648 into main Apr 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant