Skip to content

[issues/255] PR4: DestinationPickerCommand and Grouped API#302

Merged
couimet merged 5 commits intomainfrom
issues/255-pr4-destination-picker
Feb 5, 2026
Merged

[issues/255] PR4: DestinationPickerCommand and Grouped API#302
couimet merged 5 commits intomainfrom
issues/255-pr4-destination-picker

Conversation

@couimet
Copy link
Owner

@couimet couimet commented Feb 5, 2026

Summary

Fourth PR in the stacked PR series for Issue #255 (Terminal Picker). This PR introduces the DestinationPickerCommand for showing a unified destination picker, along with the grouped destination API on DestinationAvailabilityService. The command returns the user's selection without performing binding, allowing callers to decide what to do with the result.

Changes

  • Add DestinationPickerCommand class that shows a QuickPick with all available destinations
    • AI assistants first, then terminals, then text editor (via DESTINATION_PICKER_SEQUENCE)
    • Returns selection result without binding (caller decides action)
    • Delegates to terminal picker for "More terminals..." selection
    • Three outcome types: selected, cancelled, returned-to-destination-picker
  • Add getGroupedDestinationItems() method to DestinationAvailabilityService
    • Returns destinations grouped by type with pre-built QuickPick items
    • Includes terminal-more placeholder when terminals exceed inline limit
    • Active terminal sorted first with "(active)" description
  • Add buildDestinationQuickPickItems() utility for converting grouped items to QuickPick array
    • Configurable label builder for different display contexts
    • Ordered by DESTINATION_PICKER_SEQUENCE for consistent UX
  • Add BindOptions discriminated union type for type-safe bind operations
    • TerminalBindOptions carries terminal reference
    • TextEditorBindOptions, CursorAIBindOptions, CopilotChatBindOptions, ClaudeCodeBindOptions
  • Add DestinationType string literal union and DESTINATION_TYPES constant array
  • Add GroupedDestinationItems type and BindableQuickPickItem unified item type
  • Add DEFAULT_TERMINAL_PICKER_MAX_INLINE setting (configurable inline terminal limit)
  • Add TERMINAL_PICKER_MORE_TERMINALS_DESCRIPTION i18n message
  • Update DestinationRegistry to use grouped return type
  • Add shared test helpers (createMockBindableQuickPickItem.ts) for QuickPick item creation
  • Update tests with proper unit isolation (mock dependencies, test orchestration)

Test Plan

  • New tests added for:
    • DestinationPickerCommand - no destinations, cancellation, bindable selection, terminal-more flow, logging
    • buildDestinationQuickPickItems() - sequence ordering, label building, terminal-more handling
    • DestinationAvailabilityService.getGroupedDestinationItems() - grouping logic, terminal overflow
    • DestinationRegistry grouped API
  • Shared test helpers extracted to createMockBindableQuickPickItem.ts
  • Manual testing: DestinationPickerCommand not wired yet (PR5 will wire to command)

Related

Summary by CodeRabbit

  • New Features
    • Destination picker UI: grouped destinations (AI assistants, terminals, text editor), explicit outcomes (selected / cancelled / no-resource), secondary "More terminals..." flow, and auto-bind when only one terminal.
    • New setting to control how many terminals show inline in the picker.
    • Picker items show friendly display names and active-terminal badges.
  • Refactor
    • Unified destination/bind options and picker plumbing for a more consistent UX and clearer logging.

## Summary

Fourth PR in the stacked PR series for Issue #255 (Terminal Picker). This PR introduces the `DestinationPickerCommand` for showing a unified destination picker, along with the grouped destination API on `DestinationAvailabilityService`. The command returns the user's selection without performing binding, allowing callers to decide what to do with the result.

## Changes

- Add `DestinationPickerCommand` class that shows a QuickPick with all available destinations
  - AI assistants first, then terminals, then text editor (via `DESTINATION_PICKER_SEQUENCE`)
  - Returns selection result without binding (caller decides action)
  - Delegates to terminal picker for "More terminals..." selection
  - Three outcome types: `selected`, `cancelled`, `returned-to-destination-picker`
- Add `getGroupedDestinationItems()` method to `DestinationAvailabilityService`
  - Returns destinations grouped by type with pre-built QuickPick items
  - Includes `terminal-more` placeholder when terminals exceed inline limit
  - Active terminal sorted first with "(active)" description
