@stigmer/react 0.0.53 → 0.0.55

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.
Files changed (118) hide show
  1. package/execution/ArtifactCard.d.ts +11 -1
  2. package/execution/ArtifactCard.d.ts.map +1 -1
  3. package/execution/ArtifactCard.js +22 -3
  4. package/execution/ArtifactCard.js.map +1 -1
  5. package/execution/ArtifactPreviewModal.d.ts.map +1 -1
  6. package/execution/ArtifactPreviewModal.js +1 -1
  7. package/execution/ArtifactPreviewModal.js.map +1 -1
  8. package/execution/ArtifactsWidget.d.ts +26 -19
  9. package/execution/ArtifactsWidget.d.ts.map +1 -1
  10. package/execution/ArtifactsWidget.js +32 -26
  11. package/execution/ArtifactsWidget.js.map +1 -1
  12. package/execution/MessageThread.d.ts +10 -1
  13. package/execution/MessageThread.d.ts.map +1 -1
  14. package/execution/MessageThread.js +21 -17
  15. package/execution/MessageThread.js.map +1 -1
  16. package/execution/SandboxContext.d.ts +32 -0
  17. package/execution/SandboxContext.d.ts.map +1 -0
  18. package/execution/SandboxContext.js +26 -0
  19. package/execution/SandboxContext.js.map +1 -0
  20. package/execution/SetupProgress.d.ts +23 -13
  21. package/execution/SetupProgress.d.ts.map +1 -1
  22. package/execution/SetupProgress.js +18 -12
  23. package/execution/SetupProgress.js.map +1 -1
  24. package/execution/ToolArgsView.d.ts.map +1 -1
  25. package/execution/ToolArgsView.js +3 -1
  26. package/execution/ToolArgsView.js.map +1 -1
  27. package/execution/ToolCallDetail.d.ts.map +1 -1
  28. package/execution/ToolCallDetail.js +3 -1
  29. package/execution/ToolCallDetail.js.map +1 -1
  30. package/execution/ToolCallItem.d.ts.map +1 -1
  31. package/execution/ToolCallItem.js +7 -1
  32. package/execution/ToolCallItem.js.map +1 -1
  33. package/execution/WriteBackCard.d.ts +34 -0
  34. package/execution/WriteBackCard.d.ts.map +1 -0
  35. package/execution/WriteBackCard.js +75 -0
  36. package/execution/WriteBackCard.js.map +1 -0
  37. package/execution/WriteBacksWidget.d.ts +49 -0
  38. package/execution/WriteBacksWidget.d.ts.map +1 -0
  39. package/execution/WriteBacksWidget.js +44 -0
  40. package/execution/WriteBacksWidget.js.map +1 -0
  41. package/execution/__tests__/file-path-resolver.test.d.ts +2 -0
  42. package/execution/__tests__/file-path-resolver.test.d.ts.map +1 -0
  43. package/execution/__tests__/file-path-resolver.test.js +180 -0
  44. package/execution/__tests__/file-path-resolver.test.js.map +1 -0
  45. package/execution/file-path-resolver.d.ts +3 -3
  46. package/execution/file-path-resolver.d.ts.map +1 -1
  47. package/execution/file-path-resolver.js +23 -12
  48. package/execution/file-path-resolver.js.map +1 -1
  49. package/execution/index.d.ts +9 -0
  50. package/execution/index.d.ts.map +1 -1
  51. package/execution/index.js +5 -0
  52. package/execution/index.js.map +1 -1
  53. package/execution/sandbox-path-normalizer.d.ts +46 -0
  54. package/execution/sandbox-path-normalizer.d.ts.map +1 -0
  55. package/execution/sandbox-path-normalizer.js +73 -0
  56. package/execution/sandbox-path-normalizer.js.map +1 -0
  57. package/execution/useArtifactContent.d.ts +5 -1
  58. package/execution/useArtifactContent.d.ts.map +1 -1
  59. package/execution/useArtifactContent.js +6 -2
  60. package/execution/useArtifactContent.js.map +1 -1
  61. package/execution/useWorkspaceWriteBacks.d.ts +40 -0
  62. package/execution/useWorkspaceWriteBacks.d.ts.map +1 -0
  63. package/execution/useWorkspaceWriteBacks.js +41 -0
  64. package/execution/useWorkspaceWriteBacks.js.map +1 -0
  65. package/github/GitHubRepoPicker.d.ts +5 -2
  66. package/github/GitHubRepoPicker.d.ts.map +1 -1
  67. package/github/GitHubRepoPicker.js +133 -36
  68. package/github/GitHubRepoPicker.js.map +1 -1
  69. package/github/index.d.ts +1 -0
  70. package/github/index.d.ts.map +1 -1
  71. package/github/index.js +1 -0
  72. package/github/index.js.map +1 -1
  73. package/github/useGitHubSearch.d.ts +20 -0
  74. package/github/useGitHubSearch.d.ts.map +1 -0
  75. package/github/useGitHubSearch.js +127 -0
  76. package/github/useGitHubSearch.js.map +1 -0
  77. package/index.d.ts +6 -6
  78. package/index.d.ts.map +1 -1
  79. package/index.js +3 -3
  80. package/index.js.map +1 -1
  81. package/package.json +4 -4
  82. package/session/index.d.ts +4 -0
  83. package/session/index.d.ts.map +1 -1
  84. package/session/index.js +2 -0
  85. package/session/index.js.map +1 -1
  86. package/session/useSessionArtifacts.d.ts +73 -0
  87. package/session/useSessionArtifacts.d.ts.map +1 -0
  88. package/session/useSessionArtifacts.js +95 -0
  89. package/session/useSessionArtifacts.js.map +1 -0
  90. package/session/useSessionWriteBacks.d.ts +56 -0
  91. package/session/useSessionWriteBacks.d.ts.map +1 -0
  92. package/session/useSessionWriteBacks.js +56 -0
  93. package/session/useSessionWriteBacks.js.map +1 -0
  94. package/src/execution/ArtifactCard.tsx +40 -0
  95. package/src/execution/ArtifactPreviewModal.tsx +2 -0
  96. package/src/execution/ArtifactsWidget.tsx +67 -43
  97. package/src/execution/MessageThread.tsx +23 -1
  98. package/src/execution/SandboxContext.ts +47 -0
  99. package/src/execution/SetupProgress.tsx +33 -14
  100. package/src/execution/ToolArgsView.tsx +3 -1
  101. package/src/execution/ToolCallDetail.tsx +3 -1
  102. package/src/execution/ToolCallItem.tsx +7 -1
  103. package/src/execution/WriteBackCard.tsx +210 -0
  104. package/src/execution/WriteBacksWidget.tsx +82 -0
  105. package/src/execution/__tests__/file-path-resolver.test.ts +295 -0
  106. package/src/execution/file-path-resolver.ts +24 -12
  107. package/src/execution/index.ts +13 -0
  108. package/src/execution/sandbox-path-normalizer.ts +80 -0
  109. package/src/execution/useArtifactContent.ts +6 -1
  110. package/src/execution/useWorkspaceWriteBacks.ts +56 -0
  111. package/src/github/GitHubRepoPicker.tsx +413 -108
  112. package/src/github/index.ts +5 -0
  113. package/src/github/useGitHubSearch.ts +162 -0
  114. package/src/index.ts +14 -0
  115. package/src/session/index.ts +12 -0
  116. package/src/session/useSessionArtifacts.ts +143 -0
  117. package/src/session/useSessionWriteBacks.ts +94 -0
  118. package/styles.css +1 -1
@@ -9,6 +9,7 @@ import {
9
9
  type KeyboardEvent,
10
10
  } from "react";
11
11
  import { useGitHubRepos, type GitHubRepo, type GitHubBranch } from "./useGitHubRepos";
12
+ import { useGitHubSearch } from "./useGitHubSearch";
12
13
 
13
14
  export interface GitHubRepoPickerProps {
14
15
  /** GitHub access token for API calls. */
@@ -20,6 +21,8 @@ export interface GitHubRepoPickerProps {
20
21
  readonly className?: string;
21
22
  }
22
23
 
24
+ type PickerMode = "my-repos" | "all-github";
25
+
23
26
  // ---------------------------------------------------------------------------
24
27
  // Recent repos (localStorage persistence)
25
28
  // ---------------------------------------------------------------------------
@@ -52,7 +55,7 @@ function addRecentRepo(repo: RecentRepo): void {
52
55
  }
53
56
 
54
57
  // ---------------------------------------------------------------------------
55
- // Repo grouping
58
+ // Repo grouping (used in "my-repos" mode)
56
59
  // ---------------------------------------------------------------------------
57
60
 
58
61
  interface RepoGroup {
@@ -71,7 +74,6 @@ function groupRepos(
71
74
  filteredRepos.map((r) => [`${r.owner}/${r.name}`, r]),
72
75
  );
73
76
 
74
- // Recent group — only include entries that exist in the filtered set
75
77
  const recentMatched = recentEntries
76
78
  .map((r) => repoLookup.get(`${r.owner}/${r.name}`))
77
79
  .filter((r): r is GitHubRepo => r !== undefined);
@@ -85,7 +87,6 @@ function groupRepos(
85
87
  });
86
88
  }
