@brimveyn/aimux 1.12.1 → 1.12.6

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.
@@ -1,3 +1,5 @@
1
+ import type { SnippetDef, SnippetVar } from '@brimveyn/aimux-config'
2
+
1
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
4
  import { join } from 'node:path'
3
5
 
@@ -9,9 +11,15 @@ export interface SnippetRecord {
9
11
  id: string
10
12
  name: string
11
13
  content: string
14
+ trigger?: string
15
+ vars?: Record<string, SnippetVar>
16
+ }
17
+
18
+ export function getSnippetsCatalogPath(): string {
19
+ return join(getProfileConfigDir(), 'aimux-snippets.json')
12
20
  }
13
21
 
14
- const SNIPPETS_PATH = join(getProfileConfigDir(), 'aimux-snippets.json')
22
+ const SNIPPETS_PATH = getSnippetsCatalogPath()
15
23
 
16
24
  const DEFAULT_SNIPPETS: SnippetRecord[] = [
17
25
  {
@@ -52,7 +60,7 @@ export function loadSnippetCatalog(): SnippetRecord[] {
52
60
  version?: unknown
53
61
  snippets?: unknown
54
62
  }
55
- if (parsed.version !== 1 || !Array.isArray(parsed.snippets)) {
63
+ if ((parsed.version !== 1 && parsed.version !== 2) || !Array.isArray(parsed.snippets)) {
56
64
  logDebug('snippets.catalog.loadIssue', {
57
65
  issue: 'invalid snippet catalog header',
58
66
  path: SNIPPETS_PATH,
@@ -66,7 +74,7 @@ export function loadSnippetCatalog(): SnippetRecord[] {
66
74
  })
67
75
  return []
68
76
  }
69
- return parsed.snippets
77
+ return parsed.snippets.map(stripUserVars)
70
78
  } catch (error) {
71
79
  logDebug('snippets.catalog.loadIssue', {
72
80
  issue: error instanceof Error ? error.message : String(error),
@@ -79,7 +87,16 @@ export function loadSnippetCatalog(): SnippetRecord[] {
79
87
  export function saveSnippetCatalog(snippets: SnippetRecord[]): void {
80
88
  try {
81
89
  mkdirSync(getProfileConfigDir(), { recursive: true })
82
- writeFileSync(SNIPPETS_PATH, `${JSON.stringify({ snippets, version: 1 }, null, 2)}\n`)
90
+ // Persist only user-owned snippets. Config-pinned entries are reapplied
91
+ // at boot from `aimux.config.ts`.
92
+ const userSnippets = snippets.filter((s) => !isConfigSnippetId(s.id)).map(stripUserVars)
93
+ // Schema v2 adds the optional `trigger` and `vars` fields. v1 files are
94
+ // still accepted on read (they validate as v2 — both fields are optional)
95
+ // and get rewritten as v2 on the next save.
96
+ writeFileSync(
97
+ SNIPPETS_PATH,
98
+ `${JSON.stringify({ snippets: userSnippets, version: 2 }, null, 2)}\n`
99
+ )
83
100
  } catch (error) {
84
101
  logDebug('snippets.catalog.saveError', {
85
102
  error: error instanceof Error ? error.message : String(error),
@@ -88,3 +105,47 @@ export function saveSnippetCatalog(snippets: SnippetRecord[]): void {
88
105
  })
89
106
  }
90
107
  }
108
+
109
+ export const CONFIG_SNIPPET_ID_PREFIX = 'config:'
110
+
111
+ export function isConfigSnippetId(id: string): boolean {
112
+ return id.startsWith(CONFIG_SNIPPET_ID_PREFIX)
113
+ }
114
+
115
+ /**
116
+ * Shell `vars` are only authorized on config-pinned snippets (those defined in
117
+ * `aimux.config.ts`). If they ever appear on a user-edited JSON snippet — by
118
+ * hand edit, restore, or import — strip them. This keeps shell execution
119
+ * gated by the user's TypeScript config file.
120
+ *
121
+ * Exported for testing; called at both load and save time.
122
+ */
123
+ export function stripUserVars(snippet: SnippetRecord): SnippetRecord {
124
+ if (snippet.vars === undefined) return snippet
125
+ if (isConfigSnippetId(snippet.id)) return snippet
126
+ logDebug('snippets.catalog.strippedVars', { id: snippet.id, name: snippet.name })
127
+ const { vars, ...clean } = snippet
128
+ void vars
129
+ return clean
130
+ }
131
+
132
+ /**
133
+ * Merge config-defined snippets with user-edited snippets.
134
+ * Config-pinned snippets ("sticky") get a stable id `config:${name}` and win
135
+ * over a user-edited snippet with the same id (they're read-only in the UI).
136
+ */
137
+ export function mergeConfigSnippets(
138
+ userSnippets: readonly SnippetRecord[],
139
+ configSnippets: readonly SnippetDef[]
140
+ ): SnippetRecord[] {
141
+ const fromConfig: SnippetRecord[] = configSnippets.map((s) => ({
142
+ content: s.text,
143
+ id: `${CONFIG_SNIPPET_ID_PREFIX}${s.name}`,
144
+ name: s.name,
145
+ trigger: s.trigger,
146
+ vars: s.vars,
147
+ }))
148
+ const configIds = new Set(fromConfig.map((s) => s.id))
149
+ const userKept = userSnippets.filter((s) => !configIds.has(s.id))
150
+ return [...fromConfig, ...userKept]
151
+ }
@@ -1,7 +1,7 @@
1
- import type { ModeId } from '@brimveyn/aimux-config'
1
+ import type { ModeId, SnippetVar } from '@brimveyn/aimux-config'
2
2
  import type { ThemedToken } from 'shiki'
3
3
 
4
- export type BuiltinAssistantId = 'claude' | 'codex' | 'opencode' | 'terminal'
4
+ export type BuiltinAssistantId = 'claude' | 'codex' | 'opencode' | 'terminal' | 'antigravity'
5
5
 
6
6
  export type AssistantId = BuiltinAssistantId | (string & {})
7
7
 
@@ -306,6 +306,11 @@ export interface ModalRenameTab extends ModalBase {
306
306
 
307
307
  export interface ModalSnippetPicker extends ModalBase {
308
308
  type: 'snippet-picker'
309
+ /**
310
+ * Transient status line shown at the bottom of the picker (e.g. error from
311
+ * an open-in-editor attempt). Cleared automatically when the modal closes.
312
+ */
313
+ actionMessage?: string | null
309
314
  }
310
315
 
311
316
  export interface ModalThemePicker extends ModalBase {
@@ -342,7 +347,12 @@ export interface ModalCreateSession extends ModalBase {
342
347
 
343
348
  export interface ModalSnippetEditor extends ModalBase {
344
349
  type: 'snippet-editor'
345
- activeField: 'name' | 'content'
350
+ activeField: 'name' | 'trigger' | 'content'
351
+ /** Persisted value of the name field when it is not the active editor. */
352
+ nameBuffer: string
353
+ /** Persisted value of the trigger field when it is not the active editor. */
354
+ triggerBuffer: string
355
+ /** Persisted value of the content field when it is not the active editor. */
346
356
  contentBuffer: string
347
357
  }
348
358
 
@@ -388,6 +398,8 @@ export interface SnippetRecord {
388
398
  id: string
389
399
  name: string
390
400
  content: string
401
+ trigger?: string
402
+ vars?: Record<string, SnippetVar>
391
403
  }
392
404
 
393
405
  export interface DiscoveredRepo {
@@ -643,6 +655,7 @@ export type GitModeAction =
643
655
  | { type: 'git-mode-set-pending-delete'; path: string | null }
644
656
  | { type: 'git-mode-clear-diff-cache'; path: string }
645
657
  | { type: 'git-mode-set-message'; message: string | null }
658
+ | { type: 'snippet-picker-set-message'; message: string | null }
646
659
  | { type: 'git-mode-toggle-diff-view' }
647
660
  | { type: 'git-mode-shift-head-offset'; delta: number }
648
661
  | { type: 'git-mode-set-head-offset'; offset: number }
@@ -155,8 +155,29 @@ export function isSessionRecord(value: unknown): value is SessionRecord {
155
155
  )
156
156
  }
157
157
 
158
+ function isSnippetVar(value: unknown): boolean {
159
+ if (!isObjectRecord(value)) return false
160
+ if (!isString(value.sh)) return false
161
+ if (value.timeout !== undefined && !isFiniteNumber(value.timeout)) return false
162
+ if (value.trim !== undefined && !isBoolean(value.trim)) return false
163
+ return true
164
+ }
165
+
166
+ function isSnippetVarRecord(value: unknown): boolean {
167
+ if (!isObjectRecord(value)) return false
168
+ for (const entry of Object.values(value)) {
169
+ if (!isSnippetVar(entry)) return false
170
+ }
171
+ return true
172
+ }
173
+
158
174
  export function isSnippetRecord(value: unknown): value is SnippetRecord {
159
175
  return (
160
- isObjectRecord(value) && isString(value.id) && isString(value.name) && isString(value.content)
176
+ isObjectRecord(value) &&
177
+ isString(value.id) &&
178
+ isString(value.name) &&
179
+ isString(value.content) &&
180
+ (value.trigger === undefined || isString(value.trigger)) &&
181
+ (value.vars === undefined || isSnippetVarRecord(value.vars))
161
182
  )
162
183
  }
@@ -1,5 +1,6 @@
1
1
  import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
2
2
 
3
+ import type { MeasuredPaneRect } from '../../../app-runtime/use-pane-size-report'
3
4
  import type { TerminalContentOrigin } from '../../../input/raw-input-handler'
4
5
  import type { FocusMode, TabSession } from '../../../state/types'
5
6
 
@@ -39,6 +40,7 @@ interface SplitLayoutProps {
39
40
  onSeparatorDrag?: (event: OtuiMouseEvent) => boolean
40
41
  onSeparatorDragEnd?: () => void
41
42
  onLeftEdgeMouseDown?: (event: OtuiMouseEvent) => boolean
43
+ onMeasure?: (tabId: string, rect: MeasuredPaneRect) => void
42
44
  contentOrigin: TerminalContentOrigin
43
45
  bounds: PaneRect
44
46
  }
@@ -52,6 +54,7 @@ export function SplitLayout({
52
54
  mouseForwardingEnabled,
53
55
  node,
54
56
  onLeftEdgeMouseDown,
57
+ onMeasure,
55
58
  onPaneActivate,
56
59
  onSeparatorDrag,
57
60
  onSeparatorDragEnd,
@@ -104,6 +107,7 @@ export function SplitLayout({
104
107
  onSeparatorDrag={onSeparatorDrag}
105
108
  onSeparatorDragEnd={onSeparatorDragEnd}
106
109
  onLeftEdgeMouseDown={onLeftEdgeMouseDown}
110
+ onMeasure={onMeasure}
107
111
  />
108
112
  )
109
113
  }
@@ -144,6 +148,7 @@ export function SplitLayout({
144
148
  onSeparatorDrag={onSeparatorDrag}
145
149
  onSeparatorDragEnd={onSeparatorDragEnd}
146
150
  onLeftEdgeMouseDown={onLeftEdgeMouseDown}
151
+ onMeasure={onMeasure}
147
152
  contentOrigin={contentOrigin}
148
153
  bounds={firstBounds}
149
154
  />
@@ -187,6 +192,7 @@ export function SplitLayout({
187
192
  onSeparatorDrag={onSeparatorDrag}
188
193
  onSeparatorDragEnd={onSeparatorDragEnd}
189
194
  onLeftEdgeMouseDown={secondLeftEdgeMouseDown}
195
+ onMeasure={onMeasure}
190
196
  contentOrigin={contentOrigin}
191
197
  bounds={secondBounds}
192
198
  />
@@ -49,7 +49,9 @@ export function StatusBar() {
49
49
 
50
50
  return (
51
51
  <box
52
- minHeight={2}
52
+ height={2}
53
+ flexShrink={0}
54
+ overflow="hidden"
53
55
  paddingLeft={1}
54
56
  paddingRight={1}
55
57
  paddingTop={0}
@@ -5,6 +5,7 @@ import { memo, type ReactNode } from 'react'
5
5
  import type { TerminalContentOrigin } from '../../../input/raw-input-handler'
6
6
  import type { TabSession, TerminalSnapshot, TerminalSpan } from '../../../state/types'
7
7
 
8
+ import { type MeasuredPaneRect, usePaneSizeReport } from '../../../app-runtime/use-pane-size-report'
8
9
  import { logInputDebug } from '../../../debug/input-log'
9
10
  import { dispatchGlobal, runSideEffectGlobal } from '../../../state/dispatch-ref'
10
11
  import { type ContextMenuItem, openContextMenu } from '../../context-menu/controller'
@@ -28,6 +29,7 @@ interface TerminalPaneProps {
28
29
  onSeparatorDrag?: (event: OtuiMouseEvent) => boolean
29
30
  onSeparatorDragEnd?: () => void
30
31
  onLeftEdgeMouseDown?: (event: OtuiMouseEvent) => boolean
32
+ onMeasure?: (tabId: string, rect: MeasuredPaneRect) => void
31
33
  }
32
34
 
33
35
  function getTitle(
@@ -112,6 +114,7 @@ export function TerminalPane({
112
114
  localScrollbackEnabled,
113
115
  mouseForwardingEnabled,
114
116
  onLeftEdgeMouseDown,
117
+ onMeasure,
115
118
  onPaneActivate,
116
119
  onSeparatorDrag,
117
120
  onSeparatorDragEnd,
@@ -124,6 +127,7 @@ export function TerminalPane({
124
127
  tabId,
125
128
  }: TerminalPaneProps) {
126
129
  const t = useTheme()
130
+ const setContentBox = usePaneSizeReport(tabId, !!tab, onMeasure)
127
131
  const editorBg = t.background
128
132
  const paneIsActive = isActive ?? true
129
133
  const canForwardMouse = focusMode === 'terminal-input' && !!tab && mouseForwardingEnabled
@@ -287,9 +291,11 @@ export function TerminalPane({
287
291
  </box>
288
292
  ) : (
289
293
  <box
294
+ ref={setContentBox}
290
295
  flexDirection="column"
291
296
  flexGrow={1}
292
297
  width="100%"
298
+ overflow="hidden"
293
299
  onMouseDown={(e) => {
294
300
  e.stopPropagation()
295
301
  forwardMouseEvent(e)
@@ -302,10 +308,18 @@ export function TerminalPane({
302
308
  </box>
303
309
  )}
304
310
  </ContextMenuBox>
305
- {tab?.status === 'disconnected' ? (
306
- <text fg={t.warning}>Restored snapshot. Press Ctrl+r to restart this workspace.</text>
311
+ {tab?.status === 'disconnected' || tab?.errorMessage ? (
312
+ // Absolutely positioned so it overlays the bordered box instead of
313
+ // consuming a flex row. If it took a row, the rendered terminal area
314
+ // would be one line shorter than the size sent to the PTY/xterm,
315
+ // re-introducing the shifted-content / dead-row bug.
316
+ <box position="absolute" bottom={0} left={0} backgroundColor={editorBg}>
317
+ {tab?.status === 'disconnected' ? (
318
+ <text fg={t.warning}>Restored snapshot. Press Ctrl+r to restart this workspace.</text>
319
+ ) : null}
320
+ {tab?.errorMessage ? <text fg={t.error}>{tab.errorMessage}</text> : null}
321
+ </box>
307
322
  ) : null}
308
- {tab?.errorMessage ? <text fg={t.error}>{tab.errorMessage}</text> : null}
309
323
  </box>
310
324
  )
311
325
  }
@@ -2,8 +2,9 @@ import { uiTokens } from '../../../ui-tokens'
2
2
  import { Form, TextField } from '../shared/form'
3
3
 
4
4
  interface SnippetEditorModalProps {
5
- activeField: 'name' | 'content'
5
+ activeField: 'name' | 'trigger' | 'content'
6
6
  snippetName: string
7
+ snippetTrigger: string
7
8
  snippetContent: string
8
9
  isEditing: boolean
9
10
  }
@@ -13,18 +14,21 @@ export function SnippetEditorModal({
13
14
  isEditing,
14
15
  snippetContent,
15
16
  snippetName,
17
+ snippetTrigger,
16
18
  }: SnippetEditorModalProps) {
17
- const nameActive = activeField === 'name'
18
- const contentActive = activeField === 'content'
19
-
20
19
  return (
21
20
  <Form
22
21
  title={isEditing ? 'Edit snippet' : 'Create snippet'}
23
22
  keybindsModeId="modal.snippet-editor"
24
23
  width={uiTokens.modalWidth.xl}
25
24
  >
26
- <TextField active={nameActive} label="Name" value={snippetName} />
27
- <TextField active={contentActive} label="Content" value={snippetContent} />
25
+ <TextField active={activeField === 'name'} label="Name" value={snippetName} />
26
+ <TextField
27
+ active={activeField === 'trigger'}
28
+ label="Trigger (optional)"
29
+ value={snippetTrigger}
30
+ />
31
+ <TextField active={activeField === 'content'} label="Content" value={snippetContent} />
28
32
  </Form>
29
33
  )
30
34
  }
@@ -2,6 +2,7 @@ import type { SnippetRecord } from '../../../../state/types'
2
2
 
3
3
  import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
4
4
  import { filterSnippets } from '../../../../state/selectors'
5
+ import { isConfigSnippetId } from '../../../../state/snippet-catalog'
5
6
  import { useTheme } from '../../../theme'
6
7
  import { uiTokens } from '../../../ui-tokens'
7
8
  import { Picker, type PickerItem } from '../shared/picker'
@@ -11,6 +12,7 @@ interface SnippetPickerModalProps {
11
12
  selectedIndex: number
12
13
  filter: string | null
13
14
  cursorPos?: number
15
+ actionMessage?: string | null
14
16
  }
15
17
 
16
18
  const MAX_PREVIEW_LENGTH = 60
@@ -22,6 +24,7 @@ function truncateContent(content: string): string {
22
24
  }
23
25
 
24
26
  export function SnippetPickerModal({
27
+ actionMessage,
25
28
  cursorPos,
26
29
  filter,
27
30
  selectedIndex,
@@ -32,19 +35,26 @@ export function SnippetPickerModal({
32
35
 
33
36
  const items: PickerItem[] = filtered.map((snippet, index) => {
34
37
  const active = index === selectedIndex
38
+ const fromConfig = isConfigSnippetId(snippet.id)
35
39
  return {
36
40
  key: snippet.id,
37
41
  onClick: () => {
38
42
  dispatchGlobal({ type: 'close-modal' })
39
43
  runSideEffectGlobal({ type: 'paste-selected-snippet' })
40
44
  },
41
- onDelete: () => runSideEffectGlobal({ type: 'delete-selected-snippet' }),
42
- onEdit: () => runSideEffectGlobal({ type: 'edit-selected-snippet' }),
45
+ onDelete: fromConfig
46
+ ? undefined
47
+ : () => runSideEffectGlobal({ type: 'delete-selected-snippet' }),
48
+ onEdit: fromConfig ? undefined : () => runSideEffectGlobal({ type: 'edit-selected-snippet' }),
43
49
  subtitle: <text fg={t.textMuted}>{truncateContent(snippet.content)}</text>,
44
50
  title: (
45
- <text fg={active ? t.text : t.textMuted}>
46
- <strong>{snippet.name}</strong>
47
- </text>
51
+ <box flexDirection="row">
52
+ <text fg={active ? t.text : t.textMuted}>
53
+ <strong>{snippet.name}</strong>
54
+ </text>
55
+ {snippet.trigger ? <text fg={t.textMuted}>{` :${snippet.trigger}`}</text> : null}
56
+ {fromConfig ? <text fg={t.textMuted}>{' [config]'}</text> : null}
57
+ </box>
48
58
  ),
49
59
  }
50
60
  })
@@ -64,6 +74,7 @@ export function SnippetPickerModal({
64
74
  {filter ? 'No matching snippets.' : 'No snippets yet. Press n to create one.'}
65
75
  </text>
66
76
  }
77
+ footer={actionMessage ? <text fg={t.error}>{actionMessage}</text> : undefined}
67
78
  onHover={(index) => dispatchGlobal({ index, type: 'set-modal-selection-index' })}
68
79
  />
69
80
  )
package/src/ui/root.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { MouseEvent } from '@opentui/core'
2
2
 
3
+ import type { MeasuredPaneRect } from '../app-runtime/use-pane-size-report'
3
4
  import type { TerminalContentOrigin } from '../input/raw-input-handler'
4
5
  import type { FocusMode, ModalState, SessionRecord, SnippetRecord } from '../state/types'
5
6
  import type { ThemeId } from './themes'
@@ -52,19 +53,14 @@ function getCreateSessionFields(modal: ModalState) {
52
53
 
53
54
  function getSnippetEditorFields(modal: ModalState) {
54
55
  if (modal.type !== 'snippet-editor') {
55
- return { snippetContent: '', snippetName: '' }
56
- }
57
-
58
- if (modal.activeField === 'name') {
59
- return {
60
- snippetContent: modal.contentBuffer,
61
- snippetName: modal.editBuffer ?? '',
62
- }
56
+ return { snippetContent: '', snippetName: '', snippetTrigger: '' }
63
57
  }
64
58
 
59
+ const editValue = modal.editBuffer ?? ''
65
60
  return {
66
- snippetContent: modal.editBuffer ?? '',
67
- snippetName: modal.contentBuffer,
61
+ snippetContent: modal.activeField === 'content' ? editValue : modal.contentBuffer,
62
+ snippetName: modal.activeField === 'name' ? editValue : modal.nameBuffer,
63
+ snippetTrigger: modal.activeField === 'trigger' ? editValue : modal.triggerBuffer,
68
64
  }
69
65
  }
70
66
 
@@ -78,7 +74,7 @@ function renderModal(
78
74
  snippets: SnippetRecord[]
79
75
  themeId: ThemeId
80
76
  createSessionFields: { directoryQuery: string; sessionName: string }
81
- snippetEditorFields: { snippetName: string; snippetContent: string }
77
+ snippetEditorFields: { snippetName: string; snippetTrigger: string; snippetContent: string }
82
78
  focusMode: FocusMode
83
79
  activeAssistant?: string
84
80
  autoCommitModel?: string
@@ -135,6 +131,7 @@ function renderModal(
135
131
  selectedIndex={modal.selectedIndex}
136
132
  filter={modal.editBuffer}
137
133
  cursorPos={modal.cursorPos}
134
+ actionMessage={modal.actionMessage}
138
135
  />
139
136
  )
140
137
  case 'snippet-editor':
@@ -142,6 +139,7 @@ function renderModal(
142
139
  <SnippetEditorModal
143
140
  activeField={modal.activeField}
144
141
  snippetName={options.snippetEditorFields.snippetName}
142
+ snippetTrigger={options.snippetEditorFields.snippetTrigger}
145
143
  snippetContent={options.snippetEditorFields.snippetContent}
146
144
  isEditing={modal.sessionTargetId !== null}
147
145
  />
@@ -229,6 +227,7 @@ interface RootViewProps {
229
227
  }) => void
230
228
  onSeparatorDrag?: (event: MouseEvent) => boolean
231
229
  onSeparatorDragEnd?: () => void
230
+ onMeasure?: (tabId: string, rect: MeasuredPaneRect) => void
232
231
  terminalCols: number
233
232
  terminalRows: number
234
233
  }
@@ -239,6 +238,7 @@ export function RootView({
239
238
  mouseForwardingEnabled,
240
239
  onEmbeddedGitResizeStart,
241
240
  onGitPaneResizeStart,
241
+ onMeasure,
242
242
  onPaneActivate,
243
243
  onSeparatorDrag,
244
244
  onSeparatorDragEnd,
@@ -375,6 +375,7 @@ export function RootView({
375
375
  onSeparatorDrag={onSeparatorDrag}
376
376
  onSeparatorDragEnd={onSeparatorDragEnd}
377
377
  onLeftEdgeMouseDown={handleTerminalLeftEdgeMouseDown}
378
+ onMeasure={onMeasure}
378
379
  bounds={{
379
380
  cols: terminalCols + splitChrome,
380
381
  rows: terminalRows + splitChrome,
@@ -398,6 +399,7 @@ export function RootView({
398
399
  onTerminalMouseUp={onTerminalMouseUp}
399
400
  onPaneActivate={onPaneActivate}
400
401
  onLeftEdgeMouseDown={handleTerminalLeftEdgeMouseDown}
402
+ onMeasure={onMeasure}
401
403
  />
402
404
  )}
403
405
  {gitPaneMode === 'pane' && gitPaneVisible && gitPanePosition === 'right' ? (