chore: desktop release alignment + CI fixes#1
Conversation
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>
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughRestructures 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
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
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 })
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
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>
There was a problem hiding this comment.
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 | 🔴 CriticalThe notarization script is completely missing from the repository.
The script
notarize-mac-artifact.shdoes not exist atopenclaw/scripts/notarize-mac-artifact.shor anywhere else in the repository. Theopenclawdirectory exists but is empty with noscriptssubdirectory. 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 | 🟡 MinorRemove debug
console.logstatement.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.
chatComposerSpinuses camelCase, which matches other keyframes in this file (micPulse,micSpin). However, Stylelint flags this as a violation ofkeyframes-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.ttson line 246, the expression!("tts" in cfg)is alwaystrue, makingchanged || !("tts" in cfg)equivalent totrue. Since reaching this branch means legacyttsexisted 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-visiblestate.🎯 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
showOpenAiVoiceSectioncheck includes bothhasOpenAiTts(set from effect) andisOpenAiTtsConfigured(props.configSnap?.config). This handles the case whereprops.configSnapupdates 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 recreatehandleOpenAiVoiceChangewhenever 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
artifactPanelEnterviolates thekeyframes-name-patternlinting rule expecting kebab-case. If your project enforces this rule, rename toartifact-panel-enterand 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: UsefireEvent.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) usefireEvent.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
fireEventto 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: Redundantcleanup()call inbeforeEach.React Testing Library automatically calls
cleanup()after each test when using Vitest. The explicitcleanup()call inbeforeEach(Line 70) is unnecessary and redundant with theafterEachcleanup (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 upwindow.openclawDesktopafter each test.
Object.definePropertyonwindow.openclawDesktoppersists 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 inafterEach.The global spy on
HTMLMediaElement.prototype.pauseis 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 globalafterAllif 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 addingAbortControllerand a timeout for the fetch call.The
cancelledflag 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 betweenisRecordingandwavIsRecording.The combined state
isRecording || wavIsRecordingcould briefly be inconsistent. For example, instartRecording, localisRecordingis set synchronously (Line 76) butwavIsRecordingdepends on the async behavior ofstartWavRecording. Similarly,stopRecordingclears local state immediately (Line 80) beforestopWavRecordingcompletes.While this likely won't cause user-visible issues, consider using only
wavIsRecordingas 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_gwRequestparameter 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 inStartChatPage.tsx,ChatPage.tsx, anduseVoiceConfig.tsthat currently passgw.requestorgwRequest. 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
currentUserIdtracking 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:
.aac→audio/aac.flac→audio/flac.tiff/.tif→image/tiff.bmp→image/bmpThis 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.openclawDesktopmock is defined identically in 5 tests. Extracting this to abeforeEachor 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 fromgh repo view --json nameWithOwner -q .nameWithOwnerwhen 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) andflushMedia(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])returnsstopDraggingas the cleanup function, which runs on unmount and whenstopDraggingchanges. SincestopDraggingis 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
useEffectincludesartifact.panelWidthandartifact.setPanelWidthin dependencies, and callssetPanelWidthinside. While the conditional check (nextPanelWidth !== artifact.panelWidth) prevents infinite loops, consider if this logic could be moved to theArtifactContextitself or triggered only on explicit resize events to avoid running on everyviewportWidthchange.🤖 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
handleVoiceMessageStopcallback contains substantial logic: stopping recording, checking paywall, toggling voice mode, and sending the message. Consider extracting this intouseVoiceInputor 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
*.dmgwhich is macOS-specific, and*.exewhich is Windows-specific. This is fine sinceif-no-files-found: warnis 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 addingtimeout-minutesto prevent runaway builds.The verify and release jobs don't specify a timeout. Consider adding
timeout-minutesto 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
hasStructuredVoiceTranscriptfunction 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%\.awsProposed 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 thecleansession stays untouched.Both fixtures include a
cleansession, but the tests never assert that it is not patched. A regression that sendssessions.patchfor 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
📒 Files selected for processing (126)
.github/workflows/build-desktop.yml.github/workflows/sync-openclaw.ymldesktop/README.mddesktop/docs/gateway-message-metadata.mddesktop/docs/plans/2026-03-31-desktop-openclaw-current-state.mddesktop/docs/release-secrets-checklist.mddesktop/docs/telegram-manual-setup-electron.mddesktop/package.jsondesktop/renderer/index.htmldesktop/renderer/src/analytics/posthog-client.test.tsdesktop/renderer/src/analytics/posthog-client.tsdesktop/renderer/src/gateway/client.tsdesktop/renderer/src/store/slices/auth/authSlice.test.tsdesktop/renderer/src/store/slices/auth/authSlice.tsdesktop/renderer/src/store/slices/chat/chat-thunks.tsdesktop/renderer/src/store/slices/chat/chat-types.tsdesktop/renderer/src/store/slices/chat/chat-utils.test.tsdesktop/renderer/src/store/slices/chat/chat-utils.tsdesktop/renderer/src/store/slices/chat/chatSlice.test.tsdesktop/renderer/src/store/slices/chat/chatSlice.tsdesktop/renderer/src/store/slices/session-model-reset.tsdesktop/renderer/src/ui/chat/ChatPage.tsxdesktop/renderer/src/ui/chat/ChatTranscript.module.cssdesktop/renderer/src/ui/chat/components/ActionLog.test.tsxdesktop/renderer/src/ui/chat/components/ActionLog.tsxdesktop/renderer/src/ui/chat/components/ArtifactDivider.tsxdesktop/renderer/src/ui/chat/components/ArtifactPanel.module.cssdesktop/renderer/src/ui/chat/components/ArtifactPanel.test.tsxdesktop/renderer/src/ui/chat/components/ArtifactPanel.tsxdesktop/renderer/src/ui/chat/components/ChatComposer.module.cssdesktop/renderer/src/ui/chat/components/ChatComposer.test.tsxdesktop/renderer/src/ui/chat/components/ChatComposer.tsxdesktop/renderer/src/ui/chat/components/ChatMessageList.audio.test.tsxdesktop/renderer/src/ui/chat/components/ChatMessageList.tsxdesktop/renderer/src/ui/chat/components/MessageMeta.test.tsxdesktop/renderer/src/ui/chat/components/ToolCallCard.module.cssdesktop/renderer/src/ui/chat/components/ToolCallCard.test.tsxdesktop/renderer/src/ui/chat/components/ToolCallCard.tsxdesktop/renderer/src/ui/chat/components/artifact-preview.tsdesktop/renderer/src/ui/chat/components/inline-media.tsxdesktop/renderer/src/ui/chat/context/ArtifactContext.test.tsxdesktop/renderer/src/ui/chat/context/ArtifactContext.tsxdesktop/renderer/src/ui/chat/hooks/useChatStream.test.tsxdesktop/renderer/src/ui/chat/hooks/useChatStream.tsdesktop/renderer/src/ui/chat/hooks/useMarkdownComponents.tsxdesktop/renderer/src/ui/chat/hooks/useVoiceConfig.test.tsxdesktop/renderer/src/ui/chat/hooks/useVoiceConfig.tsdesktop/renderer/src/ui/chat/hooks/useVoiceInput.test.tsdesktop/renderer/src/ui/chat/hooks/useVoiceInput.tsdesktop/renderer/src/ui/onboarding/RestoreFilePage.module.cssdesktop/renderer/src/ui/settings/OtherTab.test.tsxdesktop/renderer/src/ui/settings/OtherTab.tsxdesktop/renderer/src/ui/settings/RestoreBackupModal.module.cssdesktop/renderer/src/ui/settings/account-models/AccountModelsTab.module.cssdesktop/renderer/src/ui/settings/account-models/AccountModelsTab.test.tsxdesktop/renderer/src/ui/settings/account-models/AccountModelsTab.tsxdesktop/renderer/src/ui/settings/account-models/InlineApiKey.tsxdesktop/renderer/src/ui/settings/account/AccountTab.module.cssdesktop/renderer/src/ui/settings/connectors/ConnectorsTab.tsxdesktop/renderer/src/ui/settings/providers/useModelProvidersState.tsdesktop/renderer/src/ui/settings/settings-visual-standardization.test.tsdesktop/renderer/src/ui/settings/skills/CustomSkillUpload.module.cssdesktop/renderer/src/ui/settings/skills/SkillsGrid.tsxdesktop/renderer/src/ui/settings/voice/VoiceRecognitionTab.module.cssdesktop/renderer/src/ui/settings/voice/VoiceRecognitionTab.test.tsxdesktop/renderer/src/ui/settings/voice/VoiceRecognitionTab.tsxdesktop/renderer/src/ui/shared/billing/AutoTopUpControl.module.cssdesktop/renderer/src/ui/shared/kit/icons.tsxdesktop/renderer/src/ui/shared/models/modelPresentation.tsdesktop/renderer/src/ui/sidebar/session-sidebar-activity.test.tsxdesktop/renderer/src/ui/sidebar/useSessionActivity.test.tsdesktop/renderer/src/ui/styles/base.cssdesktop/renderer/src/ui/styles/providers-shared.cssdesktop/scripts/build-dmg-from-app.shdesktop/scripts/build-memo-runtime.mjsdesktop/scripts/configure-github-release-secrets.shdesktop/scripts/electron-builder.afterAllArtifactBuild-notarize-dmg.cjsdesktop/scripts/electron-builder.afterSign-notarize.cjsdesktop/scripts/lib/openclaw-bundle-utils.mjsdesktop/scripts/lib/openclaw-bundle-verify.mjsdesktop/scripts/prepare-gh-runtime.mjsdesktop/scripts/prepare-gog-credentials.mjsdesktop/scripts/prepare-gog-runtime.mjsdesktop/scripts/prepare-jq-runtime.mjsdesktop/scripts/prepare-memo-runtime.mjsdesktop/scripts/prepare-obsidian-cli-runtime.mjsdesktop/scripts/prepare-openclaw-bundle.mjsdesktop/scripts/prepare-remindctl-runtime.mjsdesktop/scripts/prepare-whisper-cli-runtime.mjsdesktop/scripts/release.shdesktop/src/main/analytics/posthog-main.test.tsdesktop/src/main/analytics/posthog-main.tsdesktop/src/main/bootstrap/app-bootstrap.test.tsdesktop/src/main/bootstrap/app-bootstrap.tsdesktop/src/main/bootstrap/dev-global-gateway.test.tsdesktop/src/main/bootstrap/dev-global-gateway.tsdesktop/src/main/gateway/config-migrations.test.tsdesktop/src/main/gateway/config-migrations.tsdesktop/src/main/gateway/extra-models.test.tsdesktop/src/main/gateway/spawn.test.tsdesktop/src/main/gateway/spawn.tsdesktop/src/main/gog/ipc.tsdesktop/src/main/ipc/config-ipc.tsdesktop/src/main/ipc/contracts.test.tsdesktop/src/main/ipc/file-reader.test.tsdesktop/src/main/ipc/file-reader.tsdesktop/src/main/ipc/gh-ipc.tsdesktop/src/main/ipc/memo-ipc.tsdesktop/src/main/ipc/obsidian-ipc.tsdesktop/src/main/ipc/register.tsdesktop/src/main/ipc/remindctl-ipc.tsdesktop/src/main/ipc/types.tsdesktop/src/main/keys/apiKeys.tsdesktop/src/main/keys/authProfilesStore.test.tsdesktop/src/main/keys/authProfilesStore.tsdesktop/src/main/keys/openai-api-key.test.tsdesktop/src/main/keys/openai-api-key.tsdesktop/src/main/openclaw/bundle-capability-sources.test.tsdesktop/src/main/openclaw/paths.tsdesktop/src/main/whisper/ipc.test.tsdesktop/src/main/whisper/ipc.tsdesktop/src/main/window/mainWindow.tsdesktop/src/preload.tsdesktop/src/shared/desktop-bridge-contract.tsdesktop/src/shared/ipc-channels.tsopenclaw
💤 Files with no reviewable changes (1)
- desktop/renderer/src/ui/sidebar/session-sidebar-activity.test.tsx
| ``` | ||
| Atomic Bot 13:40 ↑4k ↓371 R26k W212 claude-opus-4-6 | ||
| OpenSpace 13:40 ↑4k ↓371 R26k W212 claude-opus-4-6 | ||
| ``` |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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.
| 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.
| Script: | ||
|
|
||
| - [configure-github-release-secrets.sh](/Users/guilhermevarela/Documents/Projetos/openspace/desktop/scripts/configure-github-release-secrets.sh) | ||
|
|
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
🧩 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
fiRepository: 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"
fiRepository: 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:
- 1: https://nodejs.org/en/blog/release/v22.9.0
- 2: v22.9.0 proposal nodejs/node#54966
- 3: https://nodejs.org/docs/latest-v22.x/api/cli.html
🏁 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:
- 1: https://git.homeinfra.org/actions/setup-node/src/tag/v4.0.4/README.md
- 2: https://github.com/actions/setup-node
- 3: https://github.com/marketplace/actions/setup-node-js-environment
- 4: https://github.com/actions/node-versions
- 5: https://github.com/actions/node-versions/releases
- 6: ubuntu-latest default Node.js 20.20.1 end-of-life on 2026-04-30 actions/runner-images#13833
- 7: https://github.com/actions/runner-images/releases/tag/ubuntu22/20260329.74
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.
| Guia de secrets e variables: | ||
|
|
||
| - [release-secrets-checklist.md](/Users/guilhermevarela/Documents/Projetos/openspace/desktop/docs/release-secrets-checklist.md) |
There was a problem hiding this comment.
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.
| 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.
| ensureDir(outDir); | ||
| if (!hasConfiguredSource) { | ||
| console.log( | ||
| `[electron-desktop] gog OAuth client secret not configured; leaving ${outDir} empty` | ||
| ); | ||
| return; | ||
| } |
There was a problem hiding this comment.
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.
| 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(); |
There was a problem hiding this comment.
🧩 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:
- 1: Setting
undefinedtoprocess.env.*sets a"undefined"string value nodejs/node#48875 - 2: https://nodejs.org/docs/latest/api/process.html
- 3: https://stackoverflow.com/questions/43314209/node-js-unset-environment-variable
- 4: https://stackoverflow.com/questions/24589021/how-to-stub-process-env-in-node-js/29002989
- 5: https://stackoverflow.com/questions/42170365/how-do-i-remove-a-value-in-process-env/42170366
🏁 Script executed:
cat -n desktop/src/main/analytics/posthog-main.test.ts | head -30Repository: 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.
| 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().
| 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" }); |
There was a problem hiding this comment.
🧩 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.tsRepository: 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.tsRepository: 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:
- 1: https://nodejs.org/api/child_process.html
- 2: https://r2.nodejs.org/docs/v21.0.0/api/child_process.html
- 3: https://nodejs.org/docs/latest/api/child_process.html
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"; |
There was a problem hiding this comment.
🧩 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 -20Repository: 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 -50Repository: 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.tsRepository: 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.tsRepository: 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.tsRepository: 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.tsRepository: 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 5Repository: 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:
- E2E tests with vendored openclaw:
path.resolve(openclawDir, "..", "desktop")resolves tovendor/desktopinstead of the actual desktop directory. - Production bundles: Resolves to
Resources/desktopwhich doesn't exist in the bundled app structure.
A dedicated function resolveDownloadedGogCredentialsPath(mainDir) already exists in paths.ts and is properly tested. Either:
- Add
mainDirtoGogHandlerParamsand 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 | |||
There was a problem hiding this comment.
🧩 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.
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>
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (1)
desktop/renderer/src/ui/chat/components/ArtifactPanel.tsx (1)
101-105:⚠️ Potential issue | 🟠 MajorUntrusted HTML preview still executes scripts.
srcDoccontent is sandboxed, butsandbox="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
isPaidModeis derived fromauthMode === "paid", whenauthModeis nullish,isPaidModewill always befalse, 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: RegistergetOpenclawRuntimeInfowith the shared IPC constant on both sides.
preload.tsinvokesIPC.getOpenclawRuntimeInfo, butdesktop/src/main/ipc/config-ipc.tsstill 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.pauseat 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
📒 Files selected for processing (29)
.github/workflows/build-desktop.ymldesktop/docs/plans/2026-03-31-desktop-openclaw-current-state.mddesktop/renderer/src/store/slices/auth/authSlice.test.tsdesktop/renderer/src/store/slices/auth/authSlice.tsdesktop/renderer/src/store/slices/session-model-reset.tsdesktop/renderer/src/ui/chat/ChatPage.tsxdesktop/renderer/src/ui/chat/components/ActionLog.test.tsxdesktop/renderer/src/ui/chat/components/ArtifactPanel.tsxdesktop/renderer/src/ui/chat/components/ChatComposer.test.tsxdesktop/renderer/src/ui/chat/components/ChatMessageList.tsxdesktop/renderer/src/ui/chat/components/ToolCallCard.test.tsxdesktop/renderer/src/ui/chat/components/ToolCallCard.tsxdesktop/renderer/src/ui/chat/context/ArtifactContext.test.tsxdesktop/renderer/src/ui/chat/hooks/useChatStream.test.tsxdesktop/renderer/src/ui/chat/hooks/useVoiceConfig.test.tsxdesktop/renderer/src/ui/chat/hooks/useVoiceInput.test.tsdesktop/renderer/src/ui/settings/account-models/AccountModelsTab.tsxdesktop/renderer/src/ui/settings/voice/VoiceRecognitionTab.tsxdesktop/src/main/analytics/posthog-main.tsdesktop/src/main/bootstrap/dev-global-gateway.test.tsdesktop/src/main/bootstrap/dev-global-gateway.tsdesktop/src/main/ipc/file-reader.test.tsdesktop/src/main/keys/authProfilesStore.tsdesktop/src/main/keys/openai-api-key.test.tsdesktop/src/main/openclaw/bundle-capability-sources.test.tsdesktop/src/main/whisper/ipc.test.tsdesktop/src/main/whisper/ipc.tsdesktop/src/main/window/mainWindow.tsdesktop/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
| verify: | ||
| if: github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/v') | ||
| runs-on: macos-latest |
There was a problem hiding this comment.
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.
| > Data: 2026-03-31 | ||
| > Projeto: OpenSpace Desktop | ||
| > Status: documentado conforme estado atual do ambiente e do codigo | ||
|
|
There was a problem hiding this comment.
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.
| <iframe | ||
| className={styles.ArtifactHtml} | ||
| srcDoc={fileContent ?? ""} | ||
| sandbox="allow-scripts" | ||
| /> |
There was a problem hiding this comment.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
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>
Summary
prepare-openclaw-bundleto copy capability source dirs (resolvesmusic-metadatamissing error)permissions: contents: writeto sync-openclaw workflowverify(push/PR) andrelease(tagsv*) jobsWhat 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
Bug Fixes
Improvements
Documentation