- Add `buildDestinationQuickPickItems()` utility for converting grouped items to QuickPick array
  - Configurable label builder for different display contexts
  - Ordered by `DESTINATION_PICKER_SEQUENCE` for consistent UX
- Add `BindOptions` discriminated union type for type-safe bind operations
  - `TerminalBindOptions` carries terminal reference
  - `TextEditorBindOptions`, `CursorAIBindOptions`, `CopilotChatBindOptions`, `ClaudeCodeBindOptions`
- Add `DestinationType` string literal union and `DESTINATION_TYPES` constant array
- Add `GroupedDestinationItems` type and `BindableQuickPickItem` unified item type
- Add `DEFAULT_TERMINAL_PICKER_MAX_INLINE` setting (configurable inline terminal limit)
- Add `TERMINAL_PICKER_MORE_TERMINALS_DESCRIPTION` i18n message
- Update `DestinationRegistry` to use grouped return type
- Add shared test helpers (`createMockBindableQuickPickItem.ts`) for QuickPick item creation
- Update tests with proper unit isolation (mock dependencies, test orchestration)

## Test Plan

- [x] New tests added for:
  - `DestinationPickerCommand` - no destinations, cancellation, bindable selection, terminal-more flow, logging
  - `buildDestinationQuickPickItems()` - sequence ordering, label building, terminal-more handling
  - `DestinationAvailabilityService.getGroupedDestinationItems()` - grouping logic, terminal overflow
  - `DestinationRegistry` grouped API
- [x] Shared test helpers extracted to `createMockBindableQuickPickItem.ts`
- [ ] Manual testing: DestinationPickerCommand not wired yet (PR5 will wire to command)

## Related