87
89
 
88
- // Owner groups — personal (User) repos first, then orgs by repo count
89
90
  const ownerMap = new Map<
90
91
  string,
91
92
  { ownerType: "User" | "Organization"; repos: GitHubRepo[] }
@@ -136,17 +137,21 @@ function HighlightMatch({ text, query }: { text: string; query: string }) {
136
137
  // ---------------------------------------------------------------------------
137
138
 
138
139
  const LIST_ID = "stgm-repo-list";
140
+ const GITHUB_INSTALLATIONS_URL = "https://github.com/settings/installations";
139
141
 
140
142
  /**
141
143
  * Styled component for browsing and selecting a GitHub repository.
142
144
  *
143
145
  * Features:
144
- * - Owner-grouped sections (personal repos first, then orgs)
146
+ * - Two modes: "My Repos" (user's own repos) and "All GitHub" (public search)
147
+ * - Owner-grouped sections in My Repos mode
145
148
  * - Recently selected repos pinned at top
146
- * - Fixed 300px max-height with scroll shadow indicators
149
+ * - Fixed max-height with scroll shadow indicators
147
150
  * - Keyboard navigation (Arrow keys, Enter, Escape)
148
151
  * - Search with match highlighting
149
152
  * - Branch selector after repo selection
153
+ * - Manual URL entry for repos not discoverable via search
154
+ * - Link to manage GitHub App repository access
150
155
  *
151
156
  * All visual properties flow through `--stgm-*` tokens.
152
157
  */
@@ -156,17 +161,16 @@ export function GitHubRepoPicker({
156
161
  onCancel,
157
162
  className,
158
163
  }: GitHubRepoPickerProps) {
159
- const {
160
- repos,
161
- isLoading,
162
- isBackgroundLoading,
163
- error,
164
- search,
165
- setSearch,
166
- fetchBranches,
167
- } = useGitHubRepos(token);
168
-
169
- // Branch selection state
164
+ const [mode, setMode] = useState<PickerMode>("my-repos");
165
+ const [showManualEntry, setShowManualEntry] = useState(false);
166
+
167
+ // My Repos data
168
+ const myRepos = useGitHubRepos(token);
169
+
170
+ // All GitHub search data
171
+ const githubSearch = useGitHubSearch(token);
172
+
173
+ // Branch selection state (shared across modes)
170
174
  const [selectedRepo, setSelectedRepo] = useState<{
171
175
  owner: string;
172
176
  name: string;
@@ -177,6 +181,10 @@ export function GitHubRepoPicker({
177
181
  const [selectedBranch, setSelectedBranch] = useState("");
178
182
  const [loadingBranches, setLoadingBranches] = useState(false);
179
183
 
184
+ // Manual URL state
185
+ const [manualUrl, setManualUrl] = useState("");
186
+ const [manualBranch, setManualBranch] = useState("");
187
+
180
188
  // Keyboard navigation
181
189
  const [focusIndex, setFocusIndex] = useState(-1);
182
190
  const listRef = useRef<HTMLDivElement>(null);
@@ -189,13 +197,23 @@ export function GitHubRepoPicker({
189
197
  // Recent repos
190
198
  const [recentRepos, setRecentRepos] = useState<RecentRepo[]>(getRecentRepos);
191
199
 
192
- // Group and flatten repos for rendering + keyboard nav
200
+ // --- Mode-dependent derived state ---
201
+
202
+ const activeSearch = mode === "my-repos" ? myRepos.search : githubSearch.query;
203
+ const setActiveSearch = mode === "my-repos" ? myRepos.setSearch : githubSearch.setQuery;
204
+ const activeRepos = mode === "my-repos" ? myRepos.repos : githubSearch.results;
205
+ const activeError = mode === "my-repos" ? myRepos.error : githubSearch.error;
206
+ const activeIsLoading = mode === "my-repos" ? myRepos.isLoading : githubSearch.isSearching;
207
+
208
+ // Group repos only in "my-repos" mode
193
209
  const groups = useMemo(
194
- () => groupRepos(repos, recentRepos),
195
- [repos, recentRepos],
210
+ () => (mode === "my-repos" ? groupRepos(activeRepos, recentRepos) : []),
211
+ [mode, activeRepos, recentRepos],
196
212
  );
197
213
 
214
+ // Flat list for keyboard nav: grouped in my-repos, flat in all-github
198
215
  const flatItems = useMemo(() => {
216
+ if (mode === "all-github") return [...activeRepos];
199
217
  const items: GitHubRepo[] = [];
200
218
  for (const group of groups) {
201
219
  for (const repo of group.repos) {
@@ -203,12 +221,12 @@ export function GitHubRepoPicker({
203
221
  }
204
222
  }
205
223
  return items;
206
- }, [groups]);
224
+ }, [mode, activeRepos, groups]);
207
225
 
208
- // Reset focus index when search changes
226
+ // Reset focus index when search or mode changes
209
227
  useEffect(() => {
210
228
  setFocusIndex(-1);
211
- }, [search]);
229
+ }, [activeSearch, mode]);
212
230
 
213
231
  // Scroll focused item into view
214
232
  useEffect(() => {
@@ -238,10 +256,24 @@ export function GitHubRepoPicker({
238
256
  return () => el.removeEventListener("scroll", updateScrollShadows);
239
257
  }, [updateScrollShadows]);
240
258
 
241
- // Re-check shadows when repo data changes
242
259
  useEffect(() => {
243
260
  updateScrollShadows();
244
- }, [repos, updateScrollShadows]);
261
+ }, [activeRepos, updateScrollShadows]);
262
+
263
+ const handleModeSwitch = useCallback(
264
+ (newMode: PickerMode) => {
265
+ if (newMode === mode) return;
266
+ setMode(newMode);
267
+ setFocusIndex(-1);
268
+ if (newMode === "my-repos") {
269
+ githubSearch.setQuery("");
270
+ } else {
271
+ myRepos.setSearch("");
272
+ }
273
+ setTimeout(() => searchRef.current?.focus(), 0);
274
+ },
275
+ [mode, githubSearch, myRepos],
276
+ );
245
277
 
246
278
  const handleRepoClick = useCallback(
247
279
  async (repo: GitHubRepo) => {
@@ -253,11 +285,11 @@ export function GitHubRepoPicker({
253
285
  });
254
286
  setSelectedBranch(repo.defaultBranch);
255
287
  setLoadingBranches(true);
256
- const b = await fetchBranches(repo.owner, repo.name);
288
+ const b = await myRepos.fetchBranches(repo.owner, repo.name);
257
289
  setBranches(b);
258
290
  setLoadingBranches(false);
259
291
  },
260
- [fetchBranches],
292
+ [myRepos],
261
293
  );
262
294
 
263
295
  const handleAdd = useCallback(() => {
@@ -276,8 +308,15 @@ export function GitHubRepoPicker({
276
308
  }
277
309
  }, [selectedRepo, selectedBranch, onSelect]);
278
310
 
279
- // Combobox keyboard handler all keyboard interaction goes through the
280
- // search input so focus never leaves it.
311
+ const handleManualAdd = useCallback(() => {
312
+ const url = manualUrl.trim();
313
+ if (!url) return;
314
+ onSelect(url, manualBranch.trim() || "main");
315
+ setManualUrl("");
316
+ setManualBranch("");
317
+ setShowManualEntry(false);
318
+ }, [manualUrl, manualBranch, onSelect]);
319
+
281
320
  const handleSearchKeyDown = useCallback(
282
321
  (e: KeyboardEvent<HTMLInputElement>) => {
283
322
  if (e.key === "ArrowDown") {
@@ -296,19 +335,32 @@ export function GitHubRepoPicker({
296
335
  e.preventDefault();
297
336
  handleRepoClick(flatItems[focusIndex]);
298
337
  } else if (e.key === "Enter") {
299
- // Prevent form submission when no item is focused
300
338
  e.preventDefault();
301
339
  } else if (e.key === "Escape") {
302
340
  e.preventDefault();
303
- if (search) {
304
- setSearch("");
341
+ if (activeSearch) {
342
+ setActiveSearch("");
305
343
  setFocusIndex(-1);
306
344
  } else {
307
345
  onCancel?.();
308
346
  }
309
347
  }
310
348
  },
311
- [flatItems, focusIndex, handleRepoClick, onCancel, search, setSearch],
349
+ [flatItems, focusIndex, handleRepoClick, onCancel, activeSearch, setActiveSearch],
350
+ );
351
+
352
+ const handleManualKeyDown = useCallback(
353
+ (e: KeyboardEvent<HTMLInputElement>) => {
354
+ if (e.key === "Enter") {
355
+ e.preventDefault();
356
+ handleManualAdd();
357
+ } else if (e.key === "Escape") {
358
+ e.preventDefault();
359
+ setShowManualEntry(false);
360
+ setTimeout(() => searchRef.current?.focus(), 0);
361
+ }
362
+ },
363
+ [handleManualAdd],
312
364
  );
313
365
 
314
366
  // --- Branch selection view ---
@@ -321,7 +373,6 @@ export function GitHubRepoPicker({
321
373
  onClick={() => {
322
374
  setSelectedRepo(null);
323
375
  setBranches([]);
324
- // Restore focus to search
325
376
  setTimeout(() => searchRef.current?.focus(), 0);
326
377
  }}
327
378
  className="text-muted-foreground hover:text-foreground transition-colors"
@@ -371,7 +422,58 @@ export function GitHubRepoPicker({
371
422
  );
372
423
  }
373
424
 
374
- // --- Compute flat index offsets per group for rendering ---
425
+ // --- Manual URL entry view ---
426
+ if (showManualEntry) {
427
+ return (
428
+ <div className={["space-y-2", className].filter(Boolean).join(" ")}>
429
+ <div className="flex items-center gap-2 text-xs text-foreground">
430
+ <button
431
+ type="button"
432
+ onClick={() => {
433
+ setShowManualEntry(false);
434
+ setTimeout(() => searchRef.current?.focus(), 0);
435
+ }}
436
+ className="text-muted-foreground hover:text-foreground transition-colors"
437
+ aria-label="Back to repo list"
438
+ >
439
+ <ChevronLeftIcon />
440
+ </button>
441
+ <span className="font-medium">Paste a repository URL</span>
442
+ </div>
443
+
444
+ <input
445
+ type="url"
446
+ placeholder="https://github.com/org/repo"
447
+ value={manualUrl}
448
+ onChange={(e) => setManualUrl(e.target.value)}
449
+ onKeyDown={handleManualKeyDown}
450
+ className="w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
451
+ autoFocus
452
+ />
453
+ <input
454
+ type="text"
455
+ placeholder="Branch (optional, defaults to main)"
456
+ value={manualBranch}
457
+ onChange={(e) => setManualBranch(e.target.value)}
458
+ onKeyDown={handleManualKeyDown}
459
+ className="w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
460
+ />
461
+
462
+ <div className="flex justify-end">
463
+ <button
464
+ type="button"
465
+ onClick={handleManualAdd}
466
+ disabled={!manualUrl.trim()}
467
+ className="rounded-md bg-primary px-2.5 py-1 text-xs text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-40"
468
+ >
469
+ Add
470
+ </button>
471
+ </div>
472
+ </div>
473
+ );
474
+ }
475
+
476
+ // --- Compute flat index offsets per group for "my-repos" rendering ---
375
477
  let runningIndex = 0;
376
478
  const groupOffsets = groups.map((g) => {
377
479
  const offset = runningIndex;
@@ -384,7 +486,35 @@ export function GitHubRepoPicker({
384
486
  <div
385
487
  className={["space-y-1.5", className].filter(Boolean).join(" ")}
386
488
  >
387
- {/* Search input — combobox pattern: focus stays here */}
489
+ {/* Mode toggle */}
490
+ <div className="flex rounded-md border border-border bg-muted/30 p-0.5">
491
+ <button
492
+ type="button"
493
+ onClick={() => handleModeSwitch("my-repos")}
494
+ className={[
495
+ "flex-1 rounded px-2 py-1 text-[0.65rem] font-medium transition-colors",
496
+ mode === "my-repos"
497
+ ? "bg-background text-foreground shadow-sm"
498
+ : "text-muted-foreground hover:text-foreground",
499
+ ].join(" ")}
500
+ >
501
+ My Repos
502
+ </button>
503
+ <button
504
+ type="button"
505
+ onClick={() => handleModeSwitch("all-github")}
506
+ className={[
507
+ "flex-1 rounded px-2 py-1 text-[0.65rem] font-medium transition-colors",
508
+ mode === "all-github"
509
+ ? "bg-background text-foreground shadow-sm"
510
+ : "text-muted-foreground hover:text-foreground",
511
+ ].join(" ")}
512
+ >
513
+ All GitHub
514
+ </button>
515
+ </div>
516
+
517
+ {/* Search input */}
388
518
  <input
389
519
  ref={searchRef}
390
520
  type="text"
@@ -394,17 +524,21 @@ export function GitHubRepoPicker({
394
524
  aria-activedescendant={
395
525
  focusIndex >= 0 ? `stgm-repo-${focusIndex}` : undefined
396
526
  }
397
- placeholder="Search repositories..."
398
- value={search}
399
- onChange={(e) => setSearch(e.target.value)}
527
+ placeholder={
528
+ mode === "my-repos"
529
+ ? "Search repositories..."
530
+ : "Search all of GitHub..."
531
+ }
532
+ value={activeSearch}
533
+ onChange={(e) => setActiveSearch(e.target.value)}
400
534
  onKeyDown={handleSearchKeyDown}
401
535
  className="w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
402
536
  autoFocus
403
537
  />
404
538
 
405
- {error && (
539
+ {activeError && (
406
540
  <p className="text-xs text-destructive">
407
- {error}
541
+ {activeError}
408
542
  </p>
409
543
  )}
410
544
 
@@ -427,75 +561,28 @@ export function GitHubRepoPicker({
427
561
  aria-label="Repositories"
428
562
  className="max-h-64 overflow-y-auto"
429
563
  >
430
- {isLoading ? (
431
- <LoadingSkeleton />
432
- ) : flatItems.length === 0 ? (
433
- <div className="py-4 text-center text-xs text-muted-foreground">
434
- {search
435
- ? "No repos match your search"
436
- : "No repositories found"}
437
- </div>
564
+ {mode === "my-repos" ? (
565
+ <MyReposList
566
+ groups={groups}
567
+ groupOffsets={groupOffsets}
568
+ flatItems={flatItems}
569
+ focusIndex={focusIndex}
570
+ isLoading={myRepos.isLoading}
571
+ isBackgroundLoading={myRepos.isBackgroundLoading}
572
+ search={myRepos.search}
573
+ onRepoClick={handleRepoClick}
574
+ />
438
575
  ) : (
439
- <>
440
- {groups.map((group, gi) => (
441
- <div key={group.key}>
442
- <div
443
- className="sticky top-0 z-[1] bg-card/95 px-2 py-1 text-[0.65rem] font-medium text-muted-foreground backdrop-blur-sm"
444
- >
445
- {group.label}
446
- {!group.isRecent && (
447
- <span className="ml-1 opacity-50">
448
- ({group.repos.length})
449
- </span>
450
- )}
451
- </div>
452
-
453
- {group.repos.map((repo, ri) => {
454
- const flatIdx = groupOffsets[gi] + ri;
455
- return (
456
- <button
457
- key={`${group.key}-${repo.id}`}
458
- id={`stgm-repo-${flatIdx}`}
459
- type="button"
460
- data-idx={flatIdx}
461
- onClick={() => handleRepoClick(repo)}
462
- className={[
463
- "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs transition-colors",
464
- flatIdx === focusIndex
465
- ? "bg-accent text-foreground"
466
- : "text-foreground hover:bg-accent/50",
467
- ].join(" ")}
468
- role="option"
469
- aria-selected={flatIdx === focusIndex}
470
- >
471
- <span className="min-w-0 flex-1 truncate">
472
- {group.isRecent ? (
473
- <HighlightMatch
474
- text={repo.fullName}
475
- query={search}
476
- />
477
- ) : (
478
- <HighlightMatch
479
- text={repo.name}
480
- query={search}
481
- />
482
- )}
483
- </span>
484
- <span className="shrink-0 rounded px-1 py-0.5 text-[0.6rem] bg-muted text-muted-foreground">
485
- {repo.isPrivate ? "private" : "public"}
486
- </span>
487
- </button>
488
- );
489
- })}
490
- </div>
491
- ))}
492
-
493
- {isBackgroundLoading && (
494
- <div className="py-1 text-center text-[0.6rem] text-muted-foreground">
495
- Loading more...
496
- </div>
497
- )}
498
- </>
576
+ <SearchResultsList
577
+ results={githubSearch.results}
578
+ focusIndex={focusIndex}
579
+ isSearching={githubSearch.isSearching}
580
+ query={githubSearch.query}
581
+ totalCount={githubSearch.totalCount}
582
+ hasMore={githubSearch.hasMore}
583
+ onRepoClick={handleRepoClick}
584
+ onLoadMore={githubSearch.loadMore}
585
+ />
499
586
  )}
500
587
  </div>
501
588
 
@@ -509,10 +596,228 @@ export function GitHubRepoPicker({
509
596
  />
510
597
  )}
511
598
  </div>
599
+
600
+ {/* Footer: manual URL + manage access */}
601
+ <div className="flex items-center gap-3 border-t border-border pt-1.5 text-[0.65rem] text-muted-foreground">
602
+ <button
603
+ type="button"
604
+ onClick={() => setShowManualEntry(true)}
605
+ className="hover:text-foreground transition-colors"
606
+ >
607
+ Paste a URL
608
+ </button>
609
+ <span className="opacity-30">·</span>
610
+ <a
611
+ href={GITHUB_INSTALLATIONS_URL}
612
+ target="_blank"
613
+ rel="noopener noreferrer"
614
+ className="hover:text-foreground transition-colors"
615
+ >
616
+ Manage access
617
+ </a>
618
+ </div>
512
619
  </div>
513
620
  );
514
621
  }
515
622
 
623
+ // ---------------------------------------------------------------------------
624
+ // My Repos list (grouped)
625
+ // ---------------------------------------------------------------------------
626
+
627
+ function MyReposList({
628
+ groups,
629
+ groupOffsets,
630
+ flatItems,
631
+ focusIndex,
632
+ isLoading,
633
+ isBackgroundLoading,
634
+ search,
635
+ onRepoClick,
636
+ }: {
637
+ groups: RepoGroup[];
638
+ groupOffsets: number[];
639
+ flatItems: GitHubRepo[];
640
+ focusIndex: number;
641
+ isLoading: boolean;
642
+ isBackgroundLoading: boolean;
643
+ search: string;
644
+ onRepoClick: (repo: GitHubRepo) => void;
645
+ }) {
646
+ if (isLoading) return <LoadingSkeleton />;
647
+
648
+ if (flatItems.length === 0) {
649
+ return (
650
+ <div className="py-4 text-center text-xs text-muted-foreground">
651
+ {search ? "No repos match your search" : "No repositories found"}
652
+ </div>
653
+ );
654
+ }
655
+
656
+ return (
657
+ <>
658
+ {groups.map((group, gi) => (
659
+ <div key={group.key}>
660
+ <div className="sticky top-0 z-[1] bg-card/95 px-2 py-1 text-[0.65rem] font-medium text-muted-foreground backdrop-blur-sm">
661
+ {group.label}
662
+ {!group.isRecent && (
663
+ <span className="ml-1 opacity-50">
664
+ ({group.repos.length})
665
+ </span>
666
+ )}
667
+ </div>
668
+
669
+ {group.repos.map((repo, ri) => {
670
+ const flatIdx = groupOffsets[gi] + ri;
671
+ return (
672
+ <RepoRow
673
+ key={`${group.key}-${repo.id}`}
674
+ repo={repo}
675
+ flatIdx={flatIdx}
676
+ focusIndex={focusIndex}
677
+ displayName={group.isRecent ? repo.fullName : repo.name}
678
+ query={search}
679
+ onClick={onRepoClick}
680
+ />
681
+ );
682
+ })}
683
+ </div>
684
+ ))}
685
+
686
+ {isBackgroundLoading && (
687
+ <div className="py-1 text-center text-[0.6rem] text-muted-foreground">
688
+ Loading more...
689
+ </div>
690
+ )}
691
+ </>
692
+ );
693
+ }
694
+
695
+ // ---------------------------------------------------------------------------
696
+ // All GitHub search results list (flat)
697
+ // ---------------------------------------------------------------------------
698
+
699
+ function SearchResultsList({
700
+ results,
701
+ focusIndex,
702
+ isSearching,
703
+ query,
704
+ totalCount,
705
+ hasMore,
706
+ onRepoClick,
707
+ onLoadMore,
708
+ }: {
709
+ results: readonly GitHubRepo[];
710
+ focusIndex: number;
711
+ isSearching: boolean;
712
+ query: string;
713
+ totalCount: number;
714
+ hasMore: boolean;
715
+ onRepoClick: (repo: GitHubRepo) => void;
716
+ onLoadMore: () => void;
717
+ }) {
718
+ if (!query) {
719
+ return (
720
+ <div className="py-6 text-center text-xs text-muted-foreground">
721
+ Type to search all of GitHub
722
+ </div>
723
+ );
724
+ }
725
+
726
+ if (isSearching && results.length === 0) {
727
+ return <LoadingSkeleton />;
728
+ }
729
+
730
+ if (results.length === 0) {
731
+ return (
732
+ <div className="py-4 text-center text-xs text-muted-foreground">
733
+ No repositories found
734
+ </div>
735
+ );
736
+ }
737
+
738
+ return (
739
+ <>
740
+ {totalCount > 0 && (
741
+ <div className="px-2 py-1 text-[0.6rem] text-muted-foreground">
742
+ {totalCount.toLocaleString()} {totalCount === 1 ? "result" : "results"}
743
+ </div>
744
+ )}
745
+
746
+ {results.map((repo, i) => (
747
+ <RepoRow
748
+ key={repo.id}
749
+ repo={repo}
750
+ flatIdx={i}
751
+ focusIndex={focusIndex}
752
+ displayName={repo.fullName}
753
+ query={query}
754
+ onClick={onRepoClick}
755
+ />
756
+ ))}
757
+
758
+ {isSearching && (
759
+ <div className="py-1 text-center text-[0.6rem] text-muted-foreground">
760
+ Searching...
761
+ </div>
762
+ )}
763
+
764
+ {hasMore && !isSearching && (
765
+ <button
766
+ type="button"
767
+ onClick={onLoadMore}
768
+ className="w-full py-1.5 text-center text-[0.65rem] text-muted-foreground hover:text-foreground transition-colors"
769
+ >
770
+ Load more results
771
+ </button>
772
+ )}
773
+ </>
774
+ );
775
+ }
776
+
777
+ // ---------------------------------------------------------------------------
778
+ // Shared repo row
779
+ // ---------------------------------------------------------------------------
780
+
781
+ function RepoRow({
782
+ repo,
783
+ flatIdx,
784
+ focusIndex,
785
+ displayName,
786
+ query,
787
+ onClick,
788
+ }: {
789
+ repo: GitHubRepo;
790
+ flatIdx: number;
791
+ focusIndex: number;
792
+ displayName: string;
793
+ query: string;
794
+ onClick: (repo: GitHubRepo) => void;
795
+ }) {
796
+ return (
797
+ <button
798
+ id={`stgm-repo-${flatIdx}`}
799
+ type="button"
800
+ data-idx={flatIdx}
801
+ onClick={() => onClick(repo)}
802
+ className={[
803
+ "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs transition-colors",
804
+ flatIdx === focusIndex
805
+ ? "bg-accent text-foreground"
806
+ : "text-foreground hover:bg-accent/50",
807
+ ].join(" ")}
808
+ role="option"
809
+ aria-selected={flatIdx === focusIndex}
810
+ >
811
+ <span className="min-w-0 flex-1 truncate">
812
+ <HighlightMatch text={displayName} query={query} />
813
+ </span>
814
+ <span className="shrink-0 rounded px-1 py-0.5 text-[0.6rem] bg-muted text-muted-foreground">
815
+ {repo.isPrivate ? "private" : "public"}
816
+ </span>
817
+ </button>
818
+ );
819
+ }
820
+
516
821
  // ---------------------------------------------------------------------------
517
822
  // Loading skeleton
518
823
  // ---------------------------------------------------------------------------