8000 feat: docs tab content by gastoner · Pull Request #13661 · podman-desktop/podman-desktop · GitHub
[go: up one dir, main page]

Skip to content

Conversation

gastoner
Copy link
Contributor
@gastoner gastoner commented Aug 20, 2025

What does this PR do?

This PR adds service that fetches content from our website from docs and tutorial
And shows the content in searchbar under docs tab

Screenshot / video of UI

What issues does this PR fix or reference?

Closes #13660

How to test this PR?

  • Tests are covering the bug fix or the new feature

Copy link
Contributor
coderabbitai bot commented Aug 20, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds DocumentationInfo and DocumentationBaseInfo API types, a DocumentationService that fetches/parses docs.json and tutorials.json (with fallback to a bundled JSON), unit tests for the service, and a fallback-documentation asset. Wires the service into the main plugin DI and IPC handlers (documentation:getItems, documentation:refresh), exposes preload APIs (getDocumentationItems, refreshDocumentationItems), and integrates documentation items into the CommandPalette UI and its tests (new Documentation mode, filtering, navigation, and open-URL behavior).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • benoitf
  • Firewall
  • axel7083
  • jeffmaury
  • slemeur

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly indicates the introduction of documentation tab content, succinctly reflecting the main feature change implemented in the pull request.
Linked Issues Check ✅ Passed The changes introduce a documentation fetching service, integrate it through IPC and UI components, and include tests that fulfill the objective of populating the docs tab with website-sourced entries as specified in issue #13660.
Out of Scope Changes Check ✅ Passed All modifications, including service implementation, IPC handlers, UI updates, and test adjustments, are directly related to populating the documentation tab and do not introduce unrelated functionality.
Description Check ✅ Passed The description directly describes adding a service to fetch documentation and tutorials and displaying them under the docs tab, matching the changes in the pull request.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

@gastoner
Copy link
Contributor Author

So far this is just a try to one of many approaches to getting content into searchbar docs tab

Copy link
Contributor
@jiridostal jiridostal left a comment

Choose a reason for hiding this comment

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

Two things:

  • The selection of an item in the dropdown after search is not responding to "Enter" button. I would expect it to open the selected link?
  • Cmd+Q to close the App on Mac no longer works - related to keyboard events handling?

@gastoner gastoner changed the title [wip] feat: docs tab content feat: docs tab content Sep 3, 2025
@gastoner gastoner marked this pull request as ready for review September 3, 2025 06:04
@gastoner gastoner requested review from benoitf and a team as code owners September 3, 2025 06:04
@gastoner gastoner requested review from jeffmaury and simonrey1 and removed request for a team September 3, 2025 06:04
Copy link
Contributor
@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • The CommandPalette.svelte component is getting quite large and handles multiple modes now—consider extracting the documentation-tab logic and action execution into smaller subcomponents or utility modules for better readability and testability.
  • Using regex to parse HTML in DocumentationService.parseDocumentationContent can be brittle—consider using a proper HTML parser (e.g., DOMParser) to reliably extract links and metadata.
  • Avoid magic numeric indices for searchOptionsSelectedIndex by introducing a named enum or constants for each search mode to make the mode-switching logic clearer and less error-prone.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The CommandPalette.svelte component is getting quite large and handles multiple modes now—consider extracting the documentation-tab logic and action execution into smaller subcomponents or utility modules for better readability and testability.
- Using regex to parse HTML in DocumentationService.parseDocumentationContent can be brittle—consider using a proper HTML parser (e.g., DOMParser) to reliably extract links and metadata.
- Avoid magic numeric indices for searchOptionsSelectedIndex by introducing a named enum or constants for each search mode to make the mode-switching logic clearer and less error-prone.

## Individual Comments

