kajji 0.1.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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +128 -0
  3. package/bin/kajji.js +2 -0
  4. package/package.json +56 -0
  5. package/src/App.tsx +229 -0
  6. package/src/commander/bookmarks.ts +129 -0
  7. package/src/commander/diff.ts +186 -0
  8. package/src/commander/executor.ts +285 -0
  9. package/src/commander/files.ts +87 -0
  10. package/src/commander/log.ts +99 -0
  11. package/src/commander/operations.ts +313 -0
  12. package/src/commander/types.ts +21 -0
  13. package/src/components/AnsiText.tsx +77 -0
  14. package/src/components/BorderBox.tsx +124 -0
  15. package/src/components/FileTreeList.tsx +105 -0
  16. package/src/components/Layout.tsx +48 -0
  17. package/src/components/Panel.tsx +143 -0
  18. package/src/components/RevisionPicker.tsx +165 -0
  19. package/src/components/StatusBar.tsx +158 -0
  20. package/src/components/modals/BookmarkNameModal.tsx +170 -0
  21. package/src/components/modals/DescribeModal.tsx +124 -0
  22. package/src/components/modals/HelpModal.tsx +372 -0
  23. package/src/components/modals/RevisionPickerModal.tsx +70 -0
  24. package/src/components/modals/UndoModal.tsx +75 -0
  25. package/src/components/panels/BookmarksPanel.tsx +768 -0
  26. package/src/components/panels/CommandLogPanel.tsx +40 -0
  27. package/src/components/panels/LogPanel.tsx +774 -0
  28. package/src/components/panels/MainArea.tsx +354 -0
  29. package/src/context/command.tsx +106 -0
  30. package/src/context/commandlog.tsx +45 -0
  31. package/src/context/dialog.tsx +217 -0
  32. package/src/context/focus.tsx +63 -0
  33. package/src/context/helper.tsx +24 -0
  34. package/src/context/keybind.tsx +51 -0
  35. package/src/context/loading.tsx +68 -0
  36. package/src/context/sync.tsx +868 -0
  37. package/src/context/theme.tsx +90 -0
  38. package/src/context/types.ts +51 -0
  39. package/src/index.tsx +15 -0
  40. package/src/keybind/index.ts +2 -0
  41. package/src/keybind/parser.ts +88 -0
  42. package/src/keybind/types.ts +83 -0
  43. package/src/theme/index.ts +3 -0
  44. package/src/theme/presets/lazygit.ts +45 -0
  45. package/src/theme/presets/opencode.ts +45 -0
  46. package/src/theme/types.ts +47 -0
  47. package/src/utils/double-click.ts +59 -0
  48. package/src/utils/file-tree.ts +154 -0