- Part of #255 (Terminal Picker)
- Depends on: PR1 (#294), PR2 (#296), PR3 (#298)
@coderabbitai
Copy link

coderabbitai bot commented Feb 5, 2026

Walkthrough

Renames destination discriminator typekind, adds typed BindOptions and DestinationType, introduces DestinationPickerCommand and grouped destination retrieval in DestinationAvailabilityService, refactors QuickPick items to bindOptions/bindable, adds terminal-picker overflow handling, updates wiring, settings, and tests.

Changes

Cohort / File(s) Summary
Core types & typing
src/types/BindOptions.ts, src/types/DestinationType.ts, src/types/GroupedDestinationTypes.ts, src/types/MessageCode.ts, src/types/AvailableDestination.ts, src/types/index.ts
Add BindOptions union and DestinationType const/types; introduce grouped destination types and new message codes; rename AvailableDestination.typekind and export new types.
QuickPick model & utilities
src/types/QuickPickTypes.ts, src/destinations/utils/buildDestinationQuickPickItems.ts, src/destinations/utils/showTerminalPicker.ts, src/destinations/utils/assertTerminalFromPicker.ts, src/destinations/utils/index.ts, src/destinations/utils/getTerminalDestinationEligibility.ts
Replace terminal-specific QuickPick model with BindableQuickPickItem + bindOptions; add DestinationQuickPickItem, DESTINATION_PICKER_SEQUENCE, and buildDestinationQuickPickItems; showTerminalPicker gains a selected outcome and uses bindable items; assertTerminalFromPicker reads terminal from bindOptions; rename isTerminalDestinationEligible → getTerminalDestinationEligibility.
Destination availability & registry
src/destinations/DestinationAvailabilityService.ts, src/destinations/DestinationRegistry.ts
Add configReader to availability service constructor; migrate discriminant to kind; add getGroupedDestinationItems() with terminal inline/overflow handling and terminal-threshold resolution; registry.create now uses options.kind and updated logs/errors.
Commands & picker flow
src/commands/DestinationPickerCommand.ts, src/commands/BindToTerminalCommand.ts, src/commands/index.ts
Add DestinationPickerCommand implementing primary+secondary picker loop and export types; BindToTerminalCommand now depends on DestinationAvailabilityService and consumes bindable quick-pick items; index exports picker types.
Builders & paste manager
src/destinations/destinationBuilders.ts, src/destinations/PasteDestinationManager.ts
Change builder validations and PasteDestinationManager flows to use kind; update error payloads/messages to use actualKind/expectedKind and rename internal params accordingly.
Settings, messages & constants
src/constants/settingKeys.ts, src/constants/settingDefaults.ts, src/i18n/messages.en.ts, src/types/MessageCode.ts
Add terminal picker setting key/default (terminalPicker.maxInline / DEFAULT_TERMINAL_PICKER_MAX_INLINE) and i18n messages for terminal display format and "X more".
Extension wiring & exports
src/extension.ts, src/commands/index.ts, src/destinations/PasteDestinationManager.ts
Update constructor wiring to pass configReader, availabilityService, and new context ordering; instantiate BindToTerminalCommand with availabilityService; export new command/types.
QuickPick & test helpers
src/__tests__/helpers/createMockBindableQuickPickItem.ts, src/__tests__/helpers/createMockDestinationRegistry.ts, src/__tests__/helpers/createMockDestinationAvailabilityService.ts, src/__tests__/helpers/index.ts
Add mock factories for bindable quick-pick items and grouped items; update mock registry to use kind; add getGroupedDestinationItems mock hook and re-export helper.
Tests updated
src/__tests__/commands/BindToTerminalCommand.test.ts, src/__tests__/commands/DestinationPickerCommand.test.ts, src/__tests__/destinations/**, src/__tests__/destinations/utils/**, src/__tests__/statusBar/RangeLinkStatusBar.test.ts
Widespread test updates: typekind, new constructor signatures, grouped destination items and bindOptions shapes, DestinationPickerCommand flows, adjusted logging and assertions.

Sequence Diagram(s)

sequenceDiagram
  participant Caller as Caller
  participant DAS as DestinationAvailabilityService
  participant UI as QuickPickUI
  participant Sec as SecondaryPicker
  participant Log as Logger

  Caller->>DAS: getGroupedDestinationItems({ destinationTypes: ['terminal'], terminalThreshold: Infinity })
  DAS-->>Caller: groupedItems (bindable items [+ terminal-more?])
  Caller->>UI: show primary destination picker (groupedItems)
  UI-->>Caller: selection
  alt selection.itemKind == "bindable"
    Caller->>Log: log selected bindOptions
    Caller-->>Caller: return { outcome: 'selected', bindOptions }
  else selection.itemKind == "terminal-more"
    Caller->>Sec: showSecondaryTerminalPicker(...)
    Sec-->>Caller: result (selected | cancelled | returned-to-destination-picker)
    alt selected
      Caller-->>Caller: return { outcome: 'selected', bindOptions: terminal }
    else cancelled
      Caller-->>Caller: return { outcome: 'cancelled' }
    else returned-to-destination-picker
      Caller->>UI: show primary destination picker (loop)
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through kinds, not types, today,
QuickPicks carry bindOptions on display,
Terminals inline or "more" when they grow,
A picker that loops and a service that knows,
I nibble the changes and bound happily away! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: introducing DestinationPickerCommand and a grouped destination API, which aligns with the core objectives of this PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issues/255-pr4-destination-picker

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

@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: 2

Caution

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

⚠️ Outside diff range comments (1)
packages/rangelink-vscode-extension/src/types/AvailableDestination.ts (1)

1-1: ⚠️ Potential issue | 🟡 Minor

Change the import to reference the canonical source.

DestinationType is defined in two places: ./DestinationType.ts (which also exports the canonical DESTINATION_TYPES constant) and ../destinations/PasteDestination.ts. To avoid duplication and maintain a single source of truth, import directly from ./DestinationType:

import type { DestinationType } from './DestinationType';
🤖 Fix all issues with AI agents
In
`@packages/rangelink-vscode-extension/src/destinations/DestinationAvailabilityService.ts`:
- Around line 154-160: In getGroupedDestinationItems, add DEBUG logs that state
whether destinationTypes and terminalThreshold came from the caller or from
defaults; specifically, after resolving destinationTypes (symbol:
destinationTypes, fallback: DESTINATION_TYPES) log whether
options?.destinationTypes was used vs the default, and after resolving
terminalThreshold (symbol: terminalThreshold, fallback via
this.configReader.getWithDefault with keys SETTING_TERMINAL_PICKER_MAX_INLINE
and DEFAULT_TERMINAL_PICKER_MAX_INLINE) log whether options?.terminalThreshold
was used or the config/default value was applied; use the existing logger at
DEBUG level and include the chosen value in each message for clarity.
- Around line 180-188: The terminal eligibility guard in
DestinationAvailabilityService is using a boolean negation on
isTerminalDestinationEligible(this.ideAdapter) which returns an eligibility
object; update the guard to check the object's .eligible property (e.g., if
(!isTerminalDestinationEligible(this.ideAdapter).eligible) break) so
buildGroupedTerminalItems, getEligibleTerminals and subsequent terminal grouping
only run when the terminal destination is actually eligible; ensure the same
.eligible check pattern matches how getAvailableDestinations uses the
eligibility object.
🧹 Nitpick comments (9)
packages/rangelink-vscode-extension/src/__tests__/helpers/createMockDestinationRegistry.ts (2)

17-19: Minor: Update JSDoc to reflect kind terminology.

The JSDoc comment on Line 18 still references "based on type", but the implementation now uses kind. Consider updating for consistency.

   /**
    * Pre-configured destinations to return from create().
-   * Registry will return the appropriate destination based on type.
+   * Registry will return the appropriate destination based on kind.
    */

50-52: Minor: Update JSDoc to reflect kind terminology.

The JSDoc comment on Line 51 still references "requested type", but the implementation now uses kind.

-   * based on the requested type. This eliminates test coupling to registry's
+   * based on the requested kind. This eliminates test coupling to registry's
packages/rangelink-vscode-extension/src/__tests__/helpers/createMockBindableQuickPickItem.ts (1)

94-96: Consider: Type assertion may cause issues with partial objects.

The function returns groups directly as GroupedDestinationItems, but Partial<GroupedDestinationItems> may not satisfy all required properties if GroupedDestinationItems has required fields. This works if all fields are optional, but could cause runtime issues if the type evolves.

If GroupedDestinationItems is designed to have all optional properties, this is fine. Otherwise, consider documenting this assumption or adding a type guard.

packages/rangelink-vscode-extension/src/destinations/utils/assertTerminalFromPicker.ts (1)

21-38: Consider using generic parameter for stronger type safety.

The function accepts BindableQuickPickItem and casts to TerminalBindOptions at line 28. Using the generic parameter would provide compile-time type safety:

💡 Optional: Strengthen type signature
 export const assertTerminalFromPicker = <T>(
-  selected: BindableQuickPickItem,
+  selected: BindableQuickPickItem<TerminalBindOptions>,
   functionName: string,
   logger: Logger,
   logMessage: string,
   action: (terminal: vscode.Terminal) => T,
 ): T => {
-  const terminalBindOptions = selected.bindOptions as TerminalBindOptions;
+  const terminalBindOptions = selected.bindOptions;
   if (!terminalBindOptions?.terminal) {

This would shift validation to callers at compile time rather than relying solely on runtime checks. However, the current implementation is also valid if callers may pass mixed item types.

packages/rangelink-vscode-extension/src/destinations/utils/buildDestinationQuickPickItems.ts (1)

68-75: Defensive validation is currently unreachable but provides future safety.

Given that DESTINATION_PICKER_SEQUENCE is typed as readonly (DestinationType | 'terminal-more')[] and 'terminal-more' is handled above, isDestinationKind(key) will always return true at this point. This check becomes valuable if the sequence array is ever modified or derived dynamically.

Consider if this defensive check adds sufficient value, or if a compile-time assertion (via type narrowing) would be clearer. The current approach is acceptable for runtime safety.

packages/rangelink-vscode-extension/src/__tests__/destinations/utils/showTerminalPicker.test.ts (1)

644-717: Avoid logging-only tests; fold log assertions into behavior tests.
These cases currently validate logging without asserting outcomes, which conflicts with the test guidelines.

♻️ Suggested consolidation
@@
-    await showTerminalPicker(
+    const result = await showTerminalPicker(
       terminals,
       undefined,
       adapter,
       DEFAULT_OPTIONS,
       logger,
       identityCallback,
     );
+    expect(result).toStrictEqual({ outcome: 'selected', result: terminals[0] });
@@
-    await showTerminalPicker(
+    const result = await showTerminalPicker(
       terminals,
       undefined,
       adapter,
       DEFAULT_OPTIONS,
       logger,
       identityCallback,
     );
+    expect(result).toStrictEqual({ outcome: 'cancelled' });
@@
-    await showTerminalPicker(
+    const result = await showTerminalPicker(
       terminals,
       undefined,
       adapter,
       DEFAULT_OPTIONS,
       logger,
       identityCallback,
     );
+    expect(result).toStrictEqual({ outcome: 'selected', result: terminals[1] });

Based on learnings: Include logger assertions in tests that verify method behavior - consolidate logging verification with behavior tests, never create separate tests just for logging.

packages/rangelink-vscode-extension/src/__tests__/commands/DestinationPickerCommand.test.ts (1)

241-295: Avoid standalone logging-only tests; fold logs into behavior assertions.

Please merge these log assertions into the related behavior tests instead of keeping a separate logging suite.

As per coding guidelines: Include logger assertions in tests that verify method behavior - consolidate logging verification with behavior tests, never create separate tests just for logging.

packages/rangelink-vscode-extension/src/__tests__/commands/BindToTerminalCommand.test.ts (1)

117-167: Add logger assertions for abort scenarios.
These tests validate behavior but skip log expectations; align them with the other cases that assert logging. As per coding guidelines: Include logger assertions in tests that verify method behavior - consolidate logging verification with behavior tests, never create separate tests just for logging.

packages/rangelink-vscode-extension/src/destinations/DestinationAvailabilityService.ts (1)

146-226: Consider splitting getGroupedDestinationItems into per-kind handlers.
The method is long and mixes eligibility, grouping, and logging logic; extracting handlers would reduce complexity. As per coding guidelines: Extract functions into separate modules when they exceed 50 lines, have 3+ dependencies, or mix concerns (UI + business logic) using /utils/, /services/, or /src/ subdirectories.

Fixes bug where terminal eligibility check used object as boolean (always truthy).
Renames isTerminalDestinationEligible → getTerminalDestinationEligibility to follow
naming convention (get* returns objects, is* returns booleans), preventing similar
bugs in the future.

Benefits:
- Fixes broken terminal eligibility guard in getGroupedDestinationItems
- Naming convention makes return type obvious at call sites
- Consolidates standalone logging tests into behavior tests (rule T007)
- Removes no-op createMockGroupedDestinationItems helper
- Adds DEBUG logs for parameter resolution in getGroupedDestinationItems

Ignored Feedback:
- Generic parameter suggestion for assertTerminalFromPicker: Current runtime check is valid and more flexible
- Defensive validation note in buildDestinationQuickPickItems: Acknowledged but provides future safety
- Method length suggestion for getGroupedDestinationItems: Method is cohesive; splitting adds complexity without benefit

Ref: #302 (review)
Copy link

@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/rangelink-vscode-extension/src/__tests__/destinations/utils/showTerminalPicker.test.ts (1)

85-147: ⚠️ Potential issue | 🟡 Minor

Add logger assertions in these behavior tests.

These tests assert picker behavior but don’t validate the debug logging, which the test guidelines require to be included alongside behavior verification. Consider mirroring the log expectations already used in the 2-terminal test.

💡 Example addition for the "5 terminals exist" test
       expect(showQuickPickMock).toHaveBeenCalledWith(
         [
           {
             label: 'terminal-1',
             description: undefined,
             displayName: 'terminal-1',
             bindOptions: { kind: 'terminal', terminal: terminals[0] },
             itemKind: 'bindable',
           },
           {
             label: 'terminal-2',
             description: undefined,
             displayName: 'terminal-2',
             bindOptions: { kind: 'terminal', terminal: terminals[1] },
             itemKind: 'bindable',
           },
           {
             label: 'terminal-3',
             description: undefined,
             displayName: 'terminal-3',
             bindOptions: { kind: 'terminal', terminal: terminals[2] },
             itemKind: 'bindable',
           },
           {
             label: 'terminal-4',
             description: undefined,
             displayName: 'terminal-4',
             bindOptions: { kind: 'terminal', terminal: terminals[3] },
             itemKind: 'bindable',
           },
           {
             label: 'terminal-5',
             description: undefined,
             displayName: 'terminal-5',
             bindOptions: { kind: 'terminal', terminal: terminals[4] },
             itemKind: 'bindable',
           },
         ],
         { title: 'Select Terminal', placeHolder: 'Choose a terminal to bind to' },
       );
+      expect(logger.debug).toHaveBeenCalledWith(
+        { fn: 'showTerminalPicker', terminalCount: 5, itemCount: 5 },
+        'Showing terminal picker',
+      );
+      expect(logger.debug).toHaveBeenCalledWith(
+        {
+          fn: 'showTerminalPicker',
+          selected: {
+            label: 'terminal-3',
+            displayName: 'terminal-3',
+            bindOptions: { kind: 'terminal', terminal: terminals[2] },
+            itemKind: 'bindable',
+          },
+        },
+        'Terminal selected',
+      );

Based on learnings: Include logger assertions in tests that verify method behavior - consolidate logging verification with behavior tests, never create separate tests just for logging.

Also applies to: 149-198, 201-338, 341-528

🤖 Fix all issues with AI agents
In
`@packages/rangelink-vscode-extension/src/destinations/DestinationAvailabilityService.ts`:
- Around line 167-173: The terminalThreshold value (computed from
options?.terminalThreshold or this.configReader.getWithDefault using
SETTING_TERMINAL_PICKER_MAX_INLINE/DEFAULT_TERMINAL_PICKER_MAX_INLINE) needs
sanitization to prevent negative/NaN values; in the
DestinationAvailabilityService where terminalThreshold is assigned and later
used (including the other block around 264-292), coerce to a number and
validate: if not Number.isFinite(value) or value < 0 set terminalThreshold = 0,
and use Math.floor(value) if you expect an integer, then proceed with
slicing/remaining-count logic using this sanitized terminalThreshold.
🧹 Nitpick comments (2)
packages/rangelink-vscode-extension/src/destinations/DestinationAvailabilityService.ts (1)

138-259: Consider extracting getGroupedDestinationItems into focused helpers.
It now mixes eligibility checks with QuickPick item construction and exceeds 50 lines, which makes it harder to test and maintain.

As per coding guidelines: Extract functions into separate modules when they exceed 50 lines, have 3+ dependencies, or mix concerns (UI + business logic) using /utils/, /services/, or /src/ subdirectories.

packages/rangelink-vscode-extension/src/__tests__/commands/BindToTerminalCommand.test.ts (1)

52-68: Use a named constant for the Infinity threshold.
This value has semantic meaning (no inline limit) and is repeated in expectations/options.

♻️ Suggested refactor
 import { BindAbortReason, type TerminalBindResult } from '../../types';
 import {
   createMockDestinationAvailabilityService,
@@
   createMockVscodeAdapter,
 } from '../helpers';
 
+const TERMINAL_PICKER_THRESHOLD = Infinity;
+
 describe('BindToTerminalCommand', () => {
@@
         expect(mockAvailabilityService.getGroupedDestinationItems).toHaveBeenCalledWith({
           destinationTypes: ['terminal'],
-          terminalThreshold: Infinity,
+          terminalThreshold: TERMINAL_PICKER_THRESHOLD,
         });
@@
           {
-            maxItemsBeforeMore: Infinity,
+            maxItemsBeforeMore: TERMINAL_PICKER_THRESHOLD,
             title: 'RangeLink',
             placeholder: 'Select terminal to bind to',
             activeDescription: 'active',

As per coding guidelines: Define named constants using SCREAMING_SNAKE_CASE for all numeric literals with semantic meaning.

Also applies to: 345-350

Adds validation for terminalThreshold config to guard against users manually
editing settings.json with invalid values (negative, zero, or NaN).

Changes:
- Validates terminalThreshold < MIN_TERMINAL_PICKER_THRESHOLD or NaN
- Logs warning with invalid value and falls back to default (5)
- Boundary tests for 0, NaN, 1, 2 with return value assertions

Ignored Feedback:
- Logger assertions in every test: Primary tests already include logging verification
- Extract getGroupedDestinationItems helper: Already addressed in previous review
- Use TERMINAL_PICKER_SHOW_ALL constant: Rule T003 requires literal values in assertions

Ref: #302 (review)
Copy link

@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

🤖 Fix all issues with AI agents
In
`@packages/rangelink-vscode-extension/src/destinations/DestinationAvailabilityService.ts`:
- Around line 169-198: The debug message can be misleading when
options.terminalThreshold is provided but invalid; update
DestinationAvailabilityService.getGroupedDestinationItems to determine the
effective source after validation (e.g., compute a boolean or a string like
source = options?.terminalThreshold !== undefined &&
!Number.isNaN(options.terminalThreshold) && options.terminalThreshold >=
MIN_TERMINAL_PICKER_THRESHOLD ? 'provided' : 'config/default') and use that when
logging instead of directly checking options?.terminalThreshold; ensure
terminalThreshold still falls back to DEFAULT_TERMINAL_PICKER_MAX_INLINE and the
debug call uses the computed source and the final terminalThreshold value
(referencing terminalThreshold, options.terminalThreshold,
DEFAULT_TERMINAL_PICKER_MAX_INLINE).
🧹 Nitpick comments (1)
packages/rangelink-vscode-extension/src/destinations/DestinationAvailabilityService.ts (1)

148-274: Consider extracting per-kind builders to keep this method focused.
getGroupedDestinationItems is >50 lines and mixes eligibility checks, item construction, and logging across multiple dependencies. Extracting per-kind builders into utils/private helpers would make the orchestration logic clearer and easier to test.

As per coding guidelines: Extract functions into separate modules when they exceed 50 lines, have 3+ dependencies, or mix concerns (UI + business logic) using /utils/, /services/, or /src/ subdirectories.

The debug log was checking `options?.terminalThreshold !== undefined` to determine
the source, but this logged "Using provided terminalThreshold" even when the provided
value was invalid and fell back to the default.

Now computes `providedWasValid` after checking validity, and logs the actual source
("provided" vs "config/default") that reflects what was actually used.

Extracts `isValidThreshold()` helper to avoid duplicating the validation logic
between the providedWasValid check and the fallback condition.

Benefits:
- Debug logs accurately reflect what happened during threshold resolution
- Single unified log instead of two conditional branches
- DRY: validation logic defined once in isValidThreshold()

Ignored Feedback:
- Extract per-kind builders: Switch-case methods with clear separation are readable
  as-is; extracting single-call private methods adds indirection without improving
  testability or reuse

Ref: #302 (review)
Copy link

@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: 2

🤖 Fix all issues with AI agents
In
`@packages/rangelink-vscode-extension/src/destinations/DestinationAvailabilityService.ts`:
- Around line 32-35: isValidThreshold currently allows fractional and non-finite
values which leads to non-integer remainingCount and UI issues; change
validation and normalization so terminalThreshold is a safe finite integer (use
Number.isFinite and convert to an integer via Math.trunc/Math.floor or
Number.isInteger check) before using it to compute remainingCount, and ensure
MIN_TERMINAL_PICKER_THRESHOLD is still enforced; apply the same fix where
terminalThreshold is validated/used (the block around lines 172-193) so all uses
normalize to an integer.
- Around line 151-278: getGroupedDestinationItems is over 50 lines and mixes
option resolution, validation/logging, eligibility checks and item construction;
split responsibilities by extracting: (1) threshold resolution/validation into a
helper resolveTerminalThreshold(options, configReader, logger) that returns {
terminalThreshold, thresholdSource, providedWasValid }, (2) per-kind builders
such as buildTextEditorItem(ideAdapter, displayNames),
buildTerminalGroup(ideAdapter, displayNames, terminalThreshold) — note
buildGroupedTerminalItems already exists and should be reused inside the
terminal builder — and buildAIAssistantItem(kind, displayNames,
isAIAssistantAvailable) for 'claude-code'/'cursor-ai'/'github-copilot-chat';
have getGroupedDestinationItems orchestrate calling these helpers, keep existing
logging calls but move logging related to threshold and per-kind decisions into
the new helpers where appropriate, and preserve thrown RangeLinkExtensionError
for unexpected kinds.

Comment on lines 151 to 278
async getGroupedDestinationItems(
options?: GetAvailableDestinationItemsOptions,
): Promise<GroupedDestinationItems> {
const displayNames = this.registry.getDisplayNames();
const result: {
-readonly [K in keyof GroupedDestinationItems]: GroupedDestinationItems[K];
} = {};

const destinationTypes = options?.destinationTypes ?? DESTINATION_TYPES;
if (options?.destinationTypes) {
this.logger.debug(
{ fn: 'DestinationAvailabilityService.getGroupedDestinationItems', destinationTypes },
'Using provided destinationTypes filter',
);
} else {
this.logger.debug(
{ fn: 'DestinationAvailabilityService.getGroupedDestinationItems' },
'Using default DESTINATION_TYPES',
);
}

let terminalThreshold =
options?.terminalThreshold ??
this.configReader.getWithDefault(
SETTING_TERMINAL_PICKER_MAX_INLINE,
DEFAULT_TERMINAL_PICKER_MAX_INLINE,
);

const providedWasValid =
options?.terminalThreshold !== undefined && isValidThreshold(options.terminalThreshold);

if (!isValidThreshold(terminalThreshold)) {
this.logger.warn(
{
fn: 'DestinationAvailabilityService.getGroupedDestinationItems',
invalidValue: terminalThreshold,
fallback: DEFAULT_TERMINAL_PICKER_MAX_INLINE,
},
'Invalid terminalThreshold, using default',
);
terminalThreshold = DEFAULT_TERMINAL_PICKER_MAX_INLINE;
}

const thresholdSource = providedWasValid ? 'provided' : 'config/default';
this.logger.debug(
{
fn: 'DestinationAvailabilityService.getGroupedDestinationItems',
terminalThreshold,
thresholdSource,
},
`Using ${thresholdSource} terminalThreshold`,
);

for (const kind of destinationTypes) {
switch (kind) {
case 'text-editor': {
const textEditorEligibility = isTextEditorDestinationEligible(this.ideAdapter);
if (!textEditorEligibility.eligible) break;

const displayName = `${displayNames['text-editor']} ("${textEditorEligibility.filename}")`;
result['text-editor'] = [
{
label: displayName,
displayName,
bindOptions: { kind: 'text-editor' },
itemKind: 'bindable',
},
];
break;
}

case 'terminal': {
if (!getTerminalDestinationEligibility(this.ideAdapter).eligible) break;

const eligibleTerminals = getEligibleTerminals(this.ideAdapter);
const { items, moreItem } = this.buildGroupedTerminalItems(
eligibleTerminals,
terminalThreshold,
);
if (items.length > 0) {
result['terminal'] = items;
}
if (moreItem) {
result['terminal-more'] = moreItem;
}
break;
}

case 'claude-code':
case 'cursor-ai':
case 'github-copilot-chat': {
const available = await this.isAIAssistantAvailable(kind);
if (!available) break;

const displayName = displayNames[kind];
result[kind] = [
{
label: displayName,
displayName,
bindOptions: { kind },
itemKind: 'bindable',
},
];
break;
}

default: {
const _exhaustiveCheck: never = kind;
throw new RangeLinkExtensionError({
code: RangeLinkExtensionErrorCodes.UNEXPECTED_DESTINATION_TYPE,
message: `Unhandled destination kind in getGroupedDestinationItems`,
functionName: 'DestinationAvailabilityService.getGroupedDestinationItems',
details: { kind: _exhaustiveCheck },
});
}
}
}

this.logger.debug(
{
fn: 'DestinationAvailabilityService.getGroupedDestinationItems',
groupKeys: Object.keys(result),
},
`Built grouped destination items`,
);

return result;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Extract getGroupedDestinationItems into helper module(s).
This method is >50 lines and mixes option resolution, validation/logging, eligibility checks, and item construction. Consider extracting threshold resolution and per-kind builders into /utils or a dedicated service module, leaving this as orchestration.

As per coding guidelines: Extract functions into separate modules when they exceed 50 lines, have 3+ dependencies, or mix concerns (UI + business logic) using /utils/, /services/, or /src/ subdirectories.

🤖 Prompt for AI Agents
In
`@packages/rangelink-vscode-extension/src/destinations/DestinationAvailabilityService.ts`
around lines 151 - 278, getGroupedDestinationItems is over 50 lines and mixes
option resolution, validation/logging, eligibility checks and item construction;
split responsibilities by extracting: (1) threshold resolution/validation into a
helper resolveTerminalThreshold(options, configReader, logger) that returns {
terminalThreshold, thresholdSource, providedWasValid }, (2) per-kind builders
such as buildTextEditorItem(ideAdapter, displayNames),
buildTerminalGroup(ideAdapter, displayNames, terminalThreshold) — note
buildGroupedTerminalItems already exists and should be reused inside the
terminal builder — and buildAIAssistantItem(kind, displayNames,
isAIAssistantAvailable) for 'claude-code'/'cursor-ai'/'github-copilot-chat';
have getGroupedDestinationItems orchestrate calling these helpers, keep existing
logging calls but move logging related to threshold and per-kind decisions into
the new helpers where appropriate, and preserve thrown RangeLinkExtensionError
for unexpected kinds.

Extracts threshold resolution logic into a dedicated private method to reduce
cognitive load in getGroupedDestinationItems().

The new method encapsulates:
- Retrieving threshold from options or config
- Validation with warning log for invalid values
- Normalization via Math.floor()
- Debug logging with full context (providedThreshold, configThreshold, effectiveThreshold)

The debug log now shows all three values - no ternary needed, the values tell
the story: providedThreshold undefined means config was used, effectiveThreshold
differing from input means validation/flooring was applied.

Benefits:
- Single responsibility - threshold resolution is self-contained
- getGroupedDestinationItems() stays focused on orchestration
- Debug logs show complete picture for troubleshooting

Ref: #302 (comment)
@couimet couimet merged commit d8de171 into main Feb 5, 2026
2 checks passed
@couimet couimet deleted the issues/255-pr4-destination-picker branch February 5, 2026 19:12
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