Skip to content

UI UX improvements#13

Open
ayagmar wants to merge 6 commits intomasterfrom
extmgr/scoped-cleanups-release
Open

UI UX improvements#13
ayagmar wants to merge 6 commits intomasterfrom
extmgr/scoped-cleanups-release

Conversation

@ayagmar
Copy link
Copy Markdown
Owner

@ayagmar ayagmar commented Apr 5, 2026

Summary by CodeRabbit

  • New Features

    • Keyboard navigation expanded (PageUp/Down, Home/End) and new search shortcuts (/ or Ctrl+F).
    • Inline search/filtering with fuzzy matching and visible filter cycling (Tab / Shift+Tab, 1–5).
    • Community package browsing alongside npm, with inline details and local ranking.
    • View full details (V) and contextual actions/menus for items.
  • Improvements

    • Manager UI reorganized into grouped Local vs Installed sections with compact details and package size display.
    • Release automation now supports manual dispatch and clearer CI-driven release commands.
  • Bug Fixes

    • Improved install/remove flows to surface accurate reload/install outcomes.

ayagmar added 5 commits April 5, 2026 22:53
Extract common relative-path, path-identity, and duration parsing helpers, reuse them across local and package discovery, and remove dead utility code.
Filter and cap persisted history entries while scanning session files so large session directories do not require collecting every matching change before applying the limit.
Add inline remote browse search, richer npm metadata caching, package author display, and focused regression coverage for remote flows and shared custom component rendering.
Add contextual unified-manager navigation, cleaner read-only fallbacks, shared package action outcomes, and regression coverage for manager, search, and package-config behavior.
Replace the old CI-triggered publish job with the reusable release-it flow, including workflow inputs, release config, CI helper script, dependencies, and tests.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 5, 2026

📝 Walkthrough

Walkthrough

Release automation was added and the previous CD workflow removed; shared duration and path utilities were extracted; extension discovery and package install/remove flows were refactored (outcome-based APIs); UIs replaced list components with custom browsers and improved remote/community browsing; cache metadata was extended; extensive tests were added.

Changes

