@jonathan0823/pi-sonarqube 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ui.ts CHANGED
@@ -3,7 +3,13 @@ import type {
3
3
  ExtensionContext,
4
4
  ExtensionCommandContext,
5
5
  } from "@earendil-works/pi-coding-agent";
6
- import { Input, Key, matchesKey, truncateToWidth, type Focusable } from "@earendil-works/pi-tui";
6
+ import {
7
+ Input,
8
+ Key,
9
+ matchesKey,
10
+ truncateToWidth,
11
+ type Focusable,
12
+ } from "@earendil-works/pi-tui";
7
13
  import { readFile } from "node:fs/promises";
8
14
  import { resolve } from "node:path";
9
15
  import type {
@@ -162,11 +168,7 @@ export function startAnalysisUi(
162
168
  // ── Searchable browser helpers ─────────────────────────────────────────────
163
169
 
164
170
  function normalizeSearchQuery(query: string): string[] {
165
- return query
166
- .trim()
167
- .toLowerCase()
168
- .split(/\s+/)
169
- .filter(Boolean);
171
+ return query.trim().toLowerCase().split(/\s+/).filter(Boolean);
170
172
  }
171
173
 
172
174
  function matchesSearchQuery(haystack: string, query: string): boolean {
@@ -187,7 +189,11 @@ function filterSearchItems<T>(
187
189
 
188
190
  interface SearchableBrowserConfig<T> {
189
191
  title: string;
190
- subtitle: (query: string, totalCount: number, filteredCount: number) => string;
192
+ subtitle: (
193
+ query: string,
194
+ totalCount: number,
195
+ filteredCount: number,
196
+ ) => string;
191
197
  searchHint: string;
192
198
  emptyMessage: string;
193
199
  footer: string;
@@ -295,7 +301,9 @@ export class IssueBrowser extends SearchableListBrowser<SonarIssue> {
295
301
  theme: Theme,
296
302
  done: (result: number | null) => void,
297
303
  ) {
298
- const filterSuffix = state.filters ? ` • ${issueFilterLabel(state.filters)}` : "";
304
+ const filterSuffix = state.filters
305
+ ? ` • ${issueFilterLabel(state.filters)}`
306
+ : "";
299
307
  super(state.issues, theme, done, {
300
308
  title: "SonarQube Issues",
301
309
  subtitle: (_query, totalCount, filteredCount) =>
@@ -317,7 +325,9 @@ export class IssueBrowser extends SearchableListBrowser<SonarIssue> {
317
325
  const marker = isSelected
318
326
  ? theme.fg("accent", ">")
319
327
  : theme.fg("dim", " ");
320
- const location = issue.line ? `${issue.filePath}:${issue.line}` : issue.filePath;
328
+ const location = issue.line
329
+ ? `${issue.filePath}:${issue.line}`
330
+ : issue.filePath;
321
331
  const rule = issue.ruleName
322
332
  ? `${issue.rule} (${issue.ruleName})`
323
333
  : issue.rule;
@@ -442,7 +452,8 @@ export class DuplicationBrowser extends SearchableListBrowser<FileDuplication> {
442
452
  title: "SonarQube Duplications",
443
453
  subtitle: (_query, totalCount, filteredCount) =>
444
454
  `${totalCount} file(s) with duplicate code${scopeSuffix} • ${filteredCount} match(es)`,
445
- searchHint: "Search duplications by file path, duplicated lines, blocks, or density",
455
+ searchHint:
456
+ "Search duplications by file path, duplicated lines, blocks, or density",
446
457
  emptyMessage: "No matching duplicated files found.",
447
458
  footer: "Up/Down to move, Enter for details, Esc to close",
448
459
  pageSize: 15,
@@ -476,6 +487,48 @@ export async function showDuplicationBrowser(
476
487
  );
477
488
  }
478
489
 
490
+ // ── Workspace selector browser ────────────────────────────────────────────
491
+
492
+ export class WorkspaceBrowser extends SearchableListBrowser<{
493
+ alias: string;
494
+ path: string;
495
+ }> {
496
+ constructor(
497
+ workspaces: { alias: string; path: string }[],
498
+ theme: Theme,
499
+ done: (result: number | null) => void,
500
+ ) {
501
+ super(workspaces, theme, done, {
502
+ title: "SonarQube Workspaces",
503
+ subtitle: (_query, totalCount, filteredCount) =>
504
+ `${totalCount} workspace(s) • ${filteredCount} match(es)`,
505
+ searchHint: "Search workspaces by alias or path",
506
+ emptyMessage: "No matching workspaces found.",
507
+ footer: "Up/Down to move, Enter to select, Esc to cancel",
508
+ pageSize: 15,
509
+ searchText: (ws) => `${ws.alias} ${ws.path}`,
510
+ renderItem: (ws, index, isSelected) => {
511
+ const marker = isSelected
512
+ ? theme.fg("accent", ">")
513
+ : theme.fg("dim", " ");
514
+ return `${marker} ${String(index + 1).padStart(2, " ")}. ${theme.fg("accent", ws.alias.padEnd(20))} ${theme.fg("dim", ws.path)}`;
515
+ },
516
+ });
517
+ }
518
+ }
519
+
520
+ export async function showWorkspaceBrowser(
521
+ ctx: ExtensionContext,
522
+ workspaces: { alias: string; path: string }[],
523
+ ): Promise<string | null> {
524
+ if (ctx.mode !== "tui" || workspaces.length === 0) return null;
525
+ const choice = await ctx.ui.custom<number | null>(
526
+ (_tui, theme, _kb, done) => new WorkspaceBrowser(workspaces, theme, done),
527
+ );
528
+ if (choice == null) return null;
529
+ return workspaces[choice].alias;
530
+ }
531
+
479
532
  // ── Issue browser / preview helpers (used by index.ts) ──────────────────────
480
533
 
481
534
  export async function showIssueBrowser(