### Comment 1
<location> `packages/main/src/plugin/documentation/documentation-service.ts:83` </location>
<code_context>
+    }
+  }
+
+  private parseDocumentationContent(docsHtml: string, tutorialHtml: string): DocumentationInfo[] {
+    const documentation: DocumentationInfo[] = [];
+
</code_context>

<issue_to_address>
Consider replacing manual regex parsing with a DOMParser helper and consolidating core page definitions into a shared constant to simplify the code.

```suggestion
Consider swapping the custom‐regex parsing for a small DOMParser-based helper and collapsing the duplicated “core” pages into a shared constant. This will flatten the nested loops, remove manual regex concerns, and DRY up your core/fallback lists.

1) Add a shared core pages constant and use it in both methods:
```ts
// at top of class
private static readonly CORE_PAGES: DocumentationInfo[] = [
  {
    id: 'docs-intro',
    title: 'Introduction & Getting Started',
    description: 'Learn the basics of Podman Desktop',
    url: 'https://podman-desktop.io/docs/intro',
    category: 'Documentation',
  },
  {
    id: 'tutorial-index',
    title: 'Tutorials & Guides',
    description: 'Step-by-step tutorials for common tasks',
    url: 'https://podman-desktop.io/tutorial',
    category: 'Tutorial',
  },
];

// replace methods
private getCoreDocumentationPages() {
  return DocumentationService.CORE_PAGES;
}

private getFallbackDocumentation() {
  return [
    ...DocumentationService.CORE_PAGES,
    { id: 'docs-containers', /* … */ },
    { id: 'docs-kubernetes', /* … */ },
    /* remaining extra fallback pages */
  ];
}
```

2) Extract link‐parsing into a DOM helper:
```ts
private extractLinks(
  html: string,
  category: string,
  segment: string
): DocumentationInfo[] {
  const doc = new DOMParser().parseFromString(html, 'text/html');
  return Array.from(doc.querySelectorAll(`a[href*="${segment}"]`))
    .map(a => {
      const href = a.getAttribute('href')!;
      const title = a.textContent?.trim() || '';
      const url = href.startsWith('http') ? href : `https://podman-desktop.io${href}`;
      return {
        id: `${category}-${this.generateId(title)}`,
        title,
        description: `${category}: ${title}`,
        url,
        category,
      };
    })
    .filter(({ title }) => title && !/(Edit this page|Next)/.test(title));
}
```

3) Simplify `parseDocumentationContent`:
```ts
private parseDocumentationContent(docsHtml: string, tutorialHtml: string) {
  if (!docsHtml || !tutorialHtml) {
    console.warn('Missing HTML…');
    return this.getCoreDocumentationPages();
  }

  const docsLinks     = this.extractLinks(docsHtml,     'Documentation', '/docs/');
  const tutorialLinks = this.extractLinks(tutorialHtml, 'Tutorial',      '/tutorial/');

  return [
    ...this.getCoreDocumentationPages(),
    ...this.removeDuplicates([...docsLinks, ...tutorialLinks]),
  ];
}
```

This removes regex complexity, flattens loops, and coalesces duplicated page definitions without changing functionality.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

}
}

private parseDocumentationContent(docsHtml: string, tutorialHtml: string): DocumentationInfo[] {
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (complexity): Consider replacing manual regex parsing with a DOMParser helper and consolidating core page definitions into a shared constant to simplify the code.

Suggested change
private parseDocumentationContent(docsHtml: string, tutorialHtml: string): DocumentationInfo[] {
Consider swapping the custom‐regex parsing for a small DOMParser-based helper and collapsing the duplicated “core” pages into a shared constant. This will flatten the nested loops, remove manual regex concerns, and DRY up your core/fallback lists.
1) Add a shared core pages constant and use it in both methods:
```ts
// at top of class
private static readonly CORE_PAGES: DocumentationInfo[] = [
{
id: 'docs-intro',
title: 'Introduction & Getting Started',
description: 'Learn the basics of Podman Desktop',
url: 'https://podman-desktop.io/docs/intro',
category: 'Documentation',
},
{
id: 'tutorial-index',
title: 'Tutorials & Guides',
description: 'Step-by-step tutorials for common tasks',
url: 'https://podman-desktop.io/tutorial',
category: 'Tutorial',
},
];
// replace methods
private getCoreDocumentationPages() {
return DocumentationService.CORE_PAGES;
}
private getFallbackDocumentation() {
return [
...DocumentationService.CORE_PAGES,
{ id: 'docs-containers', /* … */ },
{ id: 'docs-kubernetes', /* … */ },
/* remaining extra fallback pages */
];
}
  1. Extract link‐parsing into a DOM helper:
private extractLinks(
  html: string,
  category: string,
  segment: string
): DocumentationInfo[] {
  const doc = new DOMParser().parseFromString(html, 'text/html');
  return Array.from(doc.querySelectorAll(`a[href*="${segment}"]`))
    .map(a => {
      const href = a.getAttribute('href')!;
      const title = a.textContent?.trim() || '';
      const url = href.startsWith('http') ? href : `https://podman-desktop.io${href}`;
      return {
        id: `${category}-${this.generateId(title)}`,
        title,
        description: `${category}: ${title}`,
        url,
        category,
      };
    })
    .filter(({ title }) => title && !/(Edit this page|Next)/.test(title));
}
  1. Simplify parseDocumentationContent:
private parseDocumentationContent(docsHtml: string, tutorialHtml: string) {
  if (!docsHtml || !tutorialHtml) {
    console.warn('Missing HTML…');
    return this.getCoreDocumentationPages();
  }

  const docsLinks     = this.extractLinks(docsHtml,     'Documentation', '/docs/');
  const tutorialLinks = this.extractLinks(tutorialHtml, 'Tutorial',      '/tutorial/');

  return [
    ...this.getCoreDocumentationPages(),
    ...this.removeDuplicates([...docsLinks, ...tutorialLinks]),
  ];
}

This removes regex complexity, flattens loops, and coalesces duplicated page definitions without changing functionality.

Copy link
Contributor
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
packages/renderer/src/lib/dialogs/CommandPalette.svelte (1)

199-211: Prevent potential crash when scrollElements entry is missing

Switch to optional chaining; filtered list length can change between renders, leaving holes.

-    scrollElements[selectedFilteredIndex].scrollIntoView({
+    scrollElements[selectedFilteredIndex]?.scrollIntoView({
@@
-    scrollElements[selectedFilteredIndex].scrollIntoView({
+    scrollElements[selectedFilteredIndex]?.scrollIntoView({

Also applies to: 218-223

🧹 Nitpick comments (14)
packages/api/src/documentation-info.ts (1)

19-25: Tighten the type surface (readonly fields + discriminated category).

Make fields immutable and prevent category typos with a literal union.

+export type DocumentationCategory = 'Documentation' | 'Tutorial';

-export interface DocumentationInfo {
-  id: string;
-  title: string;
-  description: string;
-  url: string;
-  category: string;
-}
+export interface DocumentationInfo {
+  readonly id: string;
+  readonly title: string;
+  readonly description: string;
+  readonly url: string;
+  readonly category: DocumentationCategory;
+}
packages/main/src/plugin/documentation/documentation-service.spec.ts (1)

25-27: Restore global fetch after tests to avoid cross-suite leaks.

Overriding a global without restoration can create hard-to-debug flakes.

-const mockFetch = vi.fn();
-global.fetch = mockFetch;
+const originalFetch = global.fetch;
+const mockFetch = vi.fn();
+global.fetch = mockFetch;
+
+// ...
+
+afterAll(() => {
+  global.fetch = originalFetch;
+});
packages/main/src/plugin/index.ts (1)

2102-2109: IPC wiring for docs get/refresh is solid; consider event consumption guidance.

Service likely emits 'documentation-updated' via ApiSender; ensure the renderer subscribes to window.events.receive('documentation-updated', ...) to refresh UI reactively after documentation:refresh.

packages/renderer/src/lib/dialogs/CommandPalette.spec.ts (4)

43-59: Make window stubs configurable to avoid hard-to-debug test pollution

Define mocked window properties with configurable: true so subsequent tests (or other suites) can redefine/override them cleanly.

-  Object.defineProperty(window, 'executeCommand', {
-    value: executeCommandMock,
-  });
+  Object.defineProperty(window, 'executeCommand', {
+    value: executeCommandMock,
+    configurable: true,
+  });
@@
-  Object.defineProperty(window, 'openExternal', {
-    value: openExternalMock,
-  });
+  Object.defineProperty(window, 'openExternal', {
+    value: openExternalMock,
+    configurable: true,
+  });
@@
-  Object.defineProperty(window, 'getOsPlatform', {
-    value: getOsPlatformMock,
-  });
+  Object.defineProperty(window, 'getOsPlatform', {
+    value: getOsPlatformMock,
+    configurable: true,
+  });
@@
-  Object.defineProperty(window, 'getDocumentationItems', {
-    value: getDocumentationItemsMock,
-  });
+  Object.defineProperty(window, 'getDocumentationItems', {
+    value: getDocumentationItemsMock,
+    configurable: true,
+  });

436-454: Avoid variable-based RegExp in queries; use exact string match

Switch to a string name to remove the variable-RegExp warning and reduce flakiness.

-      const expectedTab = screen.getByRole('button', { name: new RegExp(expectedTabText, 'i') });
+      const expectedTab = screen.getByRole('button', { name: expectedTabText });

441-454: Prefer role/aria over CSS-class assertions for tab selection

Asserting styling classes is brittle. If possible, assert semantic state (e.g., role="tab" + aria-selected). See Svelte change below to expose aria-selected; then here query getAllByRole('tab') and assert aria-selected.

If you adopt the Svelte change to add role="tab" and aria-selected, replace these assertions with:

-  const allTabButtons = screen.getAllByRole('button').filter( /* class-based filter */ );
+  const allTabButtons = screen.getAllByRole('tab');
@@
-  expect(allTabButtons[0]).toHaveClass('text-[var(--pd-button-tab-text-selected)]');
+  expect(allTabButtons[0]).toHaveAttribute('aria-selected', 'true');

Also applies to: 477-485


396-418: Add a test that documentation items open externally

We’re mocking getDocumentationItems; add a positive-path test to ensure docs are listed and open via window.openExternal.

+  test('Documentation tab shows items and opens selection', async () => {
+    const docs = [{ id: 'docs-intro', title: 'Intro', description: 'Start', url: 'https://podman-desktop.io/docs/intro', category: 'Documentation' }];
+    getDocumentationItemsMock.mockResolvedValueOnce(docs);
+
+    render(CommandPalette);
+    await userEvent.keyboard('{Control>}k{/Control}'); // open on Documentation tab
+    const input = screen.getByRole('textbox', { name: COMMAND_PALETTE_ARIA_LABEL });
+    expect(input).toBeInTheDocument();
+
+    // item should appear and be selected by default
+    const item = await screen.findByRole('button', { name: /Intro/ });
+    await userEvent.click(item);
+    expect(openExternalMock).toHaveBeenCalledWith('https://podman-desktop.io/docs/intro');
+  });
packages/main/src/plugin/documentation/documentation-service.ts (2)

36-46: Gracefully degrade if only one endpoint succeeds

Don’t discard successfully fetched content if the other page fails. Parse what’s available and add core/fallback entries for the missing set.

-      const [docsContent, tutorialContent] = await Promise.all([
+      const [docsContent, tutorialContent] = await Promise.allSettled([
         this.fetchPageContent('https://podman-desktop.io/docs/intro'),
-        this.fetchPageContent('https://podman-desktop.io/tutorial'),
+        this.fetchPageContent('https://podman-desktop.io/tutorial'),
       ]);
-
-      if (docsContent && tutorialContent) {
-        this.documentation = this.parseDocumentationContent(docsContent, tutorialContent);
-      } else {
-        throw new Error('Failed to fetch documentation content');
-      }
+      const docsOk = docsContent.status === 'fulfilled' ? docsContent.value : undefined;
+      const tutOk = tutorialContent.status === 'fulfilled' ? tutorialContent.value : undefined;
+      if (docsOk || tutOk) {
+        this.documentation = this.parseDocumentationContent(docsOk ?? '', tutOk ?? '');
+      } else {
+        throw new Error('Failed to fetch documentation content');
+      }

83-131: Regex-on-HTML is fragile; prefer DOM parsing

Anchors often contain nested elements and attributes; regex will miss them or misparse. Consider a lightweight HTML parser (e.g., node-html-parser) to select 'a' tags under known containers and extract href/textContent safely, then resolve relative URLs.

I can provide a small parser helper using node-html-parser and targeted selectors if you’d like.

packages/renderer/src/lib/dialogs/CommandPalette.svelte (5)

236-241: Reset selection and refocus when switching tabs

Keeps UX consistent and prevents stale indices across modes.

 function switchSearchOption(direction: 1 | -1): void {
   const searchOptionsLength = searchOptions.length;
   const offset = direction === 1 ? 0 : searchOptionsLength;
   searchOptionsSelectedIndex = (searchOptionsSelectedIndex + direction + offset) % searchOptionsLength;
+  selectedFilteredIndex = 0;
+  tick().then(() => inputElement?.focus()).catch(() => {});
 }

339-356: Apply the same reset when clicking a tab button

Mirror keyboard tab-switch logic for mouse interaction.

-              on:click={(): void => {
-                searchOptionsSelectedIndex = index;
-              }}
+              on:click={(): void => {
+                searchOptionsSelectedIndex = index;
+                selectedFilteredIndex = 0;
+                tick().then(() => inputElement?.focus()).catch(() => {});
+              }}

364-371: Use Svelte’s on:click instead of inline onclick

Keeps event binding consistent and avoids confusion with DOM inline handlers.

-              <button
-                onclick={(): Promise<void> => clickOnItem(i)}
+              <button
+                on:click={(): Promise<void> => clickOnItem(i)}

282-285: Fix spelling: hideCommandPallete ➜ hideCommandPalette

Rename function and call sites.

-function hideCommandPallete(): void {
+function hideCommandPalette(): void {
   display = false;
   onclose?.();
 }
@@
-    hideCommandPallete();
+    hideCommandPalette();
@@
-    hideCommandPallete();
+    hideCommandPalette();
@@
-    hideCommandPallete();
+    hideCommandPalette();
@@
-  hideCommandPallete();
+  hideCommandPalette();

Also applies to: 174-176, 225-229, 273-279, 288-294


131-153: Listen for 'documentation-updated' to refresh items reactively

When main triggers a refresh, update the renderer list without reopening the palette.

   if (display && documentationItems.length === 0 && !isLoadingDocumentation) {
@@
   }
+  // react to updates pushed from main
+  window?.events?.receive?.('documentation-updated', async () => {
+    try {
+      isLoadingDocumentation = true;
+      documentationItems = await window.getDocumentationItems();
+    } catch (e) {
+      console.error('Failed to refresh documentation items', e);
+    } finally {
+      isLoadingDocumentation = false;
+    }
+  });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6450caa and 8da2b0d.

📒 Files selected for processing (7)
  • packages/api/src/documentation-info.ts (1 hunks)
  • packages/main/src/plugin/documentation/documentation-service.spec.ts (1 hunks)
  • packages/main/src/plugin/documentation/documentation-service.ts (1 hunks)
  • packages/main/src/plugin/index.ts (4 hunks)
  • packages/preload/src/index.ts (2 hunks)
  • packages/renderer/src/lib/dialogs/CommandPalette.spec.ts (14 hunks)
  • packages/renderer/src/lib/dialogs/CommandPalette.svelte (10 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
packages/main/src/plugin/documentation/documentation-service.ts (1)
packages/api/src/documentation-info.ts (1)
  • DocumentationInfo (19-25)
packages/renderer/src/lib/dialogs/CommandPalette.spec.ts (1)
tests/playwright/src/model/pages/command-palette.ts (1)
  • CommandPalette (24-44)
packages/preload/src/index.ts (1)
packages/api/src/documentation-info.ts (1)
  • DocumentationInfo (19-25)
packages/main/src/plugin/index.ts (1)
packages/api/src/documentation-info.ts (1)
  • DocumentationInfo (19-25)
🪛 ast-grep (0.38.6)
packages/renderer/src/lib/dialogs/CommandPalette.spec.ts

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

(regexp-from-variable)

🔇 Additional comments (8)
packages/api/src/documentation-info.ts (1)

1-17: License header looks good.

SPDX and Apache-2.0 header are correct and consistent with the repo.

packages/preload/src/index.ts (2)

65-65: Type-only import is correct.

Tree-shake friendly and consistent with the file’s import style.


1602-1609: IPC bridge for docs: API shape is clear and consistent.

Channel names match main process handlers; return types are precise. LGTM.

packages/main/src/plugin/documentation/documentation-service.spec.ts (1)

137-173: The scripts were triggered to locate and inspect the fetchDocumentation and getDocumentationItems methods. Once we have their implementations, we can confirm whether the service re-fetches on each call or caches results after initialization.

packages/main/src/plugin/index.ts (3)

87-87: Public type import is correct.

Keeps runtime bundle clean while providing types to IPC handlers.


162-162: Service import aligns with DI usage below.

No concerns.


725-727: DI binding for DocumentationService is appropriate.

Singleton scope is the right choice for caching and event emission.

packages/main/src/plugin/documentation/documentation-service.ts (1)

37-39: Tutorials URL path is correct The code already uses /tutorial, which matches the official tutorials index.

Comment on lines 63 to 81
private async fetchPageContent(url: string): Promise<string> {
try {
const response = await fetch(url);

if (!response.ok) {
throw new Error(`HTTP ${response.< 6D40 span class=pl-c1>status}: ${response.statusText}`);
}

return await response.text();
} catch (error) {
if (error instanceof Error) {
if (error.name === 'AbortError') {
throw new Error(`Request timeout while fetching ${url}`);
}
throw new Error(`Failed to fetch ${url}: ${error.message}`);
}
throw new Error(`Failed to fetch ${url}: Unknown error`);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add request timeouts to fetchPageContent (AbortController)

You already handle AbortError but never set a timeout. Add AbortController to avoid hung network calls.

-  private async fetchPageContent(url: string): Promise<string> {
+  private async fetchPageContent(url: string): Promise<string> {
     try {
-      const response = await fetch(url);
+      const controller = new AbortController();
+      const timeout = setTimeout(() => controller.abort(), 8000);
+      const response = await fetch(url, {
+        signal: controller.signal,
+        headers: { Accept: 'text/html' },
+      });
+      clearTimeout(timeout);
@@
-    } catch (error) {
+    } catch (error) {
       if (error instanceof Error) {
         if (error.name === 'AbortError') {
           throw new Error(`Request timeout while fetching ${url}`);
         }
         throw new Error(`Failed to fetch ${url}: ${error.message}`);
       }
       throw new Error(`Failed to fetch ${url}: Unknown error`);
     }
   }
📝 Committable suggestion

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

Suggested change
private async fetchPageContent(url: string): Promise<string> {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.text();
} catch (error) {
if (error instanceof Error) {
if (error.name === 'AbortError') {
throw new Error(`Request timeout while fetching ${url}`);
}
throw new Error(`Failed to fetch ${url}: ${error.message}`);
}
throw new Error(`Failed to fetch ${url}: Unknown error`);
}
}
private async fetchPageContent(url: string): Promise<string> {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
const response = await fetch(url, {
signal: controller.signal,
headers: { Accept: 'text/html' },
});
clearTimeout(timeout);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.text();
} catch (error) {
if (error instanceof Error) {
if (error.name === 'AbortError') {
throw new Error(`Request timeout while fetching ${url}`);
}
throw new Error(`Failed to fetch ${url}: ${error.message}`);
}
throw new Error(`Failed to fetch ${url}: Unknown error`);
}
}
🤖 Prompt for AI Agents
packages/main/src/plugin/documentation/documentation-service.ts around lines 63
to 81: fetchPageContent currently checks for AbortError but never sets a
timeout, so network requests can hang; add an AbortController, pass its signal
to fetch, start a timer (e.g., const timeoutId = setTimeout(() =>
controller.abort(), <desired ms>)) before calling fetch, clearTimeout(timeoutId)
after a successful response or when throwing, and in the catch keep the
AbortError branch throwing a clear timeout error message; ensure the controller
and timeout are properly typed/cleaned up to avoid leaks.

Comment on lines 100 to 107
onMount(async () => {
const platform = await window.getOsPlatform();
isMac = platform === 'darwin';
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard getOsPlatform() with try/catch to avoid unhandled rejections

If the preload API isn’t available, this throws on mount.

-onMount(async () => {
-  const platform = await window.getOsPlatform();
-  isMac = platform === 'darwin';
-});
+onMount(async () => {
+  try {
+    const platform = await window.getOsPlatform();
+    isMac = platform === 'darwin';
+  } catch (e) {
+    console.warn('getOsPlatform unavailable; defaulting to non-mac', e);
+    isMac = false;
+  }
+});
📝 Committable suggestion

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

Suggested change
onMount(async () => {
const platform = await window.getOsPlatform();
isMac = platform === 'darwin';
});
onMount(async () => {
try {
const platform = await window.getOsPlatform();
isMac = platform === 'darwin';
} catch (e) {
console.warn('getOsPlatform unavailable; defaulting to non-mac', e);
isMac = false;
}
});
🤖 Prompt for AI Agents
In packages/renderer/src/lib/dialogs/CommandPalette.svelte around lines 100 to
104, the onMount call awaits window.getOsPlatform() without guard
6D38
ing against the
preload API being missing which can cause an unhandled rejection; wrap the await
in a try/catch (or first check typeof window.getOsPlatform === 'function') and
set a safe default for isMac (e.g., false) in the catch so mount never throws
and isMac remains defined.

Copy link
Contributor
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (10)
packages/api/src/documentation-info.ts (1)

19-25: Consider making the DTO immutable and lightly documented.

Readonly fields prevent accidental mutation across renderer/preload/main. Minimal JSDoc improves DX without changing runtime.

Apply:

-export interface DocumentationInfo {
-  id: string;
-  title: string;
-  description: string;
-  url: string;
-  category: string;
-}
+/**
+ * Canonical documentation entry rendered in the Command Palette "Documentation" tab.
+ * All fields are intended to be immutable.
+ */
+export interface DocumentationInfo {
+  readonly id: string;
+  readonly title: string;
+  readonly description: string;
+  readonly url: string;
+  readonly category: string;
+}
packages/renderer/src/lib/dialogs/CommandPalette.spec.ts (9)

21-23: Using fireEvent directly is fine; prefer userEvent.keyboard for key semantics.

userEvent synthesizes realistic key sequences (keydown/keypress/keyup) and default modifiers. Not required to change, but it can reduce flakiness.


43-59: Make window property mocks configurable/writable to ease cleanup.

defineProperty defaults to non-configurable; future overrides/teardown might fail. Mark them configurable/writable.

Apply:

   Object.defineProperty(window, 'executeCommand', {
-    value: executeCommandMock,
+    value: executeCommandMock,
+    configurable: true,
+    writable: true,
   });
 
   Object.defineProperty(window, 'openExternal', {
-    value: openExternalMock,
+    value: openExternalMock,
+    configurable: true,
+    writable: true,
   });
 
   Object.defineProperty(window, 'getOsPlatform', {
-    value: getOsPlatformMock,
+    value: getOsPlatformMock,
+    configurable: true,
+    writable: true,
   });
 
   Object.defineProperty(window, 'getDocumentationItems', {
-    value: getDocumentationItemsMock,
+    value: getDocumentationItemsMock,
+    configurable: true,
+    writable: true,
   });

117-129: Navigation test reads well; minor robustness nit.

Firing ArrowDown on window matches a global listener. If the component ever scopes handlers, prefer userEvent.keyboard('{ArrowDown}') after focusing the input.

Also applies to: 135-141, 156-157


181-191: Same nit as above for ArrowUp tests.

Switching to userEvent.keyboard would keep event ordering realistic.

Also applies to: 198-217, 226-227


246-268: Enter key test is clear; assert side effects only once.

Optional: also assert that the palette is closed if that’s expected UX.


298-331: Filtering test is solid; minor stability suggestion.

Prefer findByRole for the post-filter first item to avoid any render timing edge cases before submitting Enter.


366-368: Avoid arbitrary sleeps to reduce flakiness.

The context is set before render; the 100ms delay is unnecessary.

Apply:

-  // wait a little bit for the context to be updated
-  await new Promise(resolve => setTimeout(resolve, 100));

386-394: Enablement test: Enter triggers first enabled item — good.

Optional: also verify disabled items aren’t focusable/selectable via keyboard.


396-455: Tab selection assertions rely on CSS classes; prefer ARIA state.

If feasible, expose tabs with role="tab" and aria-selected to assert selection. This improves a11y and makes tests less brittle to style refactors.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8da2b0d and 2065c1e.

📒 Files selected for processing (7)
  • packages/api/src/documentation-info.ts (1 hunks)
  • packages/main/src/plugin/documentation/documentation-service.spec.ts (1 hunks)
  • packages/main/src/plugin/documentation/documentation-service.ts (1 hunks)
  • packages/main/src/plugin/index.ts (4 hunks)
  • packages/preload/src/index.ts (2 hunks)
  • packages/renderer/src/lib/dialogs/CommandPalette.spec.ts (13 hunks)
  • packages/renderer/src/lib/dialogs/CommandPalette.svelte (10 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/preload/src/index.ts
  • packages/main/src/plugin/documentation/documentation-service.ts
  • packages/main/src/plugin/index.ts
  • packages/main/src/plugin/documentation/documentation-service.spec.ts
  • packages/renderer/src/lib/dialogs/CommandPalette.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: unit tests / ubuntu-24.04
  • GitHub Check: unit tests / macos-15
  • GitHub Check: unit tests / windows-2025
  • GitHub Check: smoke e2e tests (development)
  • GitHub Check: smoke e2e tests (production)
  • GitHub Check: typecheck
  • GitHub Check: linter, formatters
  • GitHub Check: Windows
  • GitHub Check: build website
  • GitHub Check: Linux
  • GitHub Check: macOS
🔇 Additional comments (5)
packages/api/src/documentation-info.ts (1)

1-17: License header looks good.

Apache-2.0 header is correctly included and formatted.

packages/renderer/src/lib/dialogs/CommandPalette.spec.ts (4)

31-35: Mocks declaration LGTM.

Clear separation of mocks improves readability.


61-63: Good defaults for platform and docs items.

Keeps tests deterministic across OS.


457-505: Forward Tab cycling test is comprehensive.

Good coverage of wrap-around behavior.


507-555: Backward Shift+Tab cycling test is comprehensive.

Mirrors forward flow and validates wrap-around.

Comment on lines 19 to 30
export interface DocumentationInfo {
id: string;
title: string;
description: string;
url: string;
category: string;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify import path consistency for this type.

Some codebases import TS types with a .js suffix for ESM compatibility; others omit it. Ensure consistent usage to avoid resolution issues in mixed CJS/ESM and when running tests.

Run:


🏁 Script executed:

#!/bin/bash
# Find all imports of documentation-info with/without .js suffix
rg -nP "from ['\"]/@api/documentation-info(\.js)?['\"]" -C2

Length of output: 2219


Unify import paths for DocumentationInfo
Imports of DocumentationInfo are inconsistent: in packages/preload/src/index.ts (line 65) and packages/renderer/src/lib/dialogs/CommandPalette.svelte (line 11) the path omits “.js”, but in packages/main/src/plugin/... the path includes “.js”. Update the preload and renderer imports to use /@api/documentation-info.js for ESM compatibility.

🤖 Prompt for AI Agents
In packages/api/src/documentation-info.ts around lines 19–25, the
DocumentationInfo interface is exported but other packages import it with
inconsistent paths; update the imports in packages/preload/src/index.ts (line
~65) and packages/renderer/src/lib/dialogs/CommandPalette.svelte (line ~11) to
use the ESM-compatible path "/@api/documentation-info.js" (replacing any imports
that omit the .js extension), and verify the project alias/resolution
configuration still resolves /@api to the compiled API bundle.

Signed-off-by: Evzen Gasta <evzen.ml@seznam.cz>
…chbar

Signed-off-by: Evzen Gasta <evzen.ml@seznam.cz>
Assisted by: Cursor
Signed-off-by: Evzen Gasta <evzen.ml@seznam.cz>
Signed-off-by: Evzen Gasta <evzen.ml@seznam.cz>
@gastoner
Copy link
Contributor Author
gastoner commented Oct 3, 2025

@axel7083 @jiridostal can you PTAL?

Signed-off-by: Evzen Gasta <evzen.ml@seznam.cz>
Asisted by: Cursor
@gastoner gastoner force-pushed the global_navbar_docs branch from c274493 to cf19386 Compare October 3, 2025 07:33
Copy link
Contributor
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
packages/main/src/plugin/documentation/documentation-service.ts (2)

139-142: Deduplicate across core pages and parsed items.

The current implementation only removes duplicates within the parsed documentation array but does not deduplicate between corePages and parsed items. If the JSON contains entries that match core pages, they will appear twice in the final list.

Apply this diff to deduplicate across all items:

   // Remove duplicates and add core pages
   const corePages = this.getCoreDocumentationPages();
-  return [...corePages, ...this.removeDuplicates(documentation)];
+  return this.removeDuplicates([...corePages, ...documentation]);

Based on previous review comments.


62-67: Return a copy and dedupe concurrent initialization calls.

getDocumentationItems() has two issues:

  1. It returns the internal documentation array by reference, allowing callers to mutate internal state.
  2. Concurrent calls before initialization will trigger multiple fetchDocumentation() calls, causing redundant network requests.

Apply this diff to address both concerns:

 export class DocumentationService extends Disposable {
   private documentation: DocumentationInfo[] = [];
   private isInitialized = false;
+  private initPromise?: Promise<void>;
 
   constructor(
     @inject(ApiSenderType)
     private apiSender: ApiSenderType,
   ) {
     super(() => {
       this.documentation = [];
       this.isInitialized = false;
+      this.initPromise = undefined;
     });
   }
 
   async getDocumentationItems(): Promise<DocumentationInfo[]> {
     if (!this.isInitialized) {
-      await this.fetchDocumentation();
+      this.initPromise ??= this.fetchDocumentation().finally(() => {
+        this.initPromise = undefined;
+      });
+      await this.initPromise;
     }
-    return this.documentation;
+    return [...this.documentation];
   }

Also update refreshDocumentation() to reset the promise:

 async refreshDocumentation(): Promise<void> {
   this.isInitialized = false; // Force re-fetch
+  this.initPromise = undefined;
   await this.fetchDocumentation();
   this.apiSender.send('documentation-updated');
 }

Based on previous review comments.

🧹 Nitpick comments (2)
packages/main/src/plugin/documentation/documentation-service.ts (2)

26-39: Consider using @preDestroy() instead of extending Disposable.

As suggested in a previous review, using inversify's @preDestroy() decorator would be more idiomatic for dependency-injected classes. This would eliminate the need to extend Disposable and call super() with a cleanup function.

Apply this diff to use @preDestroy():

+import { inject, injectable, preDestroy } from 'inversify';
+
-import { Disposable } from '../types/disposable.js';
 
 @injectable()
-export class DocumentationService extends Disposable {
+export class DocumentationService {
   private documentation: DocumentationInfo[] = [];
   private isInitialized = false;
 
   constructor(
     @inject(ApiSenderType)
     private apiSender: ApiSenderType,
-  ) {
-    super(() => {
-      this.documentation = [];
-      this.isInitialized = false;
-    });
-  }
+  ) {}
+
+  @preDestroy()
+  dispose(): void {
+    this.documentation = [];
+    this.isInitialized = false;
+  }

Based on previous review comments.


144-208: Consolidate core pages into a shared constant to eliminate duplication.

The same two core items (docs-intro and tutorial-index) are duplicated in both getCoreDocumentationPages() and getFallbackDocumentation(). This makes maintenance harder and increases the risk of inconsistency.

Apply this diff to use a shared constant:

 @injectable()
 export class DocumentationService extends Disposable {
+  private static readonly CORE_PAGES: DocumentationInfo[] = [
+    {
+      id: 'docs-intro',
+      title: 'Introduction & Getting Started',
+      description: 'Learn the basics of Podman Desktop',
+      url: 'https://podman-desktop.io/docs/intro',
+      category: 'Documentation',
+    },
+    {
+      id: 'tutorial-index',
+      title: 'Tutorials & Guides',
+      description: 'Step-by-step tutorials for common tasks',
+      url: 'https://podman-desktop.io/tutorial',
+      category: 'Tutorial',
+    },
+  ];
+
   private documentation: DocumentationInfo[] = [];
   private isInitialized = false;
   
   // ...
   
   private getCoreDocumentationPages(): DocumentationInfo[] {
-    return [
-      {
-        id: 'docs-intro',
-        title: 'Introduction & Getting Started',
-        description: 'Learn the basics of Podman Desktop',
-        url: 'https://podman-desktop.io/docs/intro',
-        category: 'Documentation',
-      },
-      {
-        id: 'tutorial-index',
-        title: 'Tutorials & Guides',
-        description: 'Step-by-step tutorials for common tasks',
-        url: 'https://podman-desktop.io/tutorial',
-        category: 'Tutorial',
-      },
-    ];
+    return DocumentationService.CORE_PAGES;
   }
   
   private getFallbackDocumentation(): DocumentationInfo[] {
     return [
-      {
-        id: 'docs-intro',
-        title: 'Introduction & Getting Started',
-        description: 'Learn the basics of Podman Desktop',
-        url: 'https://podman-desktop.io/docs/intro',
-        category: 'Documentation',
-      },
-      {
-        id: 'tutorial-index',
-        title: 'Tutorials & Guides',
-        description: 'Step-by-step tutorials for common tasks',
-        url: 'https://podman-desktop.io/tutorial',
-        category: 'Tutorial',
-      },
+      ...DocumentationService.CORE_PAGES,
       {
         id: 'docs-containers',
         title: 'Containers Documentation',
         description: 'Working with containers, images, and pods',
         url: 'https://podman-desktop.io/docs/containers',
         category: 'Documentation',
       },
       // ... rest of fallback items
     ];
   }

Based on previous review comments.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c274493 and cf19386.

📒 Files selected for processing (3)
  • packages/main/src/plugin/documentation/documentation-service.spec.ts (1 hunks)
  • packages/main/src/plugin/documentation/documentation-service.ts (1 hunks)
  • packages/preload/src/index.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/preload/src/index.ts
  • packages/main/src/plugin/documentation/documentation-service.spec.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/main/src/plugin/documentation/documentation-service.ts (2)
packages/extension-api/src/extension-api.d.ts (1)
  • Disposable (90-118)
packages/api/src/documentation-info.ts (2)
  • DocumentationInfo (24-30)
  • DocumentationJsonInfo (19-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: unit tests / ubuntu-24.04
  • GitHub Check: unit tests / macos-15
  • GitHub Check: smoke e2e tests (development)
  • GitHub Check: smoke e2e tests (production)
  • GitHub Check: unit tests / windows-2025
  • GitHub Check: Windows
  • GitHub Check: linter, formatters
  • GitHub Check: typecheck
  • GitHub Check: build website
  • GitHub Check: Linux
  • GitHub Check: macOS
  • GitHub Check: Sourcery review
🔇 Additional comments (2)
packages/main/src/plugin/documentation/documentation-service.ts (2)

210-246: LGTM: Utility methods are well-implemented.

Both removeDuplicates() and generateId() are correctly implemented with appropriate defensive programming:

  • removeDuplicates() efficiently uses a Set for O(1) lookups and validates items before processing.
  • generateId() handles edge cases (invalid input, empty strings, trailing dashes) and generates safe, URL-friendly IDs with a reasonable length limit.

69-73: apiSender.send invocation is correct
The send method’s data parameter is optional (data?), so omitting the payload is valid.

Comment on lines 41 to 60
async fetchDocumentation(): Promise<void> {
try {
const [docsJson, tutorialsJson] = await Promise.all([
this.fetchJsonContent('https://podman-desktop.io/docs.json'),
this.fetchJsonContent('https://podman-desktop.io/tutorials.json'),
]);

if (docsJson && tutorialsJson) {
this.documentation = this.parseDocumentationFromJson(docsJson, tutorialsJson);
} else {
throw new Error('Failed to fetch documentation JSON files');
}
this.isInitialized = true;
} catch (error) {
console.error('Failed to fetch documentation at startup:', error);
// Fallback to predefined documentation if fetching fails
this.documentation = this.getFallbackDocumentation();
this.isInitialized = true;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add request timeout to prevent hanging network calls.

The fetch operations in fetchDocumentation() do not have a timeout, which could cause the method to hang indefinitely if the network is slow or unresponsive. While the fallback mechanism handles errors, it won't help if the request simply hangs.

Consider adding a ti 65CE meout using AbortController:

 async fetchDocumentation(): Promise<void> {
   try {
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 8000);
+
     const [docsJson, tutorialsJson] = await Promise.all([
-      this.fetchJsonContent('https://podman-desktop.io/docs.json'),
-      this.fetchJsonContent('https://podman-desktop.io/tutorials.json'),
+      this.fetchJsonContent('https://podman-desktop.io/docs.json', controller.signal),
+      this.fetchJsonContent('https://podman-desktop.io/tutorials.json', controller.signal),
     ]);
+    clearTimeout(timeoutId);
 
     if (docsJson && tutorialsJson) {
       this.documentation = this.parseDocumentationFromJson(docsJson, tutorialsJson);
     } else {
       throw new Error('Failed to fetch documentation JSON files');
     }
     this.isInitialized = true;
   } catch (error) {
     console.error('Failed to fetch documentation at startup:', error);
     // Fallback to predefined documentation if fetching fails
     this.documentation = this.getFallbackDocumentation();
     this.isInitialized = true;
   }
 }

Update fetchJsonContent signature to accept an optional signal:

-private async fetchJsonContent(url: string): Promise<Array<{ name: string; url: string }> | null> {
+private async fetchJsonContent(url: string, signal?: AbortSignal): Promise<Array<{ name: string; url: string }> | null> {
   try {
-    const response = await fetch(url);
+    const response = await fetch(url, { signal });

Based on previous review comments.

📝 Committable suggestion

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

Suggested change
async fetchDocumentation(): Promise<void> {
try {
const [docsJson, tutorialsJson] = await Promise.all([
this.fetchJsonContent('https://podman-desktop.io/docs.json'),
this.fetchJsonContent('https://podman-desktop.io/tutorials.json'),
]);
if (docsJson && tutorialsJson) {
this.documentation = this.parseDocumentationFromJson(docsJson, tutorialsJson);
} else {
throw new Error('Failed to fetch documentation JSON files');
}
this.isInitialized = true;
} catch (error) {
console.error('Failed to fetch documentation at startup:', error);
// Fallback to predefined documentation if fetching fails
this.documentation = this.getFallbackDocumentation();
this.isInitialized = true;
}
}
// packages/main/src/plugin/documentation/documentation-service.ts
// Updated fetchDocumentation with timeout
async fetchDocumentation(): Promise<void> {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 8000);
const [docsJson, tutorialsJson] = await Promise.all([
this.fetchJsonContent('https://podman-desktop.io/docs.json', controller.signal),
this.fetchJsonContent('https://podman-desktop.io/tutorials.json', controller.signal),
]);
clearTimeout(timeoutId);
if (docsJson && tutorialsJson) {
this.documentation = this.parseDocumentationFromJson(docsJson, tutorialsJson);
} else {
throw new Error('Failed to fetch documentation JSON files');
}
this.isInitialized = true;
} catch (error) {
console.error('Failed to fetch d 38BA ocumentation at startup:', error);
// Fallback to predefined documentation if fetching fails
this.documentation = this.getFallbackDocumentation();
this.isInitialized = true;
}
}
// Also update fetchJsonContent to accept an optional AbortSignal:
private async fetchJsonContent(
url: string,
signal?: AbortSignal
): Promise<Array<{ name: string; url: string }> | null> {
try {
const response = await fetch(url, { signal });
if (!response.ok) {
console.warn(`Failed to fetch JSON ${url}: ${response.statusText}`);
return null;
}
return await response.json();
} catch (error: any) {
if (error.name === 'AbortError') {
console.warn(`Request timeout while fetching ${url}`);
return null;
}
console.error(`Error fetching JSON ${url}:`, error);
return null;
}
}
🤖 Prompt for AI Agents
In packages/main/src/plugin/documentation/documentation-service.ts around lines
41 to 60, the fetchDocumentation method performs parallel fetches without a
timeout which can hang; update fetchJsonContent to accept an optional
AbortSignal parameter and use AbortController here to enforce a timeout (create
a controller, set a setTimeout to call controller.abort() after a reasonable
delay, pass controller.signal into each fetchJ
D306
sonContent call, and clear the
timeout on success), ensure fetchJsonContent propagates AbortError so the catch
block falls back to getFallbackDocumentation, and handle clearing/cleanup of the
timer to avoid leaks.

Copy link
Contributor
@axel7083 axel7083 left a comment

Choose a reason for hiding this comment

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

Nice to see all coming together, left some comment

Comment on lines +62 to +67
async getDocumentationItems(): Promise<DocumentationInfo[]> {
if (!this.isInitialized) {
await this.fetchDocumentation();
}
return this.documentation;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

nice lazy loading 👍

throw new Error('Failed to fetch documentation JSON files');
}
this.isInitialized = true;
} catch (error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
} catch (error) {
} catch (error: unkown) {

no type error here?

this.apiSender.send('documentation-updated');
}

private async fetchJsonContent(url: string): Promise<Array<{ name: string; url: string }> | null> {
Copy link
Contributor

Choose a reason for hiding this comment

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

we do not use null, eslint is happy about that??

Copy link
Contributor

Choose a reason for hiding this comment

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

let's avoid returning null, either return something valid or throw an error, let's avoid the caller to handler yet another error scenario

throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

return await response.json();
Copy link
Contributor

Choose a reason for hiding this comment

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

Basic validation? At least it ensure the payload is an array

Comment on lines 96 to 97
docsJson: DocumentationJsonInfo[],
tutorialsJson: DocumentationJsonInfo[],
Copy link
Contributor

Choose a reason for hiding this comment

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

why using the wording json in interfaces name?

return [...corePages, ...this.removeDuplicates(documentation)];
}

private getCoreDocumentationPages(): DocumentationInfo[] {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please move this to a dedicated asset file somewhere, I would rather not have hardcoded documention info in the typescript files

];
}

private getFallbackDocumentation(): DocumentationInfo[] {
Copy link
Contributor

Choose a reason for hiding this comment

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

];
}

private removeDuplicates(items: DocumentationInfo[]): DocumentationInfo[] {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this needed ? I would rather have the docusaurus script ensuring that all links are unique

});
}

private generateId(title: string): string {
Copy link
Contributor

Choose a reason for hiding this comment

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

is the id gonna be visible to the user ? otherwise just hash the title

{#if searchOptionsSelectedIndex === 2}
<Button icon={faChevronRight} type='link' onclick={(): Promise<void> => window.openExternal('https://podman-desktop.io/docs')}>Browse All Documentation</Button>
{:else}
<Button icon={faChevronRight} type='link' onclick={(): void => {console.log('Variables clicked');}}>Variables</Button>
Copy link
Contributor
@jiridostal jiridostal Oct 6, 2025

Choose a reason for hiding this comment

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

Not sure what is the purpose of this "Variables" with console log?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
packages/renderer/src/lib/dialogs/CommandPalette.svelte (1)

346-367: Align type discrimination with executeAction.

Line 347 uses the same unreliable 'category' in item check. For consistency and correctness, use the same discriminator as in executeAction.

Apply this diff:

-          {#each filteredItems as item, i (i)}
-            {@const isDocItem = 'category' in item}
+          {#each filteredItems as item, i (i)}
+            {@const isDocItem = 'url' in item && typeof (item as any).url === 'string'}
             <li class="flex w-full flex-row" bind:this={scrollElements[i]} aria-label={item.id}>
♻️ Duplicate comments (3)
packages/renderer/src/lib/dialogs/CommandPalette.svelte (3)

74-83: Fix null-safe filtering; optional chaining is incomplete.

The optional chaining stops at toLowerCase(), so .includes() is called on undefined when fields are missing, causing a runtime exception.

Apply this diff:

 let filteredDocumentationInfoItems: DocumentationInfo[] = $derived(
   documentationItems.filter(item =>
     inputValue
-      ? item.name?.toLowerCase().includes(inputValue.toLowerCase()) ||
-        item.description?.toLowerCase().includes(inputValue.toLowerCase()) ||
-        item.category?.toLowerCase().includes(inputValue.toLowerCase())
+      ? item.name?.toLowerCase()?.includes(inputValue.toLowerCase()) ||
+        item.description?.toLowerCase()?.includes(inputValue.toLowerCase()) ||
+        item.category?.toLowerCase()?.includes(inputValue.toLowerCase())
       : true,
   ),
 );

227-254: Fix type discrimination and guard IPC calls.

Using 'category' in item is unreliable because CommandInfo may also have a category property. Additionally, the IPC bridge functions lack existence checks.

Apply this diff:

 async function executeAction(index: number): Promise<void> {
   const item = filteredItems[index];
   if (!item) return;
 
-  // Check if it's a documentation item by checking for 'category' property
-  const isDocItem = 'category' in item;
+  // Discriminate by presence of url field (docs) vs id field (commands)
+  const isDocItem = 'url' in item && typeof (item as any).url === 'string';
 
   if (isDocItem) {
     // Documentation item
     const docItem = item as DocumentationInfo;
-    if (docItem.url) {
-      try {
-        await window.openExternal(docItem.url);
-      } catch (error) {
-        console.error('Error opening documentation URL', error);
-      }
-    }
+    if (!docItem.url) return;
+    try {
+      const u = new URL(docItem.url);
+      if (u.protocol === 'http:' || u.protocol === 'https:') {
+        if (typeof window.openExternal === 'function') {
+          await window.openExternal(u.toString());
+        } else {
+          console.warn('openExternal unavailable');
+        }
+      } else {
+        console.warn('Blocked non-http(s) URL:', docItem.url);
+      }
+    } catch (error) {
+      console.error('Invalid documentation URL', error);
+    }
   } else {
     // Command item
     const commandItem = item as CommandInfo;
-    if (commandItem.id) {
-      try {
-        await window.executeCommand(commandItem.id);
-      } catch (error) {
-        console.error('error executing command', error);
-      }
-    }
+    if (!commandItem.id) return;
+    try {
+      if (typeof window.executeCommand === 'function') {
+        await window.executeCommand(commandItem.id);
+      } else {
+        console.warn('executeCommand unavailable');
+      }
+    } catch (error) {
+      console.error('error executing command', error);
+    }
   }
 }

378-378: Remove or implement the Variables button.

The onclick handler only logs to the console, indicating incomplete implementation. Either implement the intended functionality or remove this placeholder.

Consider removing the button until the "Go to" mode functionality is implemented:

-            {:else}
-              <Button icon={faChevronRight} type='link' onclick={(): void => {console.log('Variables clicked');}}>Variables</Button>
             {/if}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf19386 and 7024e88.

📒 Files selected for processing (5)
  • packages/api/src/documentation-info.ts (1 hunks)
  • packages/main/src/assets/fallback-documentation.json (1 hunks)
  • packages/main/src/plugin/documentation/documentation-service.spec.ts (1 hunks)
  • packages/main/src/plugin/documentation/documentation-service.ts (1 hunks)
  • packages/renderer/src/lib/dialogs/CommandPalette.svelte (10 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/main/src/assets/fallback-documentation.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/api/src/documentation-info.ts
  • packages/main/src/plugin/documentation/documentation-service.ts
  • packages/main/src/plugin/documentation/documentation-service.spec.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: smoke e2e tests (development)
  • GitHub Check: unit tests / windows-2025
  • GitHub Check: unit tests / ubuntu-24.04
  • GitHub Check: smoke e2e tests (production)
  • GitHub Check: macOS
  • GitHub Check: unit tests / macos-15
  • GitHub Check: linter, formatters
  • GitHub Check: typecheck
  • GitHub Check: build website
  • GitHub Check: Linux
  • GitHub Check: Windows
  • GitHub Check: Sourcery review

Comment on lines 102 to 106
onMount(async () => {
const platform = await window.getOsPlatform();
isMac = platform === 'darwin';
documentationItems = await window.getDocumentationItems();
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard both preload APIs to prevent mount failures.

Both getOsPlatform() and getDocumentationItems() can throw if the preload bridge is unavailable. The previous review flagged getOsPlatform() but the same issue affects getDocumentationItems().

Apply this diff:

 onMount(async () => {
-  const platform = await window.getOsPlatform();
-  isMac = platform === 'darwin';
-  documentationItems = await window.getDocumentationItems();
+  try {
+    const platform = await window.getOsPlatform();
+    isMac = platform === 'darwin';
+  } catch (e) {
+    console.warn('getOsPlatform unavailable; defaulting to non-mac', e);
+    isMac = false;
+  }
+  
+  try {
+    documentationItems = await window.getDocumentationItems();
+  } catch (e) {
+    console.warn('getDocumentationItems unavailable; skipping docs', e);
+    documentationItems = [];
+  }
 });
📝 Committable suggestion

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

Suggested change
onMount(async () => {
const platform = await window.getOsPlatform();
isMac = platform === 'darwin';
documentationItems = await window.getDocumentationItems();
});
onMount(async () => {
try {
const platform = await window.getOsPlatform();
isMac = platform === 'darwin';
} catch (e) {
console.warn('getOsPlatform unavailable; defaulting to non-mac', e);
isMac = false;
}
try {
documentationItems = await window.getDocumentationItems();
} catch (e) {
console.warn('getDocumentationItems unavailable; skipping docs', e);
documentationItems = [];
}
});
🤖 Prompt for AI Agents
In packages/renderer/src/lib/dialogs/CommandPalette.svelte around lines 102 to
106, both preload bridge calls (window.getOsPlatform() and
window.getDocumentationItems()) must be guarded to avoid mount failures when the
preload API is unavailable; wrap each call in existence checks and try/catch (or
a single try/catch around both) and fall back to safe defaults (e.g., isMac =
false and documentationItems = []), ensuring any caught error is swallowed or
logged but does not abort component mount.

Copy link
Contributor
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (7)
packages/renderer/src/lib/dialogs/CommandPalette.svelte (4)

227-254: Use explicit type guards for more reliable item discrimination.

The current discriminator ('category' in item) works but is fragile—if CommandInfo ever gains a category property, this breaks. Consider using a more explicit check.

Use a type guard that checks for documentation-specific properties:

 async function executeAction(index: number): Promise<void> {
   const item = filteredItems[index];
   if (!item) return;

-  // Check if it's a documentation item by checking for 'category' property
-  const isDocItem = 'category' 
475E
in item;
+  // Check if it's a documentation item by checking for url property (docs have urls, commands have ids)
+  const isDocItem = 'url' in item && typeof (item as any).url === 'string';

   if (isDocItem) {
     // Documentation item
     const docItem = item as DocumentationInfo;
     if (docItem.url) {
       try {
         await window.openExternal(docItem.url);
       } catch (error) {
         console.error('Error opening documentation URL', error);
       }
     }
   } else {
     // Command item
     const commandItem = item as CommandInfo;
     if (commandItem.id) {
       try {
         await window.executeCommand(commandItem.id);
       } catch (error) {
         console.error('error executing command', error);
       }
     }
   }
 }

102-106: Guard preload APIs to prevent mount failures.

Both window.getOsPlatform() and window.getDocumentationItems() can throw if the preload bridge is unavailable, causing the component to fail mounting.

Wrap each call in a try-catch with safe defaults:

 onMount(async () => {
-  const platform = await window.getOsPlatform();
-  isMac = platform === 'darwin';
-  documentationItems = await window.getDocumentationItems();
+  try {
+    const platform = await window.getOsPlatform();
+    isMac = platform === 'darwin';
+  } catch (e) {
+    console.warn('getOsPlatform unavailable; defaulting to non-mac', e);
+    isMac = false;
+  }
+  
+  try {
+    documentationItems = await window.getDocumentationItems();
+  } catch (e) {
+    console.warn('getDocumentationItems unavailable; skipping docs', e);
+    documentationItems = [];
+  }
 });

74-83: Fix null-safe filtering to prevent runtime errors.

The optional chaining stops at toLowerCase(), but calling .includes() on undefined will throw when fields are missing.

Chain through includes or use nullish coalescing:

 let filteredDocumentationInfoItems: DocumentationInfo[] = $derived(
   documentationItems.filter(item =>
     inputValue
-      ? item.name?.toLowerCase().includes(inputValue.toLowerCase()) ||
-        item.description?.toLowerCase().includes(inputValue.toLowerCase()) ||
-        item.category?.toLowerCase().includes(inputValue.toLowerCase())
+      ? (item.name ?? '').toLowerCase().includes(inputValue.toLowerCase()) ||
+        (item.description ?? '').toLowerCase().includes(inputValue.toLowerCase()) ||
+        (item.category ?? '').toLowerCase().includes(inputValue.toLowerCase())
       : true,
   ),
 );

237-243: Validate URL scheme before opening external links.

Opening external URLs without validation could pose security risks if malicious URLs are injected into documentation items.

Add URL validation to restrict to safe schemes:

   if (isDocItem) {
     // Documentation item
     const docItem = item as DocumentationInfo;
     if (docItem.url) {
       try {
-        await window.openExternal(docItem.url);
+        const url = new URL(docItem.url);
+        if (url.protocol === 'http:' || url.protocol === 'https:') {
+          await window.openExternal(url.toString());
+        } else {
+          console.warn('Blocked non-http(s) documentation URL:', docItem.url);
+        }
       } catch (error) {
         console.error('Error opening documentation URL', error);
       }
     }
   }
packages/main/src/plugin/documentation/documentation-service.ts (2)

65-70: Add concurrent call deduplication and return defensive copy.

Multiple simultaneous calls to getDocumentationItems() will each trigger fetchDocumentation(), wasting network resources. Additionally, returning this.documentation directly exposes the internal array to mutation.

Implement call deduplication and defensive copying:

 export class DocumentationService extends Disposable {
   private documentation: DocumentationInfo[] = [];
   private isInitialized = false;
+  private initPromise?: Promise<void>;

   constructor(
     @inject(ApiSenderType)
     private apiSender: ApiSenderType,
   ) {
     super(() => {
       this.documentation = [];
       this.isInitialized = false;
+      this.initPromise = undefined;
     });
   }

   async getDocumentationItems(): Promise<DocumentationInfo[]> {
     if (!this.isInitialized) {
-      await this.fetchDocumentation();
+      this.initPromise ??= this.fetchDocumentation().finally(() => {
+        this.initPromise = undefined;
+      });
+      await this.initPromise;
     }
-    return this.documentation;
+    return [...this.documentation];
   }

   async refreshDocumentation(): Promise<void> {
     this.isInitialized = false; // Force re-fetch
+    this.initPromise = undefined;
     await this.fetchDocumentation();
     this.apiSender.send('documentation-updated');
   }

78-100: Add timeout to prevent hanging fetch requests.

The code handles AbortError (lines 93-94) but never sets a timeout. Network requests can hang indefinitely if the remote server is unresponsive.

Implement timeout using AbortController:

 private async fetchJsonContent(url: string): Promise<DocumentationBaseInfo[]> {
   try {
-    const response = await fetch(url);
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 8000);
+    
+    const response = await fetch(url, { 
+      signal: controller.signal,
+      headers: { 'Accept': 'application/json' }
+    });
+    clearTimeout(timeoutId);

     if (!response.ok) {
       throw new Error(`HTTP ${response.status}: ${response.statusText}`);
     }

     const json = await response.json();
     if (!Array.isArray(json)) {
       throw new Error(`Invalid JSON format for ${url}`);
     }
     return json;
   } catch (error: unknown) {
     if (error instanceof Error) {
       if (error.name === 'AbortError') {
         throw new Error(`Request timeout while fetching ${url}`);
       }
       throw new Error(`Failed to fetch ${url}: ${error.message}`);
     }
     throw new Error(`Failed to fetch ${url}: Unknown error`);
   }
 }
packages/main/src/plugin/documentation/documentation-service.spec.ts (1)

64-73: Critical test mock mismatch prevents happy-path coverage.

The service implementation (documentation-service.ts lines 78-90) calls response.json(), but these mocks provide only a text() method. When the tests run, response.json is undefined, causing the service to throw and fall back to offline data. This means the fetch/parse success path is never exercised.

Update all fetch mocks to return json() instead of text():

 const fetchSpy = vi
   .spyOn(global, 'fetch')
   .mockResolvedValueOnce({
     ok: true,
-    text: () => Promise.resolve(mockDocsHtml),
+    json: () => Promise.resolve([
+      { name: 'Introduction', url: '/docs/intro' },
+      { name: 'Containers Guide', url: '/docs/containers' },
+      { name: 'Kubernetes Guide', url: '/docs/kubernetes' }
+    ]),
   } as Response)
   .mockResolvedValueOnce({
     ok: true,
-    text: () => Promise.resolve(mockTutorialHtml),
+    json: () => Promise.resolve([
+      { name: 'Getting Started Tutorial', url: '/tutorial/getting-started' },
+      { name: 'Kubernetes Cluster Tutorial', url: '/tutorial/kubernetes-cluster' }
+    ]),
   } as Response);

Apply the same fix to all similar mocks throughout the test file (lines 127-136, 150-159, 175-184, 237-246, 290-299, 318-327, 349-358, 380-389).

🧹 Nitpick comments (3)
packages/main/src/plugin/documentation/documentation-service.ts (3)

133-133: Consider simpler ID generation if IDs are internal-only.

Using SHA-256 for ID generation (line 133) is cryptographically secure but may be overkill if IDs are only used internally. A simpler approach like slugifying the name or using a counter would be faster and more readable.

If IDs aren't exposed to users or used for security-sensitive C462 purposes, simplify:

-id: createHash('sha256').update(item.name).digest('hex'),
+id: `${config.category.toLowerCase()}-${item.name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`,

Or use a counter for guaranteed uniqueness:

let idCounter = 0;
// ...
id: `${config.category.toLowerCase()}-${idCounter++}`,

60-60: Consider runtime validation of fallback data structure.

The fallback documentation is cast to DocumentationInfo[] (lines 60, 111, 149) without runtime validation. While the JSON structure currently matches the type, runtime validation would catch future mismatches.

Add a simple validation helper:

private validateDocumentationItem(item: unknown): item is DocumentationInfo {
  return (
    typeof item === 'object' &&
    item !== null &&
    'id' in item && typeof (item as any).id === 'string' &&
    'name' in item && typeof (item as any).name === 'string' &&
    'description' in item && typeof (item as any).description === 'string' &&
    'url' in item && typeof (item as any).url === 'string' &&
    'category' in item && typeof (item as any).category === 'string'
  );
}

// Then use it:
const validated = (fallbackDocumentation as unknown[]).filter(this.validateDocumentationItem);
if (validated.length !== fallbackDocumentation.length) {
  console.warn('Some fallback documentation items failed validation');
}
this.documentation = validated;

109-112: Input validation is adequate but could be more thorough.

The current validation checks for missing parameters and array types but doesn't validate field types or URL formats within items.

For production robustness, consider adding deeper validation:

private validateDocumentationBaseInfo(item: any): item is DocumentationBaseInfo {
  return (
    typeof item?.name === 'string' &&
    item.name.length > 0 &&
    typeof item?.url === 'string' &&
    (item.url.startsWith('http://') || item.url.startsWith('https://') || item.url.startsWith('/'))
  );
}

// Then in parseDocumentationFromJson:
for (const item of config.data) {
  if (this.validateDocumentationBaseInfo(item)) {
    // ... create DocumentationInfo
  }
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7024e88 and f25ab9a.

📒 Files selected for processing (5)
  • packages/api/src/documentation-info.ts (1 hunks)
  • packages/main/src/assets/fallback-documentation.json (1 hunks)
  • packages/main/src/plugin/documentation/documentation-service.spec.ts (1 hunks)
  • packages/main/src/plugin/documentation/documentation-service.ts (1 hunks)
  • packages/renderer/src/lib/dialogs/CommandPalette.svelte (10 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/main/src/plugin/documentation/documentation-service.ts (2)
packages/extension-api/src/extension-api.d.ts (1)
  • Disposable (90-118)
packages/api/src/documentation-info.ts (2)
  • DocumentationInfo (24-28)
  • DocumentationBaseInfo (19-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: Sourcery review
  • GitHub Check: unit tests / ubuntu-24.04
  • GitHub Check: unit tests / windows-2025
  • GitHub Check: unit tests / macos-15
  • GitHub Check: smoke e2e tests (development)
  • GitHub Check: smoke e2e tests (production)
  • GitHub Check: typecheck
  • GitHub Check: linter, formatters
  • GitHub Check: Linux
  • GitHub Check: build website
  • GitHub Check: macOS
  • GitHub Check: Windows
🔇 Additional comments (1)
packages/main/src/assets/fallback-documentation.json (1)

1-44: LGTM!

The fallback documentation structure correctly implements the DocumentationInfo interface with all required fields (id, name, description, url, category). The entries provide a sensible minimal set covering core documentation areas, and all URLs are absolute and properly formatted.

@gastoner
Copy link
Contributor Author
gastoner commented Oct 6, 2025

@jiridostal @axel7083 PR updated

<div class='flex grow items-center flex-col gap-2 py-4'>
<Icon icon={NotFoundIcon} />
<div class='text-lg font-bold'>No results matching '{inputValue}' found</div>
<div class='text-md'>Not what you expected? Double-check your spelling or try searching for:</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

I know it's a nitpick... but the message without any button or link after : looks like a bug 🤷 :-D

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated, should now be visible only when docs is selected

Signed-off-by: Evzen Gasta <evzen.ml@seznam.cz>
@gastoner gastoner force-pushed the global_navbar_docs branch from f25ab9a to 5d1d45b Compare October 6, 2025 13:01
@gastoner gastoner requested a review from jiridostal October 6, 2025 13:02
Copy link
Contributor
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
packages/renderer/src/lib/dialogs/CommandPalette.svelte (1)

346-367: Align item rendering discriminator with executeAction.

The item rendering uses the same unreliable discriminator ('category' in item) as executeAction. Once you fix the discriminator in executeAction, update this rendering logic to use the same reliable check for consistency.

Apply this diff after fixing executeAction:

-          {@const isDocItem = 'category' in item}
+          {@const isDocItem = 'url' in item && typeof (item as any).url === 'string'}
♻️ Duplicate comments (6)
packages/main/src/plugin/documentation/documentation-service.ts (3)

72-76: Verify ApiSenderType.send signature and add payload if required.

The apiSender.send call passes only the channel name. Previous reviews indicated that ApiSenderType.send expects a payload argument. If the signature requires two parameters, add an appropriate payload (e.g., undefined or a status object).

If the signature requires a payload, apply this diff:

-    this.apiSender.send('documentation-updated');
+    this.apiSender.send('documentation-updated', undefined);

Run this script to verify the ApiSenderType.send signature:

#!/bin/bash
# Check ApiSenderType.send signature
ast-grep --pattern $'class $_ {
  $$$
  send($_, $_) {
    $$$
  }
  $$$
}'

44-63: Add request timeout to prevent hanging network calls.

The fetchDocumentation method lacks a timeout on its fetch operations, which can cause the service to hang indefinitely if the network is slow or unresponsive. This was flagged in previous reviews but remains unaddressed.

Apply this diff to add timeout handling:

 async fetchDocumentation(): Promise<void> {
   try {
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 8000);
+
     const [docsJson, tutorialsJson] = await Promise.all([
-      this.fetchJsonContent('https://podman-desktop.io/docs.json'),
-      this.fetchJsonContent('https://podman-desktop.io/tutorials.json'),
+      this.fetchJsonContent('https://podman-desktop.io/docs.json', controller.signal),
+      this.fetchJsonContent('https://podman-desktop.io/tutorials.json', controller.signal),
     ]);
+    clearTimeout(timeoutId);

     if (docsJson && tutorialsJson) {
       this.documentation = this.parseDocumentationFromJson(docsJson, tutorialsJson);
     } else {
       throw new Error('Failed to fetch documentation JSON files');
     }
     this.isInitialized = true;
   } catch (error: unknown) {
     console.error('Failed to fetch documentation at startup:', error);
     // Fallback to predefined documentation if fetching fails
     this.documentation = fallbackDocumentation as DocumentationInfo[];
     this.isInitialized = true;
   }
 }

Update fetchJsonContent signature to accept the signal:

-private async fetchJsonContent(url: string): Promise<DocumentationBaseInfo[]> {
+private async fetchJsonContent(url: string, signal?: AbortSignal): Promise<DocumentationBaseInfo[]> {
   try {
-    const response = await fetch(url);
+    const response = await fetch(url, { signal });

65-70: Cache the initial load and return a defensive copy.

The method re-fetches on every call when not initialized and returns the internal array by reference, allowing external mutation. Additionally, concurrent calls can trigger multiple fetches. This was flagged in previous reviews.

Apply this diff to add caching and defensive copying:

 export class DocumentationService extends Disposable {
   private documentation: DocumentationInfo[] = [];
   private isInitialized = false;
+  private loadPromise?: Promise<void>;

   constructor(
     @inject(ApiSenderType)
     private apiSender: ApiSenderType,
   ) {
     super(() => {
       this.documentation = [];
       this.isInitialized = false;
+      this.loadPromise = undefined;
     });
   }

-  async getDocumentationItems(): Promise<DocumentationInfo[]> {
-    if (!this.isInitialized) {
-      await this.fetchDocumentation();
-    }
-    return this.documentation;
-  }
+  async getDocumentationItems(): Promise<DocumentationInfo[]> {
+    if (!this.isInitialized) {
+      this.loadPromise ??= this.fetchDocumentation().finally(() => {
+        this.loadPromise = undefined;
+      });
+      await this.loadPromise;
+    }
+    return [...this.documentation];
+  }

   async refreshDocumentation(): Promise<void> {
-    this.isInitialized = false;
+    this.loadPromise = undefined;
+    this.isInitialized = false;
     await this.fetchDocumentation();
     this.apiSender.send('documentation-updated');
   }
packages/renderer/src/lib/dialogs/CommandPalette.svelte (3)

102-106: Add error handling to prevent mount failures.

Both window.getOsPlatform() and window.getDocumentationItems() can throw if the preload bridge is unavailable, causing the component mount to fail. This issue was flagged in previous reviews but remains unaddressed.

Apply this diff to guard both calls:

 onMount(async () => {
-  const platform = await window.getOsPlatform();
-  isMac = platform === 'darwin';
-  documentationItems = await window.getDocumentationItems();
+  try {
+    const platform = await window.getOsPlatform();
+    isMac = platform === 'darwin';
+  } catch (e) {
+    console.warn('getOsPlatform unavailable; defaulting to non-mac', e);
+    isMac = false;
+  }
+  
+  try {
+    documentationItems = await window.getDocumentationItems();
+  } catch (e) {
+    console.warn('getDocumentationItems unavailable; using empty array', e);
+    documentationItems = [];
+  }
 });

74-83: Fix null-safe filtering to prevent runtime errors.

The optional chaining stops at toLowerCase(), but .includes() is still called on potentially undefined values, which will throw. This was flagged in previous reviews.

Apply this diff:

 let filteredDocumentationInfoItems: DocumentationInfo[] = $derived(
   documentationItems.filter(item =>
     inputValue
-      ? item.name?.toLowerCase().includes(inputValue.toLowerCase()) ||
-        item.description?.toLowerCase().includes(inputValue.toLowerCase()) ||
-        item.category?.toLowerCase().includes(inputValue.toLowerCase())
+      ? item.name?.toLowerCase()?.includes(inputValue.toLowerCase()) ||
+        item.description?.toLowerCase()?.includes(inputValue.toLowerCase()) ||
+        item.category?.toLowerCase()?.includes(inputValue.toLowerCase())
       : true,
   ),
 );

227-254: Use a reliable discriminator and add safety guards.

The current discriminator ('category' in item) is unreliable because CommandInfo might also have a category property, leading to misclassification. Additionally, the preload bridge methods are called without existence checks, and URL schemes are not validated.

Apply this diff:

 async function executeAction(index: number): Promise<void> {
   const item = filteredItems[index];
   if (!item) return;

-  // Check if it's a documentation item by checking for 'category' property
-  const isDocItem = 'category' in item;
+  // Discriminate by presence of a URL; commands have id but not url
+  const isDocItem = 'url' in item && typeof (item as any).url === 'string';

   if (isDocItem) {
     // Documentation item
     const docItem = item as DocumentationInfo;
-    if (docItem.url) {
-      try {
-        await window.openExternal(docItem.url);
-      } catch (error) {
-        console.error('Error opening documentation URL', error);
-      }
-    }
+    if (!docItem.url) return;
+    try {
+      const u = new URL(docItem.url);
+      if (u.protocol === 'http:' || u.protocol === 'https:') {
+        if (typeof window.openExternal === 'function') {
+          await window.openExternal(u.toString());
+        } else {
+          console.warn('openExternal unavailable; skipping');
+        }
+      } else {
+        console.warn('Blocked non-http(s) URL:', docItem.url);
+      }
+    } catch (error) {
+      console.error('Invalid documentation URL', error);
+    }
   } else {
     // Command item
     const commandItem = item as CommandInfo;
-    if (commandItem.id) {
-      try {
-        await window.executeCommand(commandItem.id);
-      } catch (error) {
-        console.error('error executing command', error);
-      }
-    }
+    if (!commandItem.id) return;
+    try {
+      if (typeof window.executeCommand === 'function') {
+        await window.executeCommand(commandItem.id);
+      } else {
+        console.warn('executeCommand unavailable; skipping');
+      }
+    } catch (error) {
+      console.error('error executing command', error);
+    }
   }
 }
🧹 Nitpick comments (1)
packages/main/src/plugin/documentation/documentation-service.ts (1)

128-144: Consider simpler ID generation.

Using SHA-256 to hash item names for IDs is functional but potentially excessive. Since IDs are not user-visible (based on past review context), a simpler approach like a counter or a lightweight hash would suffice and improve readability.

Consider this simpler approach:

+  private idCounter = 0;
+
   for (const config of parseConfigs) {
     try {
       for (const item of config.data) {
         if (item.name && item.url) {
           documentation.push({
-            id: createHash('sha256').update(item.name).digest('hex'),
+            id: `${config.category.toLowerCase()}-${++this.idCounter}`,
             name: item.name,
             description: `${config.category}: ${item.name}`,
             url: item.url,
             category: config.category,
           });
         }
       }

Or use a lightweight hash like djb2:

private simpleHash(str: string): string {
  let hash = 5381;
  for (let i = 0; i < str.length; i++) {
    hash = ((hash << 5) + hash) + str.charCodeAt(i);
  }
  return (hash >>> 0).toString(36);
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f25ab9a and 5d1d45b.

📒 Files selected for processing (5)
  • packages/api/src/documentation-info.ts (1 hunks)
  • packages/main/src/assets/fallback-documentation.json (1 hunks)
  • packages/main/src/plugin/documentation/documentation-service.spec.ts (1 hunks)
  • packages/main/src/plugin/documentation/documentation-service.ts (1 hunks)
  • packages/renderer/src/lib/dialogs/CommandPalette.svelte (10 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/main/src/plugin/documentation/documentation-service.spec.ts
  • packages/api/src/documentation-info.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/main/src/plugin/documentation/documentation-service.ts (2)
packages/extension-api/src/extension-api.d.ts (1)
  • Disposable (90-118)
packages/api/src/documentation-info.ts (2)
  • DocumentationInfo (24-28)
  • DocumentationBaseInfo (19-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: smoke e2e tests (development)
  • GitHub Check: smoke e2e tests (production)
  • GitHub Check: unit tests / ubuntu-24.04
  • GitHub Check: typecheck
  • GitHub Check: unit tests / macos-15
  • GitHub Check: linter, formatters
  • GitHub Check: unit tests / windows-2025
  • GitHub Check: Linux
  • GitHub Check: Windows
  • GitHub Check: build website
  • GitHub Check: macOS
  • GitHub Check: Sourcery review
🔇 Additional comments (2)
packages/main/src/assets/fallback-documentation.json (1)

1-44: LGTM! Fallback data structure is correct.

The JSON structure correctly matches the DocumentationInfo interface with all required fields (id, name, description, url, category). The entries provide a reasonable offline fallback covering key documentation areas.

Note: These hardcoded URLs may require periodic updates to stay aligned with the website structure.

packages/main/src/plugin/documentation/documentation-service.ts (1)

29-154: Overall architecture is sound.

The service properly extends Disposable, handles initialization state, implements fallback gracefully, and provides a clean API surface. The separation of concerns between fetching, parsing, and fallback is well-structured.

Copy link
Contributor
@jiridostal jiridostal left a comment

Choose a reason for hiding this comment

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

Now it acts and looks as per my expectations, LGTM.

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

@MariaLeonova MariaLeonova MariaLeonova left review comments

@sourcery-ai sourcery-ai[bot] sourcery-ai[bot] left review comments

@coderabbitai coderabbitai[bot] coderabbitai[bot] left review comments

@vancura vancura vancura approved these changes

@jiridostal jiridostal jiridostal approved these changes

@Firewall Firewall Awaiting requested review from Firewall

@benoitf benoitf Awaiting requested review from benoitf benoitf is a code owner

@jeffmaury jeffmaury Awaiting requested review from jeffmaury jeffmaury is a code owner automatically assigned from podman-desktop/reviewers

@slemeur slemeur Awaiting requested review from slemeur

@cdrage cdrage Awaiting requested review from cdrage

@axel7083 axel7083 Awaiting requested review from axel7083

Requested changes must be addressed to merge this pull request.

Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add docs items to searchbar component
7 participants
0