@@ -0,0 +1,768 @@
1
+ import type { ScrollBoxRenderable } from "@opentui/core"
2
+ import { For, Match, Show, Switch, createEffect, createSignal } from "solid-js"
3
+ import {
4
+ jjBookmarkCreate,
5
+ jjBookmarkDelete,
6
+ jjBookmarkForget,
7
+ jjBookmarkRename,
8
+ jjBookmarkSet,
9
+ } from "../../commander/bookmarks"
10
+ import {
11
+ type OperationResult,
12
+ isImmutableError,
13
+ jjAbandon,
14
+ jjDescribe,
15
+ jjEdit,
16
+ jjNew,
17
+ jjRestore,
18
+ jjShowDescription,
19
+ jjSquash,
20
+ } from "../../commander/operations"
21
+ import { useCommand } from "../../context/command"
22
+ import { useCommandLog } from "../../context/commandlog"
23
+ import { useDialog } from "../../context/dialog"
24
+ import { useFocus } from "../../context/focus"
25
+ import { useLoading } from "../../context/loading"
26
+ import { useSync } from "../../context/sync"
27
+ import { useTheme } from "../../context/theme"
28
+ import { createDoubleClickDetector } from "../../utils/double-click"
29
+ import { FileTreeList } from "../FileTreeList"
30
+ import { Panel } from "../Panel"
31
+ import { BookmarkNameModal } from "../modals/BookmarkNameModal"
32
+ import { DescribeModal } from "../modals/DescribeModal"
33
+ import { RevisionPickerModal } from "../modals/RevisionPickerModal"
34
+
35
+ export function BookmarksPanel() {
36
+ const {
37
+ commits,
38
+ bookmarks,
39
+ selectedBookmarkIndex,
40
+ setSelectedBookmarkIndex,
41
+ selectedBookmark,
42
+ bookmarksLoading,
43
+ bookmarksError,
44
+ selectNextBookmark,
45
+ selectPrevBookmark,
46
+ bookmarkViewMode,
47
+ bookmarkCommits,
48
+ selectedBookmarkCommitIndex,
49
+ setSelectedBookmarkCommitIndex,
50
+ bookmarkCommitsLoading,
51
+ bookmarkFlatFiles,
52
+ selectedBookmarkFileIndex,
53
+ setSelectedBookmarkFileIndex,
54
+ bookmarkFilesLoading,
55
+ bookmarkCollapsedPaths,
56
+ activeBookmarkName,
57
+ selectedBookmarkCommit,
58
+ enterBookmarkCommitsView,
59
+ enterBookmarkFilesView,
60
+ exitBookmarkView,
61
+ selectPrevBookmarkCommit,
62
+ selectNextBookmarkCommit,
63
+ selectPrevBookmarkFile,
64
+ selectNextBookmarkFile,
65
+ toggleBookmarkFolder,
66
+ } = useSync()
67
+ const focus = useFocus()
68
+ const command = useCommand()
69
+ const commandLog = useCommandLog()
70
+ const dialog = useDialog()
71
+ const globalLoading = useLoading()
72
+ const { colors } = useTheme()
73
+ const { refresh } = useSync()
74
+
75
+ const runOperation = async (
76
+ text: string,
77
+ op: () => Promise<OperationResult>,
78
+ ) => {
79
+ const result = await globalLoading.run(text, op)
80
+ commandLog.addEntry(result)
81
+ if (result.success) {
82
+ refresh()
83
+ }
84
+ }
85
+
86
+ const isFocused = () => focus.isPanel("refs")
87
+ const localBookmarks = () => bookmarks().filter((b) => b.isLocal)
88
+
89
+ let listScrollRef: ScrollBoxRenderable | undefined
90
+ let commitsScrollRef: ScrollBoxRenderable | undefined
91
+ let filesScrollRef: ScrollBoxRenderable | undefined
92
+
93
+ const [listScrollTop, setListScrollTop] = createSignal(0)
94
+ const [commitsScrollTop, setCommitsScrollTop] = createSignal(0)
95
+ const [filesScrollTop, setFilesScrollTop] = createSignal(0)
96
+
97
+ const scrollIntoView = (
98
+ ref: ScrollBoxRenderable | undefined,
99
+ index: number,
100
+ scrollTop: number,
101
+ setScrollTop: (n: number) => void,
102
+ listLength: number,
103
+ ) => {
104
+ if (!ref || listLength === 0) return
105
+ const margin = 2
106
+ const refAny = ref as unknown as Record<string, unknown>
107
+ const viewportHeight =
108
+ (typeof refAny.height === "number" ? refAny.height : null) ??
109
+ (typeof refAny.rows === "number" ? refAny.rows : null) ??
110
+ 10
111
+
112
+ const visibleStart = scrollTop
113
+ const visibleEnd = scrollTop + viewportHeight - 1
114
+ const safeStart = visibleStart + margin
115
+ const safeEnd = visibleEnd - margin
116
+
117
+ let newScrollTop = scrollTop
118
+ if (index < safeStart) {
119
+ newScrollTop = Math.max(0, index - margin)
120
+ } else if (index > safeEnd) {
121
+ newScrollTop = Math.max(0, index - viewportHeight + margin + 1)
122
+ }
123
+
124
+ if (newScrollTop !== scrollTop) {
125
+ ref.scrollTo(newScrollTop)
126
+ setScrollTop(newScrollTop)
127
+ }
128
+ }
129
+
130
+ createEffect(() => {
131
+ const index = selectedBookmarkIndex()
132
+ scrollIntoView(
133
+ listScrollRef,
134
+ index,
135
+ listScrollTop(),
136
+ setListScrollTop,
137
+ localBookmarks().length,
138
+ )
139
+ })
140
+
141
+ createEffect(() => {
142
+ const index = selectedBookmarkCommitIndex()
143
+ scrollIntoView(
144
+ commitsScrollRef,
145
+ index,
146
+ commitsScrollTop(),
147
+ setCommitsScrollTop,
148
+ bookmarkCommits().length,
149
+ )
150
+ })
151
+
152
+ createEffect(() => {
153
+ const index = selectedBookmarkFileIndex()
154
+ scrollIntoView(
155
+ filesScrollRef,
156
+ index,
157
+ filesScrollTop(),
158
+ setFilesScrollTop,
159
+ bookmarkFlatFiles().length,
160
+ )
161
+ })
162
+
163
+ const title = () => {
164
+ const mode = bookmarkViewMode()
165
+ if (mode === "files") {
166
+ const commit = selectedBookmarkCommit()
167
+ return commit ? `Files (${commit.changeId.slice(0, 8)})` : "Files"
168
+ }
169
+ if (mode === "commits") {
170
+ return `Revisions (${activeBookmarkName()})`
171
+ }
172
+ return "Bookmarks"
173
+ }
174
+
175
+ const handleListEnter = () => {
176
+ enterBookmarkCommitsView()
177
+ }
178
+
179
+ const handleCommitsEnter = () => {
180
+ enterBookmarkFilesView()
181
+ }
182
+
183
+ const handleFilesEnter = () => {
184
+ const file = bookmarkFlatFiles()[selectedBookmarkFileIndex()]
185
+ if (file?.node.isDirectory) {
186
+ toggleBookmarkFolder(file.node.path)
187
+ }
188
+ }
189
+
190
+ command.register(() => {
191
+ const mode = bookmarkViewMode()
192
+
193
+ if (mode === "files") {
194
+ return [
195
+ {
196
+ id: "refs.files.next",
197
+ title: "next file",
198
+ keybind: "nav_down",
199
+ context: "refs.files",
200
+ type: "navigation",
201
+ panel: "refs",
202
+ visibility: "help-only",
203
+ onSelect: selectNextBookmarkFile,
204
+ },
205
+ {
206
+ id: "refs.files.prev",
207
+ title: "previous file",
208
+ keybind: "nav_up",
209
+ context: "refs.files",
210
+ type: "navigation",
211
+ panel: "refs",
212
+ visibility: "help-only",
213
+ onSelect: selectPrevBookmarkFile,
214
+ },
215
+ {
216
+ id: "refs.files.toggle",
217
+ title: "toggle folder",
218
+ keybind: "enter",
219
+ context: "refs.files",
220
+ type: "action",
221
+ panel: "refs",
222
+ visibility: "help-only",
223
+ onSelect: handleFilesEnter,
224
+ },
225
+ {
226
+ id: "refs.files.back",
227
+ title: "back",
228
+ keybind: "escape",
229
+ context: "refs.files",
230
+ type: "view",
231
+ panel: "refs",
232
+ visibility: "help-only",
233
+ onSelect: exitBookmarkView,
234
+ },
235
+ {
236
+ id: "refs.files.restore",
237
+ title: "restore",
238
+ keybind: "jj_restore",
239
+ context: "refs.files",
240
+ type: "action",
241
+ panel: "refs",
242
+ onSelect: async () => {
243
+ const file = bookmarkFlatFiles()[selectedBookmarkFileIndex()]
244
+ if (!file) return
245
+ const node = file.node
246
+ const pathType = node.isDirectory ? "folder" : "file"
247
+ const confirmed = await dialog.confirm({
248
+ message: `Restore ${pathType} "${node.path}"? This will discard changes.`,
249
+ })
250
+ if (confirmed) {
251
+ await runOperation("Restoring...", () => jjRestore([node.path]))
252
+ }
253
+ },
254
+ },
255
+ ]
256
+ }
257
+
258
+ if (mode === "commits") {
259
+ return [
260
+ {
261
+ id: "refs.revisions.next",
262
+ title: "down",
263
+ keybind: "nav_down",
264
+ context: "refs.revisions",
265
+ type: "navigation",
266
+ panel: "refs",
267
+ visibility: "help-only",
268
+ onSelect: selectNextBookmarkCommit,
269
+ },
270
+ {
271
+ id: "refs.revisions.prev",
272
+ title: "up",
273
+ keybind: "nav_up",
274
+ context: "refs.revisions",
275
+ type: "navigation",
276
+ panel: "refs",
277
+ visibility: "help-only",
278
+ onSelect: selectPrevBookmarkCommit,
279
+ },
280
+ {
281
+ id: "refs.revisions.view_files",
282
+ title: "view files",
283
+ keybind: "enter",
284
+ context: "refs.revisions",
285
+ type: "view",
286
+ panel: "refs",
287
+ visibility: "help-only",
288
+ onSelect: handleCommitsEnter,
289
+ },
290
+ {
291
+ id: "refs.revisions.back",
292
+ title: "back",
293
+ keybind: "escape",
294
+ context: "refs.revisions",
295
+ type: "view",
296
+ panel: "refs",
297
+ visibility: "help-only",
298
+ onSelect: exitBookmarkView,
299
+ },
300
+ {
301
+ id: "refs.revisions.new",
302
+ title: "new",
303
+ keybind: "jj_new",
304
+ context: "refs.revisions",
305
+ type: "action",
306
+ panel: "refs",
307
+ onSelect: () => {
308
+ const commit = selectedBookmarkCommit()
309
+ if (commit)
310
+ runOperation("Creating...", () => jjNew(commit.changeId))
311
+ },
312
+ },
313
+ {
314
+ id: "refs.revisions.edit",
315
+ title: "edit",
316
+ keybind: "jj_edit",
317
+ context: "refs.revisions",
318
+ type: "action",
319
+ panel: "refs",
320
+ onSelect: () => {
321
+ const commit = selectedBookmarkCommit()
322
+ if (commit)
323
+ runOperation("Editing...", () => jjEdit(commit.changeId))
324
+ },
325
+ },
326
+ {
327
+ id: "refs.revisions.squash",
328
+ title: "squash",
329
+ keybind: "jj_squash",
330
+ context: "refs.revisions",
331
+ type: "action",
332
+ panel: "refs",
333
+ onSelect: async () => {
334
+ const commit = selectedBookmarkCommit()
335
+ if (!commit) return
336
+ const result = await jjSquash(commit.changeId)
337
+ if (isImmutableError(result)) {
338
+ const confirmed = await dialog.confirm({
339
+ message: "Parent is immutable. Squash anyway?",
340
+ })
341
+ if (confirmed) {
342
+ await runOperation("Squashing...", () =>
343
+ jjSquash(commit.changeId, { ignoreImmutable: true }),
344
+ )
345
+ }
346
+ } else {
347
+ commandLog.addEntry(result)
348
+ if (result.success) {
349
+ refresh()
350
+ }
351
+ }
352
+ },
353
+ },
354
+ {
355
+ id: "refs.revisions.describe",
356
+ title: "describe",
357
+ keybind: "jj_describe",
358
+ context: "refs.revisions",
359
+ type: "action",
360
+ panel: "refs",
361
+ onSelect: async () => {
362
+ const commit = selectedBookmarkCommit()
363
+ if (!commit) return
364
+
365
+ let ignoreImmutable = false
366
+ if (commit.immutable) {
367
+ const confirmed = await dialog.confirm({
368
+ message: "Commit is immutable. Describe anyway?",
369
+ })
370
+ if (!confirmed) return
371
+ ignoreImmutable = true
372
+ }
373
+
374
+ const desc = await jjShowDescription(commit.changeId)
375
+ dialog.open(
376
+ () => (
377
+ <DescribeModal
378
+ initialSubject={desc.subject}
379
+ initialBody={desc.body}
380
+ onSave={(subject, body) => {
381
+ const message = body ? `${subject}\n\n${body}` : subject
382
+ runOperation("Describing...", () =>
383
+ jjDescribe(commit.changeId, message, { ignoreImmutable }),
384
+ )
385
+ }}
386
+ />
387
+ ),
388
+ {
389
+ id: "describe",
390
+ hints: [
391
+ { key: "tab", label: "switch field" },
392
+ { key: "enter", label: "save" },
393
+ ],
394
+ },
395
+ )
396
+ },
397
+ },
398
+ {
399
+ id: "refs.revisions.abandon",
400
+ title: "abandon",
401
+ keybind: "jj_abandon",
402
+ context: "refs.revisions",
403
+ type: "action",
404
+ panel: "refs",
405
+ onSelect: async () => {
406
+ const commit = selectedBookmarkCommit()
407
+ if (!commit) return
408
+ const confirmed = await dialog.confirm({
409
+ message: `Abandon change ${commit.changeId.slice(0, 8)}?`,
410
+ })
411
+ if (confirmed) {
412
+ await runOperation("Abandoning...", () =>
413
+ jjAbandon(commit.changeId),
414
+ )
415
+ }
416
+ },
417
+ },
418
+ {
419
+ id: "refs.revisions.bookmark",
420
+ title: "create bookmark",
421
+ keybind: "bookmark_set",
422
+ context: "refs.revisions",
423
+ type: "action",
424
+ panel: "refs",
425
+ onSelect: () => {
426
+ const commit = selectedBookmarkCommit()
427
+ if (!commit) return
428
+ dialog.open(
429
+ () => (
430
+ <BookmarkNameModal
431
+ title="Create Bookmark"
432
+ commits={bookmarkCommits()}
433
+ defaultRevision={commit.changeId}
434
+ onSave={(name, revision) => {
435
+ runOperation("Creating bookmark...", () =>
436
+ jjBookmarkCreate(name, { revision }),
437
+ )
438
+ }}
439
+ />
440
+ ),
441
+ {
442
+ id: "bookmark-create",
443
+ hints: [
444
+ { key: "tab", label: "switch field" },
445
+ { key: "enter", label: "save" },
446
+ ],
447
+ },
448
+ )
449
+ },
450
+ },
451
+ ]
452
+ }
453
+
454
+ return [
455
+ {
456
+ id: "refs.bookmarks.next",
457
+ title: "down",
458
+ keybind: "nav_down",
459
+ context: "refs.bookmarks",
460
+ type: "navigation",
461
+ panel: "refs",
462
+ visibility: "help-only",
463
+ onSelect: selectNextBookmark,
464
+ },
465
+ {
466
+ id: "refs.bookmarks.prev",
467
+ title: "up",
468
+ keybind: "nav_up",
469
+ context: "refs.bookmarks",
470
+ type: "navigation",
471
+ panel: "refs",
472
+ visibility: "help-only",
473
+ onSelect: selectPrevBookmark,
474
+ },
475
+ {
476
+ id: "refs.bookmarks.view_revisions",
477
+ title: "view revisions",
478
+ keybind: "enter",
479
+ context: "refs.bookmarks",
480
+ type: "view",
481
+ panel: "refs",
482
+ visibility: "help-only",
483
+ onSelect: handleListEnter,
484
+ },
485
+ {
486
+ id: "refs.bookmarks.create",
487
+ title: "create",
488
+ keybind: "bookmark_create",
489
+ context: "refs.bookmarks",
490
+ type: "action",
491
+ panel: "refs",
492
+ onSelect: () => {
493
+ const workingCopy = commits().find((c) => c.isWorkingCopy)
494
+ dialog.open(
495
+ () => (
496
+ <BookmarkNameModal
497
+ title="Create Bookmark"
498
+ commits={commits()}
499
+ defaultRevision={workingCopy?.changeId}
500
+ onSave={(name, revision) => {
501
+ runOperation("Creating bookmark...", () =>
502
+ jjBookmarkCreate(name, { revision }),
503
+ )
504
+ }}
505
+ />
506
+ ),
507
+ {
508
+ id: "bookmark-create",
509
+ hints: [
510
+ { key: "tab", label: "switch field" },
511
+ { key: "enter", label: "save" },
512
+ ],
513
+ },
514
+ )
515
+ },
516
+ },
517
+ {
518
+ id: "refs.bookmarks.delete",
519
+ title: "delete",
520
+ keybind: "bookmark_delete",
521
+ context: "refs.bookmarks",
522
+ type: "action",
523
+ panel: "refs",
524
+ onSelect: async () => {
525
+ const bookmark = selectedBookmark()
526
+ if (!bookmark) return
527
+ const confirmed = await dialog.confirm({
528
+ message: `Delete bookmark "${bookmark.name}"?`,
529
+ })
530
+ if (confirmed) {
531
+ await runOperation("Deleting bookmark...", () =>
532
+ jjBookmarkDelete(bookmark.name),
533
+ )
534
+ }
535
+ },
536
+ },
537
+ {
538
+ id: "refs.bookmarks.rename",
539
+ title: "rename",
540
+ keybind: "bookmark_rename",
541
+ context: "refs.bookmarks",
542
+ type: "action",
543
+ panel: "refs",
544
+ onSelect: () => {
545
+ const bookmark = selectedBookmark()
546
+ if (!bookmark) return
547
+ dialog.open(
548
+ () => (
549
+ <BookmarkNameModal
550
+ title="Rename Bookmark"
551
+ initialValue={bookmark.name}
552
+ onSave={(newName) => {
553
+ runOperation("Renaming bookmark...", () =>
554
+ jjBookmarkRename(bookmark.name, newName),
555
+ )
556
+ }}
557
+ />
558
+ ),
559
+ {
560
+ id: "bookmark-rename",
561
+ hints: [{ key: "enter", label: "save" }],
562
+ },
563
+ )
564
+ },
565
+ },
566
+ {
567
+ id: "refs.bookmarks.forget",
568
+ title: "forget",
569
+ keybind: "bookmark_forget",
570
+ context: "refs.bookmarks",
571
+ type: "action",
572
+ panel: "refs",
573
+ onSelect: async () => {
574
+ const bookmark = selectedBookmark()
575
+ if (!bookmark) return
576
+ const confirmed = await dialog.confirm({
577
+ message: `Forget bookmark "${bookmark.name}"? (local only)`,
578
+ })
579
+ if (confirmed) {
580
+ await runOperation("Forgetting bookmark...", () =>
581
+ jjBookmarkForget(bookmark.name),
582
+ )
583
+ }
584
+ },
585
+ },
586
+ {
587
+ id: "refs.bookmarks.move",
588
+ title: "move",
589
+ keybind: "bookmark_move",
590
+ context: "refs.bookmarks",
591
+ type: "action",
592
+ panel: "refs",
593
+ onSelect: () => {
594
+ const bookmark = selectedBookmark()
595
+ if (!bookmark) return
596
+ dialog.open(
597
+ () => (
598
+ <RevisionPickerModal
599
+ title={`Move "${bookmark.name}" to`}
600
+ commits={commits()}
601
+ defaultRevision={bookmark.changeId}
602
+ onSelect={(revision) => {
603
+ runOperation("Moving bookmark...", () =>
604
+ jjBookmarkSet(bookmark.name, revision),
605
+ )
606
+ }}
607
+ />
608
+ ),
609
+ {
610
+ id: "bookmark-move",
611
+ hints: [{ key: "enter", label: "confirm" }],
612
+ },
613
+ )
614
+ },
615
+ },
616
+ ]
617
+ })
618
+
619
+ return (
620
+ <Panel title={title()} hotkey="2" panelId="refs" focused={isFocused()}>
621
+ <Switch>
622
+ <Match when={bookmarkViewMode() === "list"}>
623
+ <Show when={bookmarksLoading() && localBookmarks().length === 0}>
624
+ <text fg={colors().textMuted}>Loading bookmarks...</text>
625
+ </Show>
626
+ <Show when={bookmarksError() && localBookmarks().length === 0}>
627
+ <text fg={colors().error}>Error: {bookmarksError()}</text>
628
+ </Show>
629
+ <Show
630
+ when={localBookmarks().length > 0}
631
+ fallback={
632
+ !bookmarksLoading() && !bookmarksError() ? (
633
+ <text fg={colors().textMuted}>No bookmarks</text>
634
+ ) : null
635
+ }
636
+ >
637
+ <scrollbox
638
+ ref={listScrollRef}
639
+ flexGrow={1}
640
+ scrollbarOptions={{ visible: false }}
641
+ >
642
+ <For each={localBookmarks()}>
643
+ {(bookmark, index) => {
644
+ const isSelected = () => index() === selectedBookmarkIndex()
645
+ const handleDoubleClick = createDoubleClickDetector(() => {
646
+ enterBookmarkCommitsView()
647
+ })
648
+ const handleMouseDown = () => {
649
+ setSelectedBookmarkIndex(index())
650
+ handleDoubleClick()
651
+ }
652
+ return (
653
+ <box
654
+ backgroundColor={
655
+ isSelected() ? colors().selectionBackground : undefined
656
+ }
657
+ overflow="hidden"
658
+ onMouseDown={handleMouseDown}
659
+ >
660
+ <text>
661
+ <span style={{ fg: colors().primary }}>
662
+ {bookmark.name}
663
+ </span>
664
+ <span style={{ fg: colors().textMuted }}>
665
+ {" "}
666
+ {bookmark.changeId.slice(0, 8)}
667
+ </span>
668
+ </text>
669
+ </box>
670
+ )
671
+ }}
672
+ </For>
673
+ </scrollbox>
674
+ </Show>
675
+ </Match>
676
+
677
+ <Match when={bookmarkViewMode() === "commits"}>
678
+ <Show when={bookmarkCommitsLoading()}>
679
+ <text fg={colors().textMuted}>Loading commits...</text>
680
+ </Show>
681
+ <Show when={!bookmarkCommitsLoading()}>
682
+ <Show
683
+ when={bookmarkCommits().length > 0}
684
+ fallback={<text fg={colors().textMuted}>No commits</text>}
685
+ >
686
+ <scrollbox
687
+ ref={commitsScrollRef}
688
+ flexGrow={1}
689
+ scrollbarOptions={{ visible: false }}
690
+ >
691
+ <For each={bookmarkCommits()}>
692
+ {(commit, index) => {
693
+ const isSelected = () =>
694
+ index() === selectedBookmarkCommitIndex()
695
+ const icon = commit.isWorkingCopy ? "◆" : "○"
696
+ const handleDoubleClick = createDoubleClickDetector(() => {
697
+ enterBookmarkFilesView()
698
+ })
699
+ const handleMouseDown = () => {
700
+ setSelectedBookmarkCommitIndex(index())
701
+ handleDoubleClick()
702
+ }
703
+ return (
704
+ <box
705
+ backgroundColor={
706
+ isSelected()
707
+ ? colors().selectionBackground
708
+ : undefined
709
+ }
710
+ overflow="hidden"
711
+ onMouseDown={handleMouseDown}
712
+ >
713
+ <text wrapMode="none">
714
+ <span
715
+ style={{
716
+ fg: commit.isWorkingCopy
717
+ ? colors().primary
718
+ : colors().textMuted,
719
+ }}
720
+ >
721
+ {icon}{" "}
722
+ </span>
723
+ <span style={{ fg: colors().warning }}>
724
+ {commit.changeId.slice(0, 8)}
725
+ </span>
726
+ <span style={{ fg: colors().text }}>
727
+ {" "}
728
+ {commit.description}
729
+ </span>
730
+ </text>
731
+ </box>
732
+ )
733
+ }}
734
+ </For>
735
+ </scrollbox>
736
+ </Show>
737
+ </Show>
738
+ </Match>
739
+
740
+ <Match when={bookmarkViewMode() === "files"}>
741
+ <Show when={bookmarkFilesLoading()}>
742
+ <text fg={colors().textMuted}>Loading files...</text>
743
+ </Show>
744
+ <Show when={!bookmarkFilesLoading()}>
745
+ <Show
746
+ when={bookmarkFlatFiles().length > 0}
747
+ fallback={<text fg={colors().textMuted}>No files</text>}
748
+ >
749
+ <scrollbox
750
+ ref={filesScrollRef}
751
+ flexGrow={1}
752
+ scrollbarOptions={{ visible: false }}
753
+ >
754
+ <FileTreeList
755
+ files={bookmarkFlatFiles}
756
+ selectedIndex={selectedBookmarkFileIndex}
757
+ setSelectedIndex={setSelectedBookmarkFileIndex}
758
+ collapsedPaths={bookmarkCollapsedPaths}
759
+ toggleFolder={toggleBookmarkFolder}
760
+ />
761
+ </scrollbox>
762
+ </Show>
763
+ </Show>
764
+ </Match>
765
+ </Switch>
766
+ </Panel>
767
+ )
768
+ }