Skip to content

fix: ship extmgr improvements#6

Merged
ayagmar merged 12 commits intomasterfrom
fix/extmgr-improvements
Mar 11, 2026
Merged

fix: ship extmgr improvements#6
ayagmar merged 12 commits intomasterfrom
fix/extmgr-improvements

Conversation

@ayagmar
Copy link
Copy Markdown
Owner

@ayagmar ayagmar commented Mar 11, 2026

Summary

  • Custom UI degradation / RPC fallback
    • /extensions now degrades cleanly when full custom TUI is unavailable
    • read-only local/package listings still work in limited UI mode
    • remote browsing and package config warn cleanly instead of failing
  • Unified manager improvements
    • unified local + installed package view got stronger fallback behavior
    • package update badges now match normalized package identities instead of just names
    • local extension deletion is logged properly in history
  • Package extension configuration
    • package extension config now validates settings earlier
    • multiple extension state changes are applied in a batch
    • missing/unavailable entrypoints are handled more safely
  • Install / standalone package flow
    • local standalone installs now verify tar availability
    • standalone installs reject unresolved runtime dependencies
    • temp extraction artifacts are cleaned up after success/failure
    • URL installs use a longer timeout so slow downloads do not fail too early
    • cleanup now uses Promise.allSettled() for best-effort removal
  • Package identity / discovery normalization
    • normalized identities are now used more consistently across install/update/status flows
    • git identities now treat git+https://... and git:https://... as the same repo identity
    • installed package parsing docs were clarified for raw-vs-deduped helpers
  • Auto-update / status / settings hardening
    • auto-update stores canonical normalized update identities
    • stale/legacy update markers are filtered more safely
    • redundant auto-update config writes were removed
    • auto-update config changes are logged in history
  • History / help / docs
    • README now consistently documents duration formats, including 1mo
    • history help text now derives actions from HISTORY_ACTIONS instead of duplicating literals
    • standalone install wording in README is more source-agnostic
  • Selection / UI safety
    • settings list selected index now only accepts true integers
    • added regression coverage for invalid selection index shapes
  • Test coverage
    • added/updated tests for:
      - degraded custom UI / RPC behavior
      - auto-update scheduling and normalized update markers
      - standalone install cleanup and dependency rejection
      - stalled URL download abort behavior
      - parser identity handling
      - package config safety
      - unified item update matching

Summary by CodeRabbit

  • New Features

    • Package manifests now support declared extension entrypoints alongside conventional discovery.
    • History records local extension deletions and auto-update configuration changes.
    • Auto-update accepts "1mo" as a monthly alias.
    • Non-interactive/RPC mode exposes clearer behavior and history action for auto-update config.
  • Bug Fixes & Improvements

    • Cache clearing removes persistent and runtime in-memory caches (including remote package info).
    • Standalone/local installs validate runtime dependencies and require tar with clearer errors.
    • Remote browsing requires a full interactive TUI; UI gracefully falls back to read-only when unavailable.
    • Update tracking uses normalized package identities for more accurate duplicate handling.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 11, 2026

📝 Walkthrough

Walkthrough

The PR standardizes package identity handling, adds manifest-driven extension entrypoint discovery and standalone-install validation (tar, deps, timeouts), extends cache clearing to in-memory caches, records new history actions, and refactors UIs to use custom UI wrappers with read-only fallbacks.

Changes

Cohort / File(s) Summary
Docs & Command Help
README.md, src/commands/auto-update.ts, src/commands/registry.ts
Help/docs updated: standalone local install requirements, manifest/convention entrypoints, extra auto-update alias (1mo), clarified RPC/non-interactive notes, and history updates.
Package Identity & Discovery
src/utils/package-source.ts, src/packages/discovery.ts, src/packages/management.ts, src/utils/status.ts
Introduces normalizePackageIdentity() and stripGitSourcePrefix(). Discovery/management now dedupe and match by normalized identity; status/update filtering uses identities.
Extensions & Entrypoint Discovery
src/packages/extensions.ts, src/packages/install.ts
Adds manifest reading (PackageManifest), manifest-based entrypoint discovery, convention-directory and root-index fallbacks, standalone-entrypoint checks, dependency validation for standalone installs, tar availability checks, timeout-aware fetch/extract, and cleanup utilities.
Auto-update & Timer Behavior
src/utils/auto-update.ts, src/utils/timer.ts, src/utils/settings.ts
Auto-update uses normalized identities, delayed initial checks via timer options, persists nextCheck without immediate run, sanitizes update identities, and allows identity-scoped clearUpdatesAvailable.
Cache, History & Logging
src/commands/cache.ts, src/ui/remote.ts, src/commands/history.ts, src/utils/history.ts
Cache clear now also clears in-memory search and remote package-info caches and exposes clearRemotePackageInfoCache(); history accepts extension_delete and auto_update_config actions and adds logging helpers (logExtensionDelete, logAutoUpdateConfig).
UI Capability & Custom UI Integration
src/utils/mode.ts, src/ui/unified.ts, src/ui/package-config.ts, src/ui/remote.ts, src/utils/settings-list.ts
Adds UI capability detection (getUICapability, hasCustomUI), requireCustomUI and runCustomUI wrappers; refactors unified, config, and remote UI flows to use custom UI with read-only fallbacks and safer settings-list index extraction.
Utilities & Helpers
src/utils/ui-helpers.ts, src/utils/settings-list.ts, src/utils/package-source.ts
Minor output/titleing changes for UI notifications; new getSettingsListSelectedIndex() helper; package-source utilities added.
Tests & Mocks
test/helpers/mocks.ts, test/*.test.ts (many files)
Extends mock harness for custom UI/notifications; adds/updates tests covering identity normalization, auto-update timer behavior, cache/history entries, install-from-URL/local standalone flows (timeouts, tar absence, dep validation), RPC-mode UI fallbacks, and settings-list validation.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Installer as Installer
    participant Fetch as Fetch (with Timeout)
    participant Tar as Tar Utility
    participant Manifest as Manifest Reader
    participant Deps as Dependency Validator
    participant FS as File System
    participant History as History Logger

    User->>Installer: request install (URL/local)
    Installer->>Tar: ensure tar available
    alt tar unavailable
        Tar-->>Installer: error
        Installer-->>User: abort with actionable error
    else tar available
        Installer->>Fetch: download with timeout
        alt download times out
            Fetch-->>Installer: timeout error
            Installer-->>User: abort with timeout message
        else download success
            Installer->>FS: extract to temp dir
            Installer->>Manifest: read package.json / discover entrypoints
            Manifest-->>Installer: manifest + entrypoints
            Installer->>Deps: validate runtime deps present in tar
            alt deps missing
                Deps-->>Installer: unresolved deps error
                Installer->>FS: cleanup temp artifacts
                Installer-->>User: abort with dependency error
            else deps ok
                Installer->>FS: move package into place
                Installer->>History: log install success
                Installer->>FS: cleanup temp artifacts
                Installer-->>User: install success
            end
        end
    end
Loading
sequenceDiagram
    participant User
    participant UIHandler as UI Handler
    participant Capability as UI Capability Detector
    participant CustomUI as Custom UI
    participant Fallback as Read-only Fallback

    User->>UIHandler: request interactive UI (configure/browse)
    UIHandler->>Capability: getUICapability(ctx)
    alt custom UI available
        Capability-->>CustomUI: runCustomUI(open)
        CustomUI-->>UIHandler: interactive result
        UIHandler-->>User: show interactive UI
    else custom UI unavailable
        Capability-->>Fallback: show read-only lists / warn
        Fallback-->>User: emit warning and display packages
        UIHandler-->>User: read-only view (no config)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • Feat/extension package configuration #3: Overlaps manifest-driven extension entrypoint discovery and package-config UI changes in src/packages/extensions.ts and src/ui/package-config.ts.
  • bug fixes #5: Related package identity normalization and discovery refactors touching src/utils/package-source.ts and src/packages/discovery.ts.

Poem

🐰 I hopped through manifests and caches bright,

Found entrypoints shining in the night,
Timed fetches, tar checks, tidy cleanup done,
Custom UIs danced — fallbacks kept the fun,
Identity sewn, extensions take flight! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: ship extmgr improvements' accurately captures the primary objective of the pull request—shipping extmgr (extension manager) improvements. It is concise, clear, and directly related to the substantial codebase changes across multiple utility modules, UI components, and test files that collectively implement these improvements.
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 (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/extmgr-improvements

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: 6

🧹 Nitpick comments (9)
README.md (1)

159-160: Prefer source-agnostic wording for local install constraints.

“Bundled in the tarball” is a bit too implementation-specific here since the documented local sources also include folders and single files. “Bundled with the package contents” or similar would be clearer and less confusing.

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

In `@README.md` around lines 159 - 160, Update the second bullet's wording to be
source-agnostic: replace the phrase "bundled in the tarball" with something like
"bundled with the package contents" (or "included with the package contents") so
the sentence reads that the local installer "requires `tar` on `PATH`, and
rejects packages whose runtime `dependencies` are not already bundled with the
package contents"; keep the rest of the bullet (references to
`~/.pi/agent/extensions/{package}/`, runnable standalone layouts, and requiring
`tar`) unchanged.
src/commands/history.ts (1)

191-192: Derive the --action help text from HISTORY_ACTIONS.

This list now has two sources of truth. The next action change can easily update parsing but leave the help stale.

♻️ Proposed fix
 function showHistoryHelp(ctx: ExtensionCommandContext): void {
+  const actionList = HISTORY_ACTIONS.join(" | ");
   const lines = [
     "Usage: /extensions history [options]",
     "",
     "Options:",
     "  --limit <n>      Maximum entries to show (default: 20)",
     "  --action <type>  Filter by action",
-    "                   extension_toggle | extension_delete | package_install | package_update | package_remove | cache_clear | auto_update_config",
+    `                   ${actionList}`,
     "  --success        Show only successful entries",
     "  --failed         Show only failed entries",
     "  --package <q>    Filter by package/source/extension id",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/history.ts` around lines 191 - 192, Replace the hard-coded
`--action` help text with a generated string derived from the `HISTORY_ACTIONS`
array so there isn't a second source of truth; locate the help lines for the
`--action <type>` option in the help array and build the second line by joining
`HISTORY_ACTIONS` (e.g., `HISTORY_ACTIONS.join(' | ')`) or formatting it the
same way the current literals are, ensuring the help output preserves the
existing spacing/indentation and uses the `HISTORY_ACTIONS` identifier.
src/ui/unified.ts (1)

63-69: Use the shared notifier in the no-custom-UI fallback.

hasCustomUI(ctx) is also false when ctx.hasUI is false, so this branch now bypasses the helper that already knows how to surface messages in headless/RPC mode. Routing this through notify(...) keeps the fallback visible before showInteractiveFallback() prints the read-only lists.

Possible fix
 import { parseChoiceByLabel } from "../utils/command.js";
 import { getPackageSourceKind, normalizePackageIdentity } from "../utils/package-source.js";
 import { hasCustomUI, runCustomUI } from "../utils/mode.js";
+import { notify } from "../utils/notify.js";
 import { getSettingsListSelectedIndex } from "../utils/settings-list.js";
 import { UI } from "../constants.js";
 import { configurePackageExtensions } from "./package-config.js";
@@
   if (!hasCustomUI(ctx)) {
-    ctx.ui.notify(
+    notify(
+      ctx,
       "The unified extensions manager requires the full interactive TUI. Showing read-only local and installed package lists instead.",
       "warning"
     );
     await showInteractiveFallback(ctx, pi);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/unified.ts` around lines 63 - 69, The branch that runs when
hasCustomUI(ctx) is false currently calls ctx.ui.notify directly, which bypasses
the shared headless/RPC notifier; instead route the warning through the shared
notifier helper before calling showInteractiveFallback so the message is
surfaced in headless/RPC mode (i.e., replace the direct ctx.ui.notify call with
the shared notifier invocation used elsewhere, then await
showInteractiveFallback(ctx, pi) as before); reference symbols: hasCustomUI,
ctx.ui.notify, showInteractiveFallback.
test/helpers/mocks.ts (1)

22-30: Let tests drive the ui.custom() outcome.

The new callers distinguish a real cancel/result from a degraded custom UI, but this harness always resolves ctx.ui.custom() to undefined. That means it can only exercise the fallback path, not the happy path for the new custom UI flows.

Possible fix
 export interface MockHarnessOptions {
   cwd?: string;
   hasUI?: boolean;
   hasCustomUI?: boolean;
   execImpl?: ExecImpl;
   inputResult?: string;
   selectResult?: string;
   confirmResult?: boolean;
   confirmImpl?: (title: string, message?: string) => boolean | Promise<boolean>;
+  customUIResult?: unknown;
+  customUIImpl?: (factory: unknown) => unknown | Promise<unknown>;
 }
@@
     custom:
       options.hasUI && options.hasCustomUI !== false
-        ? (_factory: unknown) => {
+        ? (factory: unknown) => {
             customCalls += 1;
-            return Promise.resolve(undefined);
+            if (options.customUIImpl) {
+              return Promise.resolve(options.customUIImpl(factory));
+            }
+            return Promise.resolve(options.customUIResult);
           }
         : undefined,

Also applies to: 150-156

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

In `@test/helpers/mocks.ts` around lines 22 - 30, The harness always resolves
ctx.ui.custom() to undefined, preventing tests from exercising the success path;
add a customizable handler (e.g., a new MockHarnessOptions field named
customImpl or customResult) similar to confirmImpl so tests can specify the
resolved value or async function, then update the harness code that supplies
ctx.ui.custom() to call and/or return that customImpl result (falling back to
undefined only when not provided). Reference MockHarnessOptions and
ctx.ui.custom() (and mirror the confirmImpl pattern) so callers can simulate
real results, cancellations, or degraded behavior.
src/utils/mode.ts (1)

53-70: Avoid overloading undefined as the degraded-UI signal.

This helper now forces every caller to invent its own cancel sentinel to avoid a misleading warning/fallback on normal user cancel. A small result union here would keep cancellation semantics inside runCustomUI() instead of pushing that contract into each call site.

One possible API shape
 export async function runCustomUI<T>(
   ctx: AnyContext,
   featureName: string,
-  open: () => Promise<T | undefined>,
+  open: () => Promise<
+    | { type: "ok"; value: T }
+    | { type: "cancel" }
+    | { type: "unavailable" }
+  >,
   fallbackMessage?: string
 ): Promise<T | undefined> {
   if (!requireCustomUI(ctx, featureName, fallbackMessage)) {
     return undefined;
   }

   const result = await open();
-  if (result !== undefined) {
-    return result;
+  if (result.type === "ok") {
+    return result.value;
+  }
+  if (result.type === "cancel") {
+    return undefined;
   }

   const suffix = fallbackMessage ? ` ${fallbackMessage}` : "";
   notify(ctx, `${featureName} requires the full interactive TUI.${suffix}`, "warning");
   return undefined;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/mode.ts` around lines 53 - 70, runCustomUI currently overloads
undefined for three meanings (no-custom-ui, user-cancel, and fallback-needed)
which forces callers to invent sentinels; change its return type to a
discriminated union (e.g., Promise<{status:"ok", value:T} | {status:"cancel"} |
{status:"needs-tui"}), update runCustomUI to map open() results into those three
statuses (return {status:"cancel"} when the user cancelled without emitting the
notify, return {status:"needs-tui"} when custom UI is unavailable and emit the
existing notify, and return {status:"ok", value} when open yields a value), and
update all callers of runCustomUI to handle the new union instead of treating
undefined specially; keep the function name runCustomUI and the notify call but
ensure notify is only invoked for the needs-tui status.
src/packages/install.ts (1)

149-155: Consider swallowing errors in cleanup to avoid masking original failures.

If rm throws (e.g., permission denied), it could mask the original error that triggered cleanup. Consider wrapping in try-catch or using { force: true } semantics more defensively.

🛡️ Defensive cleanup approach
 async function cleanupStandaloneTempArtifacts(tempDir: string, extractDir?: string): Promise<void> {
-  if (extractDir) {
-    await rm(extractDir, { recursive: true, force: true });
-  }
-
-  await rm(tempDir, { recursive: true, force: true });
+  try {
+    if (extractDir) {
+      await rm(extractDir, { recursive: true, force: true });
+    }
+    await rm(tempDir, { recursive: true, force: true });
+  } catch {
+    // Ignore cleanup errors to avoid masking the original failure
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/packages/install.ts` around lines 149 - 155, The
cleanupStandaloneTempArtifacts function currently awaits rm calls that can throw
and mask original errors; wrap each rm invocation (the extractDir rm and the
tempDir rm) in try-catch (or use Promise.allSettled) so deletion failures are
swallowed or logged as warnings instead of rethrowing. Specifically, update
cleanupStandaloneTempArtifacts to catch errors from rm(...) and call a
logger.warn/console.warn with context (including the path) or ignore them,
ensuring the function never throws during cleanup.
src/packages/discovery.ts (1)

288-290: Clarify intent of parseInstalledPackagesOutputAllScopes.

This function returns raw parsed results without deduplication, unlike parseInstalledPackagesOutput. Consider adding a doc comment to clarify when to use each variant, since callers might inadvertently use the wrong one.

📝 Add clarifying documentation
+/**
+ * Parse installed packages output without deduplication.
+ * Use this when you need to inspect all entries including duplicates
+ * (e.g., for isSourceInstalled scope-specific checks).
+ * For deduplicated results honoring project precedence, use parseInstalledPackagesOutput.
+ */
 export function parseInstalledPackagesOutputAllScopes(text: string): InstalledPackage[] {
   return parseInstalledPackagesOutputInternal(text);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/packages/discovery.ts` around lines 288 - 290, Add a short doc comment
above parseInstalledPackagesOutputAllScopes explaining that it returns raw
parsed results from parseInstalledPackagesOutputInternal without deduplication
or scope-merging, and note that parseInstalledPackagesOutput is the
deduplicating/normalized variant callers should prefer for user-facing lists;
reference the function names parseInstalledPackagesOutputAllScopes,
parseInstalledPackagesOutput, and parseInstalledPackagesOutputInternal in the
comment so maintainers know the behavioral difference and when to use each.
src/utils/auto-update.ts (1)

57-80: Redundant config save when initialDelayMs > 0.

When initialDelayMs > 0, the config is saved with the same nextCheck value that was just read. This write has no effect unless startTimer modifies state that needs persisting. Consider removing this redundant save or clarifying intent with a comment.

♻️ Remove redundant save or add clarifying comment
   if (initialDelayMs > 0 && nextCheck !== undefined) {
-    saveAutoUpdateConfig(pi, {
-      ...config,
-      nextCheck,
-    });
+    // Config already has nextCheck set; no need to re-save
   }

Alternatively, if this save is intentional (e.g., to ensure config is persisted after timer start), add a comment explaining why.

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

In `@src/utils/auto-update.ts` around lines 57 - 80, The code saves the same
nextCheck back into config redundantly after computing initialDelayMs; remove
the conditional saveAutoUpdateConfig(pi, { ...config, nextCheck }) block (or if
the save is required for persistence after startTimer, replace it with a
one-line comment clarifying that intent). Update the area around startTimer,
initialDelayMs, nextCheck, and saveAutoUpdateConfig so there is no unnecessary
write unless there is a documented side-effect requiring persistence.
src/ui/package-config.ts (1)

211-213: Potential index out-of-bounds when settingsItems is empty.

If settingsItems is empty, selectedIndex defaults to 0, and settingsItems[0]?.id would be undefined. The fallback ?? settingsItems[0]?.id is also undefined in this case. While selectedRow would then be undefined and the toggle block is skipped, consider adding an early return when there are no items.

🛡️ Guard against empty items
         handleInput(data: string) {
           if (matchesKey(data, Key.ctrl("s")) || data === "s" || data === "S") {
             done({ type: "save" });
             return;
           }

+          if (settingsItems.length === 0) {
+            return;
+          }
+
           const selectedIndex = getSettingsListSelectedIndex(settingsList) ?? 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/package-config.ts` around lines 211 - 213, The code calculates
selectedIndex and selectedId without guarding for an empty settingsItems array,
which can yield undefined and risk out-of-bounds access; update the logic in the
block using getSettingsListSelectedIndex, selectedIndex, settingsItems,
selectedId and selectedRow to first check if settingsItems.length === 0 and
return early (or skip further processing) when empty, otherwise compute
selectedIndex (e.g., clamp to valid range) and derive selectedId and selectedRow
from rowById as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 111-113: Clarify the README to remove the apparent contradiction
about duration formats: update the description for `/extensions auto-update
<duration>` and the parenthetical after "history keeps `30m`/`24h` style
durations for lookback windows" to state explicitly whether `1mo` is accepted
for history/lookbacks (i.e., either note those are only examples and `1mo` is
also valid, or state that `1mo` is not supported), and make the `--since ...
1mo` example consistent with that wording so users see a single authoritative
rule; reference the `/extensions auto-update <duration>` entry and the `--since`
example when editing so both lines align.

In `@src/packages/install.ts`:
- Line 276: The call in installFromUrl uses TIMEOUTS.fetchPackageInfo for file
downloads which is too short; update the fetchWithTimeout call in installFromUrl
to use a longer timeout constant (either TIMEOUTS.packageInstall or a new
dedicated constant) instead of TIMEOUTS.fetchPackageInfo so large or slow
downloads won't prematurely fail; change the argument passed to fetchWithTimeout
and adjust any related tests or constants as needed.

In `@src/utils/package-source.ts`:
- Around line 71-75: The git branch only strips a plain "git:" prefix, so specs
like "git+https://..." remain different and break identity matching; update the
logic in the git branch (where gitSpec is computed and splitGitRepoAndRef is
called) to strip both "git:" and "git+..." prefixes (e.g. remove a leading
"git+" if present, e.g. via a regex like /^git\+?:/), then proceed to call
splitGitRepoAndRef(gitSpec) and normalize repo with repo.replace(/\\/g,
"/").toLowerCase() as before.

In `@src/utils/settings-list.ts`:
- Around line 5-11: The helper getSettingsListSelectedIndex currently accepts
any JS number (including NaN, Infinity, and fractions); change the guard to only
accept true integers by replacing the typeof check with a Number.isInteger check
on selectable.selectedIndex (i.e., return selectable.selectedIndex only if
Number.isInteger(selectable.selectedIndex)); reference the
SelectableListLike.selectedIndex field when updating the conditional and keep
returning undefined otherwise.

In `@src/utils/settings.ts`:
- Around line 52-62: The sanitizeUpdateIdentities function currently only
filters entries by prefix using isUpdateIdentity, which allows non-canonical
forms to persist; update it to map each retained entry through the existing
normalizePackageIdentity() function after sanitizeStringArray(value) and
filtering, and return the array of normalized identities (or undefined if
empty). Keep the initial filtering with isUpdateIdentity to reject irrelevant
values, but ensure you call normalizePackageIdentity on each value before
returning so stored identities match the canonical form used by
clearUpdatesAvailable and other consumers.

In `@test/install-remove.test.ts`:
- Around line 612-649: The mocked globalThis.fetch in the "installFromUrl aborts
stalled downloads instead of hanging forever" test can hang if the abort wiring
regresses; change the mock to fail-fast by adding a fallback timeout: inside the
fetch mock (the anonymous function used to replace fetch), start a short timer
via the saved originalSetTimeout that rejects the promise with a distinct
timeout Error (e.g., name "TimeoutError") if the abort event never fires, and
clear that timer when the abort handler runs (or when the promise settles); this
keeps the test deterministic while still allowing the abort flow to reject with
an AbortError when signalled and ensures you still restore globalThis.fetch and
globalThis.setTimeout in the finally block.

---

Nitpick comments:
In `@README.md`:
- Around line 159-160: Update the second bullet's wording to be source-agnostic:
replace the phrase "bundled in the tarball" with something like "bundled with
the package contents" (or "included with the package contents") so the sentence
reads that the local installer "requires `tar` on `PATH`, and rejects packages
whose runtime `dependencies` are not already bundled with the package contents";
keep the rest of the bullet (references to `~/.pi/agent/extensions/{package}/`,
runnable standalone layouts, and requiring `tar`) unchanged.

In `@src/commands/history.ts`:
- Around line 191-192: Replace the hard-coded `--action` help text with a
generated string derived from the `HISTORY_ACTIONS` array so there isn't a
second source of truth; locate the help lines for the `--action <type>` option
in the help array and build the second line by joining `HISTORY_ACTIONS` (e.g.,
`HISTORY_ACTIONS.join(' | ')`) or formatting it the same way the current
literals are, ensuring the help output preserves the existing
spacing/indentation and uses the `HISTORY_ACTIONS` identifier.

In `@src/packages/discovery.ts`:
- Around line 288-290: Add a short doc comment above
parseInstalledPackagesOutputAllScopes explaining that it returns raw parsed
results from parseInstalledPackagesOutputInternal without deduplication or
scope-merging, and note that parseInstalledPackagesOutput is the
deduplicating/normalized variant callers should prefer for user-facing lists;
reference the function names parseInstalledPackagesOutputAllScopes,
parseInstalledPackagesOutput, and parseInstalledPackagesOutputInternal in the
comment so maintainers know the behavioral difference and when to use each.

In `@src/packages/install.ts`:
- Around line 149-155: The cleanupStandaloneTempArtifacts function currently
awaits rm calls that can throw and mask original errors; wrap each rm invocation
(the extractDir rm and the tempDir rm) in try-catch (or use Promise.allSettled)
so deletion failures are swallowed or logged as warnings instead of rethrowing.
Specifically, update cleanupStandaloneTempArtifacts to catch errors from rm(...)
and call a logger.warn/console.warn with context (including the path) or ignore
them, ensuring the function never throws during cleanup.

In `@src/ui/package-config.ts`:
- Around line 211-213: The code calculates selectedIndex and selectedId without
guarding for an empty settingsItems array, which can yield undefined and risk
out-of-bounds access; update the logic in the block using
getSettingsListSelectedIndex, selectedIndex, settingsItems, selectedId and
selectedRow to first check if settingsItems.length === 0 and return early (or
skip further processing) when empty, otherwise compute selectedIndex (e.g.,
clamp to valid range) and derive selectedId and selectedRow from rowById as
before.

In `@src/ui/unified.ts`:
- Around line 63-69: The branch that runs when hasCustomUI(ctx) is false
currently calls ctx.ui.notify directly, which bypasses the shared headless/RPC
notifier; instead route the warning through the shared notifier helper before
calling showInteractiveFallback so the message is surfaced in headless/RPC mode
(i.e., replace the direct ctx.ui.notify call with the shared notifier invocation
used elsewhere, then await showInteractiveFallback(ctx, pi) as before);
reference symbols: hasCustomUI, ctx.ui.notify, showInteractiveFallback.

In `@src/utils/auto-update.ts`:
- Around line 57-80: The code saves the same nextCheck back into config
redundantly after computing initialDelayMs; remove the conditional
saveAutoUpdateConfig(pi, { ...config, nextCheck }) block (or if the save is
required for persistence after startTimer, replace it with a one-line comment
clarifying that intent). Update the area around startTimer, initialDelayMs,
nextCheck, and saveAutoUpdateConfig so there is no unnecessary write unless
there is a documented side-effect requiring persistence.

In `@src/utils/mode.ts`:
- Around line 53-70: runCustomUI currently overloads undefined for three
meanings (no-custom-ui, user-cancel, and fallback-needed) which forces callers
to invent sentinels; change its return type to a discriminated union (e.g.,
Promise<{status:"ok", value:T} | {status:"cancel"} | {status:"needs-tui"}),
update runCustomUI to map open() results into those three statuses (return
{status:"cancel"} when the user cancelled without emitting the notify, return
{status:"needs-tui"} when custom UI is unavailable and emit the existing notify,
and return {status:"ok", value} when open yields a value), and update all
callers of runCustomUI to handle the new union instead of treating undefined
specially; keep the function name runCustomUI and the notify call but ensure
notify is only invoked for the needs-tui status.

In `@test/helpers/mocks.ts`:
- Around line 22-30: The harness always resolves ctx.ui.custom() to undefined,
preventing tests from exercising the success path; add a customizable handler
(e.g., a new MockHarnessOptions field named customImpl or customResult) similar
to confirmImpl so tests can specify the resolved value or async function, then
update the harness code that supplies ctx.ui.custom() to call and/or return that
customImpl result (falling back to undefined only when not provided). Reference
MockHarnessOptions and ctx.ui.custom() (and mirror the confirmImpl pattern) so
callers can simulate real results, cancellations, or degraded behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 808686eb-6dae-4fa3-ba9a-4f46cb1bb005

📥 Commits

Reviewing files that changed from the base of the PR and between 8b7d21f and ca275ef.

📒 Files selected for processing (30)
  • README.md
  • src/commands/auto-update.ts
  • src/commands/cache.ts
  • src/commands/history.ts
  • src/commands/registry.ts
  • src/packages/discovery.ts
  • src/packages/extensions.ts
  • src/packages/install.ts
  • src/packages/management.ts
  • src/ui/package-config.ts
  • src/ui/remote.ts
  • src/ui/unified.ts
  • src/utils/auto-update.ts
  • src/utils/history.ts
  • src/utils/mode.ts
  • src/utils/package-source.ts
  • src/utils/settings-list.ts
  • src/utils/settings.ts
  • src/utils/status.ts
  • src/utils/timer.ts
  • src/utils/ui-helpers.ts
  • test/auto-update.test.ts
  • test/cache-history.test.ts
  • test/discovery-parser.test.ts
  • test/helpers/mocks.ts
  • test/install-remove.test.ts
  • test/package-config.test.ts
  • test/package-extensions.test.ts
  • test/rpc-mode.test.ts
  • test/unified-items.test.ts

Comment thread README.md
Comment thread src/packages/install.ts Outdated
Comment thread src/utils/package-source.ts
Comment thread src/utils/settings-list.ts Outdated
Comment thread src/utils/settings.ts
Comment thread test/install-remove.test.ts
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.

🧹 Nitpick comments (1)
src/packages/install.ts (1)

149-164: Consider using allSettled for more robust cleanup.

The current Promise.all approach will short-circuit if one deletion fails. While the individual try/catch blocks prevent exceptions from propagating, using Promise.allSettled would be more semantically correct for cleanup operations where all attempts should complete.

Optional refactor
 async function cleanupStandaloneTempArtifacts(tempDir: string, extractDir?: string): Promise<void> {
   const paths = [extractDir, tempDir].filter((path): path is string => Boolean(path));
 
-  await Promise.all(
+  await Promise.allSettled(
     paths.map(async (path) => {
       try {
         await rm(path, { recursive: true, force: true });
       } catch (error) {
         console.warn(
           `[extmgr] Failed to remove temporary standalone install artifact at ${path}:`,
           error
         );
       }
     })
   );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/packages/install.ts` around lines 149 - 164, The
cleanupStandaloneTempArtifacts function uses Promise.all to run removals; change
it to Promise.allSettled so each rm call is awaited to completion regardless of
others (keeping the existing try/catch per-path). Locate
cleanupStandaloneTempArtifacts and replace the Promise.all(...) wrapper with
Promise.allSettled(...) over the same paths.map(async (path) => { try { await
rm(...) } catch (error) { console.warn(...) } }); this ensures all rm operations
finish without short-circuiting while preserving the rm call and error logging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/packages/install.ts`:
- Around line 149-164: The cleanupStandaloneTempArtifacts function uses
Promise.all to run removals; change it to Promise.allSettled so each rm call is
awaited to completion regardless of others (keeping the existing try/catch
per-path). Locate cleanupStandaloneTempArtifacts and replace the
Promise.all(...) wrapper with Promise.allSettled(...) over the same
paths.map(async (path) => { try { await rm(...) } catch (error) {
console.warn(...) } }); this ensures all rm operations finish without
short-circuiting while preserving the rm call and error logging.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 91cdcbcd-db0e-460d-aaba-2d884166e020

📥 Commits

Reviewing files that changed from the base of the PR and between ca275ef and e1f0729.

📒 Files selected for processing (13)
  • README.md
  • src/commands/history.ts
  • src/packages/discovery.ts
  • src/packages/install.ts
  • src/ui/unified.ts
  • src/utils/auto-update.ts
  • src/utils/package-source.ts
  • src/utils/settings-list.ts
  • src/utils/settings.ts
  • test/auto-update.test.ts
  • test/discovery-parser.test.ts
  • test/install-remove.test.ts
  • test/settings-list.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • test/auto-update.test.ts
  • src/utils/auto-update.ts
  • src/utils/settings-list.ts

@ayagmar ayagmar changed the title fix: ship extmgr improvements without plan docs fix: ship extmgr improvements Mar 11, 2026
@ayagmar ayagmar merged commit e896da6 into master Mar 11, 2026
2 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Apr 5, 2026
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