Cohort / File(s) Summary
Release Automation
/.github/workflows/cd.yml (deleted), .github/workflows/release.yml, .release-it.json, scripts/release-ci.ts, package.json, CHANGELOG.md
Removed old CD workflow; added manual-dispatch Release workflow, release-it config, CI runner script that normalizes inputs and builds release-it args, and new npm scripts/devDeps for release automation.
Duration Utilities & Consumers
src/utils/duration.ts, src/utils/settings.ts, src/commands/history.ts, src/utils/auto-update.ts, test/auto-update.test.ts, test/duration.test.ts
New centralized duration parser (schedule/lookback) and callers updated to delegate (history, settings, auto-update tests adjusted).
Path Identity & Relative Selection
src/utils/path-identity.ts, src/utils/relative-path-selection.ts, src/utils/package-source.ts
Added path identity normalizer and robust relative-path selection helpers; package-source now delegates normalization to shared helper and uses selection utilities.
Extension Discovery & Package Extensions
src/extensions/discovery.ts, src/packages/extensions.ts, test/local-discovery.test.ts, test/package-extensions.test.ts
Directory discovery rewritten to return multiple ExtensionEntry items, honor manifest-declared entrypoints, handle .disabled variants, and delegate relative-path resolution to new utilities.
Package Discovery & Install API
src/packages/discovery.ts, src/packages/install.ts, src/packages/management.ts, test/install-remove.test.ts, test/package-config.test.ts
NPM result parsing extended (publisher/maintainers → author); install/remove flows refactored to return InstallOutcome; choice parsing unified via parseChoiceByLabel.
UI Refactor: Unified & Remote Browsers
src/ui/unified.ts, src/ui/remote.ts, src/ui/footer.ts, src/ui/help.ts
Replaced list-driven UIs with custom focusable browsers (UnifiedManagerBrowser, RemotePackageBrowser) supporting inline search, filters, selection persistence, community vs npm query planning, and community browse caching; footer/help updated for contextual shortcuts.
Cache & Metadata Handling
src/utils/cache.ts, test/metadata-cache.test.ts
Cache schema extended with author/keywords/date plus per-field freshness timestamps; partial updates merge with existing data and TTL checks apply per field.
Utilities Removed / Simplified
src/utils/format.ts, src/utils/retry.ts, src/utils/notify.ts, src/utils/history.ts, src/utils/mode.ts
Removed terminal-width truncation helpers, retry/backoff APIs, and notify info/warning wrappers; history collection optimized to incremental limit-bound selection; minor messaging change in mode helper.
Types & Signatures
src/types/index.ts, src/ui/remote.ts, src/ui/unified.ts, src/ui/footer.ts, src/packages/install.ts
Added author to NpmPackage; updated BrowseAction variants; several UI and install functions updated signatures to accept extra params or return outcomes.
Tests & Test Helpers
test/helpers/custom-component.ts, many test/*.test.ts (see diff)
Added custom UI component harness and broad test coverage for release script, duration parsing, discovery, cache behavior, unified/remote UIs, and install flows.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UnifiedBrowser as UnifiedManagerBrowser
    participant FilterMap as readPackageFilterMap
    participant Discovery as discoverExtensions
    participant UI as renderManager
    participant Actions as handleManagerInput

    User->>UnifiedBrowser: open manager
    UnifiedBrowser->>FilterMap: load package filter state
    FilterMap-->>UnifiedBrowser: filter map
    UnifiedBrowser->>Discovery: discover local + installed entries
    Discovery-->>UnifiedBrowser: ExtensionEntry[]
    UnifiedBrowser->>UI: render list + header summary
    User->>Actions: press "/" (search)
    Actions->>UnifiedBrowser: set search mode
    UnifiedBrowser->>UnifiedBrowser: recompute visible items (token scoring/fuzzy)
    UnifiedBrowser->>UI: render filtered results
    User->>Actions: select item
    Actions->>UnifiedBrowser: set selected item
    UnifiedBrowser->>UI: render details panel
Loading
sequenceDiagram
    participant User
    participant RemoteBrowser as RemotePackageBrowser
    participant Planner as browseRemotePackages
    participant Cache as communityBrowseCache
    participant Npm as NpmRegistry
    participant UI as renderResults

    User->>RemoteBrowser: start browse / search
    RemoteBrowser->>Planner: planBrowseQuery(query)
    alt community mode (keywords or empty)
        Planner->>Cache: lookup community key
        alt cache hit
            Cache-->>Planner: cached results
        else
            Planner->>Npm: search community keywords
            Npm-->>Planner: raw results
            Planner->>Cache: store community results
        end
        Planner->>Planner: apply local filtering & scoring
    else npm lookup
        Planner->>Npm: search npm registry
        Npm-->>Planner: results
    else path-like query
        Planner-->>User: emit warning (looks like local path)
    end
    Planner-->>RemoteBrowser: filtered results
    RemoteBrowser->>UI: render package list
    User->>RemoteBrowser: install action
    RemoteBrowser->>Npm: (install flow) call installPackageWithOutcome
    Npm-->>RemoteBrowser: InstallOutcome (installed,reloaded)
    RemoteBrowser->>UI: update view based on outcome
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 From scripts and schemas, tidy paths emerge,
New browsers hum and caches gently surge.
Releases now wait for a human tap,
Outcomes return — no more guesswork trap.
— The Release Rabbit 🎉

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'UI UX improvements' is vague and generic, using non-descriptive terms that do not convey meaningful information about the substantial changeset. Replace with a specific, descriptive title that captures the main change, such as 'Refactor unified UI with release automation and path identity utilities' or similar.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch extmgr/scoped-cleanups-release

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (1)
src/packages/extensions.ts (1)

479-485: ⚠️ Potential issue | 🟡 Minor

Callback signature mismatch with resolveRelativePathSelection.

The callback at line 481 accepts only one parameter (path), but resolveRelativePathSelection passes two: (path, allFiles). While JavaScript ignores the extra argument, this violates the function's type contract and differs from the correct usage in src/extensions/discovery.ts (line 206).

To match the expected signature and ensure the path actually exists in the collection:

♻️ Suggested fix for consistent callback signature
 async function resolveManifestExtensionEntries(
   packageRoot: string,
   entries: string[]
 ): Promise<string[]> {
   const allFiles = await collectExtensionFilesFromDir(packageRoot, packageRoot);
-  return resolveRelativePathSelection(allFiles, entries, (path) => isExtensionEntrypointPath(path));
+  return resolveRelativePathSelection(allFiles, entries, (path, files) => 
+    isExtensionEntrypointPath(path) && files.includes(path)
+  );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/packages/extensions.ts` around lines 479 - 485, The callback passed to
resolveRelativePathSelection from resolveManifestExtensionEntries currently uses
a single parameter (path) but resolveRelativePathSelection invokes the callback
with (path, allFiles); update the callback signature to accept both parameters
and use the allFiles collection to verify membership and entrypoint status.
Specifically, inside resolveManifestExtensionEntries (which calls
collectExtensionFilesFromDir and resolveRelativePathSelection), change the
predicate to (path, allFiles) => isExtensionEntrypointPath(path) &&
allFiles.includes(path) (or equivalent existence check) so the callback matches
resolveRelativePathSelection's expected (path, allFiles) signature and ensures
the path exists in the collected files.
🧹 Nitpick comments (5)
src/ui/footer.ts (1)

11-24: Consider simplifying the conditional.

Both branches compute pendingChanges identically. The ternary can be simplified:

♻️ Simplification
 export function buildFooterState(
   staged: Map<string, State>,
   byId: Map<string, UnifiedItem>,
   selectedItem?: UnifiedItem
 ): FooterState {
-  return selectedItem
-    ? {
-        selectedType: selectedItem.type,
-        pendingChanges: getPendingToggleChangeCount(staged, byId),
-      }
-    : {
-        pendingChanges: getPendingToggleChangeCount(staged, byId),
-      };
+  return {
+    selectedType: selectedItem?.type,
+    pendingChanges: getPendingToggleChangeCount(staged, byId),
+  };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/footer.ts` around lines 11 - 24, The buildFooterState function uses a
ternary where both branches compute pendingChanges the same; simplify by
creating the base FooterState with pendingChanges =
getPendingToggleChangeCount(staged, byId) and then, if selectedItem is present,
add selectedType: selectedItem.type to that object; update buildFooterState to
use the single base object and conditional property addition (referencing
buildFooterState, selectedItem, and getPendingToggleChangeCount) so the
redundant ternary is removed.
src/utils/relative-path-selection.ts (1)

11-20: Consider rejecting Windows absolute paths.

isSafeRelativePath validates against Unix absolute paths and traversal patterns, but Windows absolute paths like C:/foo (after normalization from C:\foo) would pass the checks. If this function is meant to restrict to relative paths only, Windows drive-letter paths should also be rejected.

🛡️ Proposed fix
 export function isSafeRelativePath(path: string): boolean {
   return (
     path !== "" &&
     path !== ".." &&
     !path.startsWith("/") &&
+    !/^[a-zA-Z]:/.test(path) &&
     !path.startsWith("../") &&
     !path.includes("/../") &&
     !path.endsWith("/..")
   );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/relative-path-selection.ts` around lines 11 - 20,
isSafeRelativePath currently only rejects POSIX absolute/traversal forms but
lets Windows absolute paths through; update isSafeRelativePath(path: string) to
also reject Windows drive-letter roots and UNC paths by detecting a leading
drive-letter + colon (e.g. "C:/" or "C:\") and leading backslashes for UNC (e.g.
"\\server\share"), in addition to the existing checks so any path matching a
drive-letter colon prefix or starting with backslashes is considered unsafe.
src/utils/history.ts (1)

384-385: Minor: The slice(-limit) is redundant but defensive.

Since pushGlobalHistoryEntry already enforces the limit, the final slice(-limit) will always return the full array. It's harmless as defensive code but could be simplified.

♻️ Optional simplification
   matchedEntries.sort((a, b) => a.change.timestamp - b.change.timestamp);
-  return limit && limit > 0 ? matchedEntries.slice(-limit) : matchedEntries;
+  return matchedEntries;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/history.ts` around lines 384 - 385, The final slice(-limit) in the
return of the matchedEntries pipeline is redundant because
pushGlobalHistoryEntry already enforces the global history length; remove the
defensive slicing and simply return matchedEntries after sorting. Update the
function that contains matchedEntries.sort(...)/return ... so it returns
matchedEntries directly (remove the conditional branch that calls
slice(-limit)); reference matchedEntries, pushGlobalHistoryEntry, and the sort
line to locate where to remove the unnecessary slice.
CHANGELOG.md (1)

1-2: Consider populating the changelog with this PR's changes.

The changelog file is empty. Given this PR introduces significant features (release automation, UI/UX improvements, new utilities like duration parsing and path identity), documenting these changes would improve release communication.

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

In `@CHANGELOG.md` around lines 1 - 2, The CHANGELOG.md is empty; populate it with
an "Unreleased" section summarizing this PR's changes: add brief bullet points
for release automation, UI/UX improvements, and new utilities (duration parsing,
path identity), include PR number/author(s) and an expected version or date
placeholder, and follow the project's changelog style (e.g., "Added", "Changed",
"Fixed") so maintainers can review and promote these notes into a release entry
later.
src/ui/remote.ts (1)

118-147: Keep community browse on a single cache path.

Line 903 prefers communityBrowseCache over the existing search cache, so setSearchCache() and clearSearchCache() no longer fully control the keywords:pi-package data. That split invalidation path is easy to miss and is already leaking into the test cleanup story. I'd collapse this to one cache or route both stores through a shared set/clear helper.

Also applies to: 902-916

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

In `@src/ui/remote.ts` around lines 118 - 147, The code introduces a separate
communityBrowseCache that bypasses the existing search cache APIs
(setSearchCache/clearSearchCache) causing split invalidation for the
"keywords:pi-package" entry; refactor so there's a single cache path: either
remove communityBrowseCache and have
getCommunityBrowseCache/setCommunityBrowseCache/clearCommunityBrowseCache
delegate to the existing search cache functions (use the same query key
"keywords:pi-package"/COMMUNITY_BROWSE_QUERY) or consolidate search cache into a
shared helper that both code paths call, ensuring
setSearchCache/clearSearchCache fully control the community data and no separate
communityBrowseCache state remains.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/release.yml:
- Around line 3-19: Add workflow-level concurrency and a branch guard: under the
top-level on:workflow_dispatch block (where release_type and first_release are
defined) add a top-level concurrency key (e.g. concurrency: { group:
"release-${{ github.repository }}-${{ github.ref }}", cancel-in-progress: true
}) to serialize manual runs, and add a job-level guard on the release job (e.g.
if: github.ref == 'refs/heads/main') to ensure releases only run for the
intended branch (or require/validate an explicit branch input and check
github.event.inputs.branch == 'main' in the job if you want a selectable
branch). Reference the workflow_dispatch inputs (release_type, first_release)
and the release job name when adding these keys.

In `@src/packages/discovery.ts`:
- Around line 102-110: The current find callback on pkg.maintainers (the
expression pkg.maintainers?.find(...)) uses an OR so it returns the first
maintainer with either a username or email, causing email-only maintainers to be
selected before later username entries; change the logic to first search for a
maintainer with a non-empty username (use pkg.maintainers?.find(entry =>
entry.username?.trim()) and return that username if found), and only if none is
found search for a maintainer with a non-empty email
(pkg.maintainers?.find(entry => entry.email?.trim()) and return that email);
keep the existing trimming and null checks (maintainer?.username?.trim(),
maintainer?.email?.trim()) when returning values.

In `@src/ui/unified.ts`:
- Around line 1305-1331: The handlers for selection branches
(promptLocalActionSelection, showUnifiedItemDetails,
resolvePendingChangesBeforeLeave) currently use "return false" which exits the
custom UI and forces showInteractiveOnce() to restart, losing staged/selection
state; change these returns so the manager session is preserved (e.g., replace
"return false" with logic that re-renders or continues the current interaction
loop and preserves staged, filter/search and selection state) — specifically
update the branches handling selection === "cancel", selection === "details"
(after calling showUnifiedItemDetails), and pending === "stay" (from
resolvePendingChangesBeforeLeave) to avoid exiting the function and instead
resume the current manager loop or refresh the UI so staged remains intact.
Ensure you preserve the existing variables staged, items, byId and selection
when changing control flow.
- Around line 254-255: The duplicate set is built using entry.activePath only
(in the loop over localEntries adding normalizePathIdentity(entry.activePath) to
localPaths), which misses disabled locals that later use entry.disabledPath;
update the logic in that loop to use the "live" local path (prefer
entry.disabledPath when present, otherwise entry.activePath), i.e. add
normalizePathIdentity(entry.disabledPath || entry.activePath) to localPaths so
disabled locals are correctly deduplicated.

In `@src/utils/cache.ts`:
- Around line 255-268: mergeCachedPackageData currently resets the single shared
timestamp to Date.now() even when it inherits stale fields from existing, which
breaks isCacheValid since inherited fields keep living under the new TTL; change
the merge logic so that inherited fields retain their original freshness rather
than treating the whole record as freshly fetched—either preserve
existing.timestamp when any preserved fields are copied (e.g., if next omitted a
field, keep existing.timestamp for those fields) or store per-field timestamps
(a timestamps map) and update only the timestamps for fields present in next;
update corresponding merge usages around the other noted blocks (the similar
merges at the other ranges) and ensure isCacheValid is adjusted to check the
per-field timestamp or use the preserved existing.timestamp semantics.

In `@src/utils/relative-path-selection.ts`:
- Line 1: The import of matchesGlob from node:path in
src/utils/relative-path-selection.ts is unsafe for Node.js <22.5.0; either
update package.json engines.node to ">=22.20.0" (preferred) or add a
runtime-safe fallback: detect whether path.matchesGlob exists (or try
require('node:path').matchesGlob) before using it and fall back to a bundled
implementation or a stable library like minimatch, ensuring any code that calls
matchesGlob (in this module) uses the chosen fallback function (e.g., local
matchesGlobWrapper) so the module works across older v22 releases.

In `@test/unified-ui.test.ts`:
- Line 121: The test currently hardcodes a POSIX temp path when calling
createMockHarness ({ cwd: "/tmp", hasUI: true }), which breaks on Windows;
update the test to create a portable temporary directory (e.g., using Node's
os.tmpdir() + fs.mkdtempSync or the tmpdir()/mkdtemp helpers already used
elsewhere) and pass that generated dir as the cwd to createMockHarness so the
call to createMockHarness({ cwd, hasUI: true }) uses a platform-agnostic temp
path.

---

Outside diff comments:
In `@src/packages/extensions.ts`:
- Around line 479-485: The callback passed to resolveRelativePathSelection from
resolveManifestExtensionEntries currently uses a single parameter (path) but
resolveRelativePathSelection invokes the callback with (path, allFiles); update
the callback signature to accept both parameters and use the allFiles collection
to verify membership and entrypoint status. Specifically, inside
resolveManifestExtensionEntries (which calls collectExtensionFilesFromDir and
resolveRelativePathSelection), change the predicate to (path, allFiles) =>
isExtensionEntrypointPath(path) && allFiles.includes(path) (or equivalent
existence check) so the callback matches resolveRelativePathSelection's expected
(path, allFiles) signature and ensures the path exists in the collected files.

---

Nitpick comments:
In `@CHANGELOG.md`:
- Around line 1-2: The CHANGELOG.md is empty; populate it with an "Unreleased"
section summarizing this PR's changes: add brief bullet points for release
automation, UI/UX improvements, and new utilities (duration parsing, path
identity), include PR number/author(s) and an expected version or date
placeholder, and follow the project's changelog style (e.g., "Added", "Changed",
"Fixed") so maintainers can review and promote these notes into a release entry
later.

In `@src/ui/footer.ts`:
- Around line 11-24: The buildFooterState function uses a ternary where both
branches compute pendingChanges the same; simplify by creating the base
FooterState with pendingChanges = getPendingToggleChangeCount(staged, byId) and
then, if selectedItem is present, add selectedType: selectedItem.type to that
object; update buildFooterState to use the single base object and conditional
property addition (referencing buildFooterState, selectedItem, and
getPendingToggleChangeCount) so the redundant ternary is removed.

In `@src/ui/remote.ts`:
- Around line 118-147: The code introduces a separate communityBrowseCache that
bypasses the existing search cache APIs (setSearchCache/clearSearchCache)
causing split invalidation for the "keywords:pi-package" entry; refactor so
there's a single cache path: either remove communityBrowseCache and have
getCommunityBrowseCache/setCommunityBrowseCache/clearCommunityBrowseCache
delegate to the existing search cache functions (use the same query key
"keywords:pi-package"/COMMUNITY_BROWSE_QUERY) or consolidate search cache into a
shared helper that both code paths call, ensuring
setSearchCache/clearSearchCache fully control the community data and no separate
communityBrowseCache state remains.

In `@src/utils/history.ts`:
- Around line 384-385: The final slice(-limit) in the return of the
matchedEntries pipeline is redundant because pushGlobalHistoryEntry already
enforces the global history length; remove the defensive slicing and simply
return matchedEntries after sorting. Update the function that contains
matchedEntries.sort(...)/return ... so it returns matchedEntries directly
(remove the conditional branch that calls slice(-limit)); reference
matchedEntries, pushGlobalHistoryEntry, and the sort line to locate where to
remove the unnecessary slice.

In `@src/utils/relative-path-selection.ts`:
- Around line 11-20: isSafeRelativePath currently only rejects POSIX
absolute/traversal forms but lets Windows absolute paths through; update
isSafeRelativePath(path: string) to also reject Windows drive-letter roots and
UNC paths by detecting a leading drive-letter + colon (e.g. "C:/" or "C:\") and
leading backslashes for UNC (e.g. "\\server\share"), in addition to the existing
checks so any path matching a drive-letter colon prefix or starting with
backslashes is considered unsafe.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3ab81f48-f9c0-48cb-ad98-e17d33f73978

📥 Commits

Reviewing files that changed from the base of the PR and between e8baacd and b378081.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (45)
  • .github/workflows/cd.yml
  • .github/workflows/release.yml
  • .release-it.json
  • CHANGELOG.md
  • README.md
  • package.json
  • scripts/release-ci.ts
  • src/commands/auto-update.ts
  • src/commands/history.ts
  • src/constants.ts
  • src/extensions/discovery.ts
  • src/packages/discovery.ts
  • src/packages/extensions.ts
  • src/packages/install.ts
  • src/packages/management.ts
  • src/types/index.ts
  • src/ui/footer.ts
  • src/ui/help.ts
  • src/ui/remote.ts
  • src/ui/unified.ts
  • src/utils/auto-update.ts
  • src/utils/cache.ts
  • src/utils/duration.ts
  • src/utils/format.ts
  • src/utils/history.ts
  • src/utils/mode.ts
  • src/utils/notify.ts
  • src/utils/package-source.ts
  • src/utils/path-identity.ts
  • src/utils/relative-path-selection.ts
  • src/utils/retry.ts
  • src/utils/settings.ts
  • test/auto-update.test.ts
  • test/cache-history.test.ts
  • test/duration.test.ts
  • test/helpers/custom-component.ts
  • test/install-remove.test.ts
  • test/local-discovery.test.ts
  • test/metadata-cache.test.ts
  • test/package-config.test.ts
  • test/package-extensions.test.ts
  • test/release-ci.test.ts
  • test/remote-ui.test.ts
  • test/search-regression.test.ts
  • test/unified-ui.test.ts
💤 Files with no reviewable changes (5)
  • .github/workflows/cd.yml
  • src/utils/format.ts
  • src/utils/notify.ts
  • src/constants.ts
  • src/utils/retry.ts

Preserve unified manager state across detail and stay-in-manager flows, tighten local/package dedupe, and restore clear-on-Escape behavior.

Also add per-field cache freshness, consolidate community browse caching, harden manifest/path handling, and gate manual releases to main with workflow concurrency.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (2)
src/ui/remote.ts (1)

1054-1108: ⚠️ Potential issue | 🟡 Minor

Treat a cancelled details menu like “Back to results”.

If this select is dismissed, showPackageDetails() just returns, which also ends browseRemotePackages() and drops the current query/page. Esc/cancel here should reopen the previous results the same way the explicit back option does.

Suggested fix
   const choice = parseChoiceByLabel(
     PACKAGE_DETAILS_CHOICES,
     await ctx.ui.select(packageName, Object.values(PACKAGE_DETAILS_CHOICES))
   );
+
+  if (!choice || choice === "back") {
+    await browseRemotePackages(ctx, previousQuery, pi, previousOffset, browseSource);
+    return;
+  }
 
   switch (choice) {
@@
-    case "back":
-      await browseRemotePackages(ctx, previousQuery, pi, previousOffset, browseSource);
-      return;
     default:
       return;
   }

Also applies to: 1110-1111

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

In `@src/ui/remote.ts` around lines 1054 - 1108, The viewInfo branch treats a
cancelled details fetch as a no-op (it notifies then calls showPackageDetails
which returns and ends browseRemotePackages), so change the cancel-handling to
behave like the explicit "Back to results": when runTaskWithLoader returns falsy
(cancelled) call browseRemotePackages(ctx, previousQuery, pi, previousOffset,
browseSource) and return, instead of calling showPackageDetails; also apply the
same fix to the similar cancellation path around lines 1110-1111 so any
cancelled buildPackageInfoText invocation re-opens browseRemotePackages rather
than exiting.
src/ui/unified.ts (1)

1234-1260: ⚠️ Potential issue | 🟠 Major

“Discard changes” is not actually discarding on resume paths.

resolvePendingChangesBeforeLeave() still leaves staged untouched for the discard branch, and these flows now resume the same manager session. That means discarding before Help/Auto-update, or before local removal and then cancelling the delete confirm, brings the user right back to the same pending toggles.

Suggested fix
 async function resolvePendingChangesBeforeLeave(
   items: UnifiedItem[],
   staged: Map<string, State>,
   byId: Map<string, UnifiedItem>,
   ctx: ExtensionCommandContext,
   pi: ExtensionAPI,
   destinationLabel: string
 ): Promise<"continue" | "stay"> {
@@
   if (choice === "Discard changes") {
+    staged.clear();
     return "continue";
   }

Also applies to: 1372-1386

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

In `@src/ui/unified.ts` around lines 1234 - 1260, The resume-returning branches
(e.g., the "auto-update" case using promptAutoUpdateWizard, the "help" case
calling showHelp, and similar resume flows around local removal) are not
actually discarding pending toggles because resolvePendingChangesBeforeLeave()
leaves manager.staged untouched; update those branches to call
resolvePendingChangesBeforeLeave(ctx, pi) and, if it indicates a discard branch,
explicitly clear the staged toggles on the current manager (e.g., reset
manager.staged or call the manager's clear method) before returning "resume" so
resumed sessions do not see the old pending changes.
🧹 Nitpick comments (2)
test/relative-path-selection.test.ts (1)

8-23: Expand safety tests for POSIX/traversal edge cases.

The current suite is Windows-focused. Please also assert behavior for POSIX absolute and traversal tokens so cross-platform safety stays regression-proof (Line 8 and Line 15 test blocks).

Suggested test additions
 void test("isSafeRelativePath rejects Windows absolute and UNC paths", () => {
   assert.equal(isSafeRelativePath("C:/repo/extensions/index.ts"), false);
   assert.equal(isSafeRelativePath("C:\\repo\\extensions\\index.ts"), false);
   assert.equal(isSafeRelativePath("\\\\server\\share\\index.ts"), false);
+  assert.equal(isSafeRelativePath("/repo/extensions/index.ts"), false);
+  assert.equal(isSafeRelativePath("../extensions/index.ts"), false);
+  assert.equal(isSafeRelativePath("extensions/../index.ts"), false);
   assert.equal(isSafeRelativePath("extensions/index.ts"), true);
 });
 
 void test("resolveRelativePathSelection ignores Windows absolute tokens", () => {
   const selected = resolveRelativePathSelection(
     ["extensions/index.ts"],
-    ["C:/repo/extensions/index.ts", "\\\\server\\share\\index.ts"],
+    [
+      "C:/repo/extensions/index.ts",
+      "\\\\server\\share\\index.ts",
+      "/repo/extensions/index.ts",
+      "../extensions/index.ts",
+    ],
     () => true
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/relative-path-selection.test.ts` around lines 8 - 23, Add POSIX absolute
and traversal cases to the existing safety tests: call isSafeRelativePath with
POSIX absolute paths (e.g., "/repo/extensions/index.ts") and path traversal
tokens ("../etc/passwd", "extensions/../../secret") and assert they return false
where appropriate, and update the resolveRelativePathSelection test to include
those POSIX absolute/traversal paths in the available files array and assert the
selected result excludes them (use the same pattern as the Windows checks
referencing isSafeRelativePath and resolveRelativePathSelection).
src/utils/history.ts (1)

317-330: Consider a heap if limit can grow.

Once the buffer is full, every accepted entry rescans entries to find the current oldest item. That makes this path O(matches × limit), so larger user-supplied limits can be slower than the old collect-and-sort flow. A min-heap keeps the bounded-memory behavior without the linear rescans.

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

In `@src/utils/history.ts` around lines 317 - 330, The current logic in the block
that maintains entries (variables entries, nextEntry, oldestIndex, oldestEntry)
does a linear scan to find the oldest entry when the buffer is full, causing
O(matches × limit) behavior; replace this with a bounded min-heap (priority
queue) keyed by entry.change.timestamp to maintain the oldest element in O(log
limit) per insertion. Concretely, replace the array-only approach where you
push/scan entries with a heap data structure that supports push and popMin, keep
heap size ≤ limit, and on accepting nextEntry push it to the heap and if
heap.size > limit pop the oldest; update any callers/iterations that expect an
array to either rebuild a stable array from the heap when needed or iterate the
heap safely. Ensure the heap compares using entry.change.timestamp and preserve
existing semantics for ties and null/undefined entries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/ui/remote.ts`:
- Around line 1054-1108: The viewInfo branch treats a cancelled details fetch as
a no-op (it notifies then calls showPackageDetails which returns and ends
browseRemotePackages), so change the cancel-handling to behave like the explicit
"Back to results": when runTaskWithLoader returns falsy (cancelled) call
browseRemotePackages(ctx, previousQuery, pi, previousOffset, browseSource) and
return, instead of calling showPackageDetails; also apply the same fix to the
similar cancellation path around lines 1110-1111 so any cancelled
buildPackageInfoText invocation re-opens browseRemotePackages rather than
exiting.

In `@src/ui/unified.ts`:
- Around line 1234-1260: The resume-returning branches (e.g., the "auto-update"
case using promptAutoUpdateWizard, the "help" case calling showHelp, and similar
resume flows around local removal) are not actually discarding pending toggles
because resolvePendingChangesBeforeLeave() leaves manager.staged untouched;
update those branches to call resolvePendingChangesBeforeLeave(ctx, pi) and, if
it indicates a discard branch, explicitly clear the staged toggles on the
current manager (e.g., reset manager.staged or call the manager's clear method)
before returning "resume" so resumed sessions do not see the old pending
changes.

---

Nitpick comments:
In `@src/utils/history.ts`:
- Around line 317-330: The current logic in the block that maintains entries
(variables entries, nextEntry, oldestIndex, oldestEntry) does a linear scan to
find the oldest entry when the buffer is full, causing O(matches × limit)
behavior; replace this with a bounded min-heap (priority queue) keyed by
entry.change.timestamp to maintain the oldest element in O(log limit) per
insertion. Concretely, replace the array-only approach where you push/scan
entries with a heap data structure that supports push and popMin, keep heap size
≤ limit, and on accepting nextEntry push it to the heap and if heap.size > limit
pop the oldest; update any callers/iterations that expect an array to either
rebuild a stable array from the heap when needed or iterate the heap safely.
Ensure the heap compares using entry.change.timestamp and preserve existing
semantics for ties and null/undefined entries.

In `@test/relative-path-selection.test.ts`:
- Around line 8-23: Add POSIX absolute and traversal cases to the existing
safety tests: call isSafeRelativePath with POSIX absolute paths (e.g.,
"/repo/extensions/index.ts") and path traversal tokens ("../etc/passwd",
"extensions/../../secret") and assert they return false where appropriate, and
update the resolveRelativePathSelection test to include those POSIX
absolute/traversal paths in the available files array and assert the selected
result excludes them (use the same pattern as the Windows checks referencing
isSafeRelativePath and resolveRelativePathSelection).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e2ae37cd-137e-4374-bbf4-430eb827334a

📥 Commits

Reviewing files that changed from the base of the PR and between b378081 and a1f5a61.

📒 Files selected for processing (19)
  • .github/workflows/release.yml
  • CHANGELOG.md
  • README.md
  • package.json
  • src/packages/discovery.ts
  • src/packages/extensions.ts
  • src/ui/footer.ts
  • src/ui/remote.ts
  • src/ui/unified.ts
  • src/utils/cache.ts
  • src/utils/history.ts
  • src/utils/relative-path-selection.ts
  • test/metadata-cache.test.ts
  • test/npm-search.test.ts
  • test/package-config.test.ts
  • test/package-extensions.test.ts
  • test/relative-path-selection.test.ts
  • test/unified-items.test.ts
  • test/unified-ui.test.ts
✅ Files skipped from review due to trivial changes (5)
  • CHANGELOG.md
  • src/packages/discovery.ts
  • .github/workflows/release.yml
  • README.md
  • src/utils/relative-path-selection.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • package.json
  • test/metadata-cache.test.ts
  • test/package-config.test.ts
  • src/packages/extensions.ts

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant