@brimveyn/aimux 1.5.4 → 1.6.1

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 (67) hide show
  1. package/README.md +14 -2
  2. package/package.json +5 -3
  3. package/src/app-runtime/side-effects.ts +41 -6
  4. package/src/app.tsx +17 -5
  5. package/src/config.ts +29 -4
  6. package/src/diff-parser/clean-last-newline.ts +5 -0
  7. package/src/diff-parser/constants.ts +13 -0
  8. package/src/diff-parser/index.ts +12 -0
  9. package/src/diff-parser/parse-line-type.ts +29 -0
  10. package/src/diff-parser/parse-patch-files.ts +369 -0
  11. package/src/diff-parser/types.ts +75 -0
  12. package/src/git/git-diff.ts +24 -12
  13. package/src/git/git-poller.ts +11 -3
  14. package/src/git/git-status.ts +98 -17
  15. package/src/input/modes/bridge.ts +8 -0
  16. package/src/input/modes/transitions.ts +2 -1
  17. package/src/input/modes/types.ts +4 -1
  18. package/src/pty/terminal-snapshot.ts +5 -5
  19. package/src/state/git-tree.ts +220 -0
  20. package/src/state/reducers/git-mode-state.ts +276 -36
  21. package/src/state/reducers/git-panel-state.ts +35 -4
  22. package/src/state/reducers/modal-state.ts +43 -10
  23. package/src/state/store.ts +5 -1
  24. package/src/state/types.ts +46 -6
  25. package/src/state/workspace-save.ts +2 -0
  26. package/src/ui/components/create-session-modal.tsx +25 -8
  27. package/src/ui/components/diff-renderer/build-rows.ts +349 -0
  28. package/src/ui/components/diff-renderer/filetype.ts +29 -0
  29. package/src/ui/components/diff-renderer/fold-strip.tsx +86 -0
  30. package/src/ui/components/diff-renderer/highlight.ts +48 -0
  31. package/src/ui/components/diff-renderer/index.ts +1 -0
  32. package/src/ui/components/diff-renderer/pierre-diff.tsx +171 -0
  33. package/src/ui/components/diff-renderer/split-view.tsx +211 -0
  34. package/src/ui/components/diff-renderer/stacked-view.tsx +168 -0
  35. package/src/ui/components/git-commit-modal.tsx +16 -3
  36. package/src/ui/components/git-pane-widget.tsx +6 -1
  37. package/src/ui/components/git-panel.tsx +215 -86
  38. package/src/ui/components/git-view.tsx +103 -90
  39. package/src/ui/components/help-modal.tsx +45 -12
  40. package/src/ui/components/input-field.tsx +4 -3
  41. package/src/ui/components/list-item.tsx +11 -2
  42. package/src/ui/components/modal-filter-bar.tsx +3 -2
  43. package/src/ui/components/modal-keybinds-overlay.tsx +4 -3
  44. package/src/ui/components/modal-shell.tsx +5 -4
  45. package/src/ui/components/new-tab-modal.tsx +17 -4
  46. package/src/ui/components/pending-chord-overlay.tsx +5 -4
  47. package/src/ui/components/session-bar.tsx +13 -6
  48. package/src/ui/components/session-picker-modal.tsx +28 -10
  49. package/src/ui/components/sidebar.tsx +26 -11
  50. package/src/ui/components/snippet-editor-modal.tsx +18 -3
  51. package/src/ui/components/snippet-picker-modal.tsx +13 -4
  52. package/src/ui/components/split-layout.tsx +3 -2
  53. package/src/ui/components/status-bar.tsx +15 -10
  54. package/src/ui/components/surface.tsx +6 -6
  55. package/src/ui/components/tab-item.tsx +28 -17
  56. package/src/ui/components/terminal-pane.tsx +28 -16
  57. package/src/ui/components/theme-picker-modal.tsx +113 -17
  58. package/src/ui/components/update-available-modal.tsx +13 -2
  59. package/src/ui/filter-themes.ts +15 -0
  60. package/src/ui/keymap-context.ts +10 -2
  61. package/src/ui/root.tsx +29 -7
  62. package/src/ui/shiki.ts +49 -0
  63. package/src/ui/status-bar-model.ts +32 -4
  64. package/src/ui/theme-store.ts +36 -0
  65. package/src/ui/theme.ts +4 -17
  66. package/src/ui/themes.ts +23 -277
  67. package/src/ui/syntax.ts +0 -102
@@ -1,41 +1,62 @@
1
- import { memo, type ReactNode, useMemo } from 'react'
1
+ import type { ScrollBoxRenderable } from '@opentui/core'
2
+
3
+ import { memo, type ReactNode, useEffect, useMemo, useRef } from 'react'
2
4
 
3
5
  import type {
4
6
  GitFileEntry,
7
+ GitFileListMode,
5
8
  GitFileSection,
6
9
  GitPaneDiffCountConfig,
7
10
  GitPanelState,
8
11
  GitPanePathConfig,
9
12
  } from '../../state/types'
10
13
 
11
- import { theme } from '../theme'
14
+ import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
15
+ import { buildGitTreeRows, type GitTreeFileRow, type GitTreeFolderRow } from '../../state/git-tree'
16
+ import { getCurrentTheme, useTheme } from '../theme'
12
17
 
13
18
  interface GitPanelProps {
19
+ collapsedFolders?: Record<string, true>
20
+ fileListMode?: GitFileListMode
14
21
  gitPanel: GitPanelState
15
22
  projectPath: string | undefined
16
- selectedFileKey?: string | null
23
+ selectedEntryKey?: string | null
17
24
  pathConfig?: GitPanePathConfig
18
25
  diffCountConfig?: GitPaneDiffCountConfig
26
+ headOffset?: number
19
27
  }
20
28
 
21
- export function fileKey(file: Pick<GitFileEntry, 'path' | 'section'>): string {
22
- return `${file.section}:${file.path}`
29
+ function sectionTitle(section: GitFileSection, headOffset: number): string {
30
+ switch (section) {
31
+ case 'historical':
32
+ return `HEAD~${headOffset}`
33
+ case 'staged':
34
+ return 'Staged Changes'
35
+ case 'unstaged':
36
+ return 'Changes'
37
+ case 'untracked':
38
+ return 'Untracked'
39
+ }
23
40
  }
24
41
 
25
- const SECTION_ORDER: { key: GitFileSection; title: string }[] = [
26
- { key: 'staged', title: 'Staged Changes' },
27
- { key: 'unstaged', title: 'Changes' },
28
- { key: 'untracked', title: 'Untracked' },
29
- ]
42
+ const BASE_SECTION_ORDER: GitFileSection[] = ['staged', 'unstaged', 'untracked']
43
+ const HISTORICAL_SECTION_ORDER: GitFileSection[] = ['historical', 'untracked']
30
44
 
31
- const STATUS_COLORS: Record<GitFileEntry['status'], string> = {
32
- '?': theme.success,
33
- 'A': theme.success,
34
- 'C': theme.accent,
35
- 'D': theme.danger,
36
- 'M': theme.warning,
37
- 'R': theme.accent,
38
- 'U': theme.danger,
45
+ function statusColor(status: GitFileEntry['status']): string {
46
+ const t = getCurrentTheme()
47
+ switch (status) {
48
+ case '?':
49
+ case 'A':
50
+ return t.colors['gitDecoration.addedResourceForeground']
51
+ case 'C':
52
+ case 'R':
53
+ return t.colors['textLink.foreground']
54
+ case 'D':
55
+ case 'U':
56
+ return t.colors['editorError.foreground']
57
+ case 'M':
58
+ return t.colors['editorWarning.foreground']
59
+ }
39
60
  }
40
61
 
41
62
  function displayStatus(file: GitFileEntry): string {
@@ -43,16 +64,6 @@ function displayStatus(file: GitFileEntry): string {
43
64
  return file.status
44
65
  }
45
66
 
46
- function groupBySection(files: GitFileEntry[]): Record<GitFileSection, GitFileEntry[]> {
47
- const groups: Record<GitFileSection, GitFileEntry[]> = {
48
- staged: [],
49
- unstaged: [],
50
- untracked: [],
51
- }
52
- for (const file of files) groups[file.section].push(file)
53
- return groups
54
- }
55
-
56
67
  function maxDigitWidth(files: GitFileEntry[]): { added: number; removed: number } {
57
68
  let addedMax = 1
58
69
  let removedMax = 1
@@ -67,7 +78,7 @@ function padRight(value: number | null, width: number): string {
67
78
  return String(value ?? 0).padStart(width, ' ')
68
79
  }
69
80
 
70
- export function splitPath(path: string): { prefix: string; basename: string } {
81
+ function splitPath(path: string): { prefix: string; basename: string } {
71
82
  const slash = path.lastIndexOf('/')
72
83
  if (slash < 0) return { basename: path, prefix: '' }
73
84
  return { basename: path.slice(slash + 1), prefix: path.slice(0, slash + 1) }
@@ -77,30 +88,38 @@ function stripTrailingSlash(prefix: string): string {
77
88
  return prefix.endsWith('/') ? prefix.slice(0, -1) : prefix
78
89
  }
79
90
 
80
- function renderPath(file: GitFileEntry, pathConfig: GitPanePathConfig): ReactNode {
81
- const showDir = pathConfig.enabled
91
+ function renderFileLabel(
92
+ file: GitFileEntry,
93
+ pathConfig: GitPanePathConfig,
94
+ fileListMode: GitFileListMode
95
+ ): ReactNode {
82
96
  const transform = pathConfig.enabled ? pathConfig.pathFn : undefined
83
97
  const displayPath = transform ? transform(file.path) : file.path
84
98
  const { basename, prefix } = splitPath(displayPath)
85
99
  const dir = stripTrailingSlash(prefix)
86
- if (file.renamedFrom) {
87
- const renamedDisplay = transform ? transform(file.renamedFrom) : file.renamedFrom
88
- const renamed = splitPath(renamedDisplay)
89
- const renamedDir = stripTrailingSlash(renamed.prefix)
100
+ const showDir = fileListMode === 'flat' && pathConfig.enabled && dir
101
+ if (!file.renamedFrom) {
90
102
  return (
91
103
  <text wrapMode="none">
92
- <span fg={theme.text}>{renamed.basename}</span>
93
- {showDir && renamedDir ? <span fg={theme.textMuted}> {renamedDir}</span> : null}
94
- <span fg={theme.textMuted}> </span>
95
- <span fg={theme.text}>{basename}</span>
96
- {showDir && dir ? <span fg={theme.textMuted}> {dir}</span> : null}
104
+ <span fg={getCurrentTheme().colors['editor.foreground']}>{basename}</span>
105
+ {showDir ? (
106
+ <span fg={getCurrentTheme().colors['descriptionForeground']}> {dir}</span>
107
+ ) : null}
97
108
  </text>
98
109
  )
99
110
  }
111
+ const renamedDisplay = transform ? transform(file.renamedFrom) : file.renamedFrom
112
+ const renamed = splitPath(renamedDisplay)
113
+ const renamedDir = stripTrailingSlash(renamed.prefix)
100
114
  return (
101
115
  <text wrapMode="none">
102
- <span fg={theme.text}>{basename}</span>
103
- {showDir && dir ? <span fg={theme.textMuted}> {dir}</span> : null}
116
+ <span fg={getCurrentTheme().colors['descriptionForeground']}>{renamed.basename}</span>
117
+ {showDir && renamedDir ? (
118
+ <span fg={getCurrentTheme().colors['descriptionForeground']}> {renamedDir}</span>
119
+ ) : null}
120
+ <span fg={getCurrentTheme().colors['descriptionForeground']}> → </span>
121
+ <span fg={getCurrentTheme().colors['editor.foreground']}>{basename}</span>
122
+ {showDir ? <span fg={getCurrentTheme().colors['descriptionForeground']}> {dir}</span> : null}
104
123
  </text>
105
124
  )
106
125
  }
@@ -116,74 +135,145 @@ function renderDiffCount(
116
135
  if (!diffCountConfig.enabled) return null
117
136
  if (!hasNumstat) {
118
137
  return (
119
- <text fg={theme.textMuted} bg={bg} flexShrink={0}>
138
+ <text fg={getCurrentTheme().colors['descriptionForeground']} bg={bg} flexShrink={0}>
120
139
 
121
140
  </text>
122
141
  )
123
142
  }
124
143
  return (
125
144
  <box flexDirection="row" flexShrink={0}>
126
- <text fg={theme.success} bg={bg}>{`+${padRight(file.added, addedW)}`}</text>
127
- <text fg={theme.dim} bg={bg}>
145
+ <text
146
+ fg={getCurrentTheme().colors['gitDecoration.addedResourceForeground']}
147
+ bg={bg}
148
+ >{`+${padRight(file.added, addedW)}`}</text>
149
+ <text fg={getCurrentTheme().colors['editor.lineHighlightBackground']} bg={bg}>
128
150
  {' '}
129
151
  </text>
130
- <text fg={theme.danger} bg={bg}>{`−${padRight(file.removed, removedW)}`}</text>
152
+ <text
153
+ fg={getCurrentTheme().colors['editorError.foreground']}
154
+ bg={bg}
155
+ >{`−${padRight(file.removed, removedW)}`}</text>
156
+ </box>
157
+ )
158
+ }
159
+
160
+ function renderFolderRow(row: GitTreeFolderRow, isSelected: boolean): ReactNode {
161
+ const bg = isSelected ? getCurrentTheme().colors['list.activeSelectionBackground'] : undefined
162
+ const onSelect = (): void => {
163
+ dispatchGlobal({ key: row.key, type: 'git-mode-select-entry-by-key' })
164
+ }
165
+ const onToggle = (): void => {
166
+ dispatchGlobal({ key: row.key, type: 'git-mode-toggle-folder' })
167
+ }
168
+ return (
169
+ <box key={row.key} flexDirection="row" gap={1} backgroundColor={bg} onMouseDown={onSelect}>
170
+ <box flexGrow={1} overflow="hidden" paddingLeft={row.depth * 2}>
171
+ <box flexDirection="row" gap={1} onMouseDown={onToggle}>
172
+ <text fg={getCurrentTheme().colors['descriptionForeground']} bg={bg}>
173
+ {row.isCollapsed ? '▸' : '▾'}
174
+ </text>
175
+ <text fg={getCurrentTheme().colors['terminal.ansiMagenta']} bg={bg} wrapMode="none">
176
+ {row.name}
177
+ </text>
178
+ </box>
179
+ </box>
131
180
  </box>
132
181
  )
133
182
  }
134
183
 
135
184
  function renderFileRow(
136
- file: GitFileEntry,
137
- key: string,
185
+ row: GitTreeFileRow,
138
186
  addedW: number,
139
187
  removedW: number,
140
188
  isSelected: boolean,
189
+ fileListMode: GitFileListMode,
141
190
  pathConfig: GitPanePathConfig,
142
191
  diffCountConfig: GitPaneDiffCountConfig
143
192
  ): ReactNode {
193
+ const file = row.file
144
194
  const hasNumstat = file.added !== null || file.removed !== null
145
- const bg = isSelected ? theme.panelHighlight : undefined
195
+ const bg = isSelected ? getCurrentTheme().colors['list.activeSelectionBackground'] : undefined
196
+ const onSelect = (): void => {
197
+ dispatchGlobal({ key: row.key, type: 'git-mode-select-entry-by-key' })
198
+ }
146
199
  return (
147
- <box key={key} flexDirection="row" gap={1} paddingLeft={1} backgroundColor={bg}>
148
- <text fg={STATUS_COLORS[file.status]} bg={bg}>
149
- <strong>{displayStatus(file)}</strong>
150
- </text>
151
- <box flexGrow={1} overflow="hidden">
152
- {renderPath(file, pathConfig)}
200
+ <box key={row.key} flexDirection="row" gap={1} backgroundColor={bg} onMouseDown={onSelect}>
201
+ <box width={2} flexShrink={0} justifyContent="center">
202
+ <text fg={statusColor(file.status)} bg={bg}>
203
+ <strong>{displayStatus(file)}</strong>
204
+ </text>
205
+ </box>
206
+ <box flexGrow={1} overflow="hidden" paddingLeft={fileListMode === 'tree' ? row.depth * 2 : 0}>
207
+ {renderFileLabel(file, pathConfig, fileListMode)}
153
208
  </box>
154
209
  {renderDiffCount(file, addedW, removedW, bg, diffCountConfig, hasNumstat)}
155
210
  </box>
156
211
  )
157
212
  }
158
213
 
159
- function renderSection(
214
+ function renderTreeSection(
160
215
  section: GitFileSection,
161
216
  title: string,
162
217
  files: GitFileEntry[],
218
+ rows: Array<GitTreeFolderRow | GitTreeFileRow>,
163
219
  addedW: number,
164
220
  removedW: number,
165
- selectedFileKey: string | null | undefined,
221
+ fileListMode: GitFileListMode,
222
+ selectedEntryKey: string | null | undefined,
223
+ showListModeToggle: boolean,
166
224
  pathConfig: GitPanePathConfig,
167
225
  diffCountConfig: GitPaneDiffCountConfig
168
226
  ): ReactNode {
169
227
  if (files.length === 0) return null
228
+ const nextFileListMode = fileListMode === 'tree' ? 'flat' : 'tree'
229
+ const toggleListMode = (): void => {
230
+ dispatchGlobal({ type: 'git-mode-toggle-file-list-mode' })
231
+ runSideEffectGlobal({ mode: nextFileListMode, type: 'persist-git-file-list-mode' })
232
+ }
170
233
  return (
171
234
  <box key={section} flexDirection="column">
172
- <text fg={theme.accentAlt}>
173
- <strong>
174
- {title} ({files.length})
175
- </strong>
176
- </text>
177
- {files.map((file, i) =>
178
- renderFileRow(
179
- file,
180
- `${section}-${i}`,
181
- addedW,
182
- removedW,
183
- !!selectedFileKey && fileKey(file) === selectedFileKey,
184
- pathConfig,
185
- diffCountConfig
186
- )
235
+ <box flexDirection="row" justifyContent="space-between">
236
+ <text fg={getCurrentTheme().colors['terminal.ansiMagenta']}>
237
+ <strong>
238
+ {title} ({files.length})
239
+ </strong>
240
+ </text>
241
+ {showListModeToggle ? (
242
+ <box flexDirection="row" gap={1} onMouseDown={toggleListMode}>
243
+ <text
244
+ fg={
245
+ fileListMode === 'tree'
246
+ ? getCurrentTheme().colors['textLink.foreground']
247
+ : getCurrentTheme().colors['descriptionForeground']
248
+ }
249
+ >
250
+ tree
251
+ </text>
252
+ <text fg={getCurrentTheme().colors['descriptionForeground']}>|</text>
253
+ <text
254
+ fg={
255
+ fileListMode === 'flat'
256
+ ? getCurrentTheme().colors['textLink.foreground']
257
+ : getCurrentTheme().colors['descriptionForeground']
258
+ }
259
+ >
260
+ flat
261
+ </text>
262
+ </box>
263
+ ) : null}
264
+ </box>
265
+ {rows.map((row) =>
266
+ row.kind === 'folder'
267
+ ? renderFolderRow(row, row.key === selectedEntryKey)
268
+ : renderFileRow(
269
+ row,
270
+ addedW,
271
+ removedW,
272
+ row.key === selectedEntryKey,
273
+ fileListMode,
274
+ pathConfig,
275
+ diffCountConfig
276
+ )
187
277
  )}
188
278
  </box>
189
279
  )
@@ -209,16 +299,25 @@ function computeStatusPlaceholder(
209
299
  hasProjectPath: boolean
210
300
  ): StatusPlaceholder | null {
211
301
  if (!hasProjectPath) {
212
- return { label: 'No active session', labelColor: theme.textMuted }
302
+ return {
303
+ label: 'No active session',
304
+ labelColor: getCurrentTheme().colors['descriptionForeground'],
305
+ }
213
306
  }
214
307
  if (gitPanel.error === 'not-a-repo') {
215
- return { label: 'Not a git repository', labelColor: theme.textMuted }
308
+ return {
309
+ label: 'Not a git repository',
310
+ labelColor: getCurrentTheme().colors['descriptionForeground'],
311
+ }
216
312
  }
217
313
  if (gitPanel.error === 'unknown') {
218
- return { label: 'Git error', labelColor: theme.danger }
314
+ return { label: 'Git error', labelColor: getCurrentTheme().colors['editorError.foreground'] }
219
315
  }
220
316
  if (gitPanel.files.length === 0) {
221
- return { label: 'Working tree clean', labelColor: theme.textMuted }
317
+ return {
318
+ label: 'Working tree clean',
319
+ labelColor: getCurrentTheme().colors['descriptionForeground'],
320
+ }
222
321
  }
223
322
  return null
224
323
  }
@@ -227,13 +326,22 @@ const DEFAULT_PATH_CONFIG: GitPanePathConfig = { enabled: true }
227
326
  const DEFAULT_DIFF_COUNT_CONFIG: GitPaneDiffCountConfig = { enabled: true }
228
327
 
229
328
  export const GitPanel = memo(function GitPanel({
329
+ collapsedFolders = {},
230
330
  diffCountConfig = DEFAULT_DIFF_COUNT_CONFIG,
331
+ fileListMode = 'tree',
231
332
  gitPanel,
333
+ headOffset = 0,
232
334
  pathConfig = DEFAULT_PATH_CONFIG,
233
335
  projectPath,
234
- selectedFileKey,
336
+ selectedEntryKey,
235
337
  }: GitPanelProps) {
236
- const groups = useMemo(() => groupBySection(gitPanel.files), [gitPanel.files])
338
+ const theme = useTheme()
339
+ const sectionOrder = headOffset > 0 ? HISTORICAL_SECTION_ORDER : BASE_SECTION_ORDER
340
+ const scrollRef = useRef<ScrollBoxRenderable | null>(null)
341
+ const tree = useMemo(
342
+ () => buildGitTreeRows(gitPanel.files, collapsedFolders, fileListMode),
343
+ [collapsedFolders, fileListMode, gitPanel.files]
344
+ )
237
345
  const { added: addedW, removed: removedW } = useMemo(
238
346
  () => maxDigitWidth(gitPanel.files),
239
347
  [gitPanel.files]
@@ -242,33 +350,54 @@ export const GitPanel = memo(function GitPanel({
242
350
  const statusNode = renderStatus(gitPanel, !!projectPath)
243
351
 
244
352
  const hasRemoteTracking = gitPanel.ahead > 0 || gitPanel.behind > 0
353
+ const toggleSection =
354
+ tree.sections.find((section) => section.section === 'unstaged' && section.files.length > 0)
355
+ ?.section ??
356
+ tree.sections.find((section) => section.files.length > 0)?.section ??
357
+ null
358
+
359
+ useEffect(() => {
360
+ const scrollbox = scrollRef.current
361
+ if (!scrollbox || !selectedEntryKey) return
362
+ const selectedIndex = tree.visibleRows.findIndex((row) => row.key === selectedEntryKey)
363
+ if (selectedIndex < 0) return
364
+ const viewportHeight = Math.max(1, scrollbox.viewport.height)
365
+ const target = Math.max(0, selectedIndex - Math.floor(viewportHeight / 2))
366
+ scrollbox.scrollTo({ x: 0, y: target })
367
+ }, [selectedEntryKey, tree.visibleRows])
245
368
 
246
369
  return (
247
370
  <box flexDirection="column" flexGrow={1} gap={0}>
248
371
  {hasRemoteTracking ? (
249
- <text fg={theme.textMuted}>
372
+ <text fg={theme.colors['descriptionForeground']}>
250
373
  ↑{gitPanel.ahead} ↓{gitPanel.behind}
251
374
  </text>
252
375
  ) : null}
253
376
  {statusNode ?? (
254
377
  <scrollbox
255
378
  flexGrow={1}
379
+ ref={scrollRef}
256
380
  scrollY
381
+ scrollbarOptions={{ visible: false }}
257
382
  viewportCulling
258
383
  contentOptions={{ flexDirection: 'column', gap: 0 }}
259
384
  >
260
- {SECTION_ORDER.map((s) =>
261
- renderSection(
262
- s.key,
263
- s.title,
264
- groups[s.key],
385
+ {sectionOrder.map((key) => {
386
+ const section = tree.sections.find((entry) => entry.section === key)
387
+ return renderTreeSection(
388
+ key,
389
+ sectionTitle(key, headOffset),
390
+ section?.files ?? [],
391
+ section?.rows ?? [],
265
392
  addedW,
266
393
  removedW,
267
- selectedFileKey,
394
+ fileListMode,
395
+ selectedEntryKey,
396
+ key === toggleSection,
268
397
  pathConfig,
269
398
  diffCountConfig
270
399
  )
271
- )}
400
+ })}
272
401
  </scrollbox>
273
402
  )}
274
403
  </box>