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,313 @@
1
+ import { type ExecuteResult, execute } from "./executor"
2
+
3
+ export interface OperationResult extends ExecuteResult {
4
+ command: string
5
+ }
6
+
7
+ export interface OpLogEntry {
8
+ operationId: string
9
+ lines: string[]
10
+ isCurrent: boolean
11
+ }
12
+
13
+ function stripAnsi(str: string): string {
14
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequence
15
+ return str.replace(/\x1b\[[0-9;]*m/g, "")
16
+ }
17
+
18
+ export function parseOpLog(lines: string[]): OpLogEntry[] {
19
+ const operations: OpLogEntry[] = []
20
+ let current: OpLogEntry | null = null
21
+
22
+ for (const line of lines) {
23
+ const stripped = stripAnsi(line)
24
+ const isHeader = stripped.startsWith("@") || stripped.startsWith("○")
25
+
26
+ if (isHeader) {
27
+ if (current) {
28
+ operations.push(current)
29
+ }
30
+ const parts = stripped.split(/\s+/)
31
+ const operationId = parts[1] || ""
32
+ current = {
33
+ operationId,
34
+ lines: [line],
35
+ isCurrent: stripped.startsWith("@"),
36
+ }
37
+ } else if (current && stripped.trim()) {
38
+ current.lines.push(line)
39
+ }
40
+ }
41
+
42
+ if (current) {
43
+ operations.push(current)
44
+ }
45
+
46
+ return operations
47
+ }
48
+
49
+ export async function jjNew(revision: string): Promise<OperationResult> {
50
+ const args = ["new", revision]
51
+ const result = await execute(args)
52
+ return {
53
+ ...result,
54
+ command: `jj ${args.join(" ")}`,
55
+ }
56
+ }
57
+
58
+ export async function jjEdit(revision: string): Promise<OperationResult> {
59
+ const args = ["edit", revision]
60
+ const result = await execute(args)
61
+ return {
62
+ ...result,
63
+ command: `jj ${args.join(" ")}`,
64
+ }
65
+ }
66
+
67
+ export async function jjSquash(
68
+ revision?: string,
69
+ options?: { ignoreImmutable?: boolean },
70
+ ): Promise<OperationResult> {
71
+ const args = revision ? ["squash", "-r", revision] : ["squash"]
72
+ if (options?.ignoreImmutable) {
73
+ args.push("--ignore-immutable")
74
+ }
75
+ const result = await execute(args)
76
+ return {
77
+ ...result,
78
+ command: `jj ${args.join(" ")}`,
79
+ }
80
+ }
81
+
82
+ export function isImmutableError(result: OperationResult): boolean {
83
+ return (
84
+ !result.success &&
85
+ (result.stderr.includes("immutable") || result.stderr.includes("Immutable"))
86
+ )
87
+ }
88
+
89
+ export async function jjDescribe(
90
+ revision: string,
91
+ message: string,
92
+ options?: { ignoreImmutable?: boolean },
93
+ ): Promise<OperationResult> {
94
+ const args = ["describe", revision, "-m", message]
95
+ if (options?.ignoreImmutable) {
96
+ args.push("--ignore-immutable")
97
+ }
98
+ const result = await execute(args)
99
+ return {
100
+ ...result,
101
+ command: `jj describe ${revision} -m "..."`,
102
+ }
103
+ }
104
+
105
+ export async function jjShowDescription(
106
+ revision: string,
107
+ ): Promise<{ subject: string; body: string }> {
108
+ const result = await execute([
109
+ "log",
110
+ "-r",
111
+ revision,
112
+ "--no-graph",
113
+ "-T",
114
+ "description",
115
+ ])
116
+
117
+ if (!result.success) {
118
+ return { subject: "", body: "" }
119
+ }
120
+
121
+ const description = result.stdout.trim()
122
+ const lines = description.split("\n")
123
+ const subject = lines[0] ?? ""
124
+ const body = lines.slice(1).join("\n").trim()
125
+
126
+ return { subject, body }
127
+ }
128
+
129
+ export async function jjShowDescriptionStyled(
130
+ revision: string,
131
+ ): Promise<{ subject: string; body: string }> {
132
+ const styledTemplate = `if(empty, label("empty", "(empty) "), "") ++ if(description.first_line(), description.first_line(), label("description placeholder", "(no description set)"))`
133
+ const subjectResult = await execute([
134
+ "log",
135
+ "-r",
136
+ revision,
137
+ "--no-graph",
138
+ "--color",
139
+ "always",
140
+ "-T",
141
+ styledTemplate,
142
+ ])
143
+
144
+ const bodyResult = await execute([
145
+ "log",
146
+ "-r",
147
+ revision,
148
+ "--no-graph",
149
+ "-T",
150
+ 'if(description.first_line(), description.rest(), "")',
151
+ ])
152
+
153
+ const subject = subjectResult.success ? subjectResult.stdout.trim() : ""
154
+ const body = bodyResult.success ? bodyResult.stdout.trim() : ""
155
+
156
+ return { subject, body }
157
+ }
158
+
159
+ export async function jjAbandon(revision: string): Promise<OperationResult> {
160
+ const args = ["abandon", revision]
161
+ const result = await execute(args)
162
+ return {
163
+ ...result,
164
+ command: `jj ${args.join(" ")}`,
165
+ }
166
+ }
167
+
168
+ export async function fetchOpLog(limit?: number): Promise<string[]> {
169
+ const args = ["op", "log", "--color", "always"]
170
+ if (limit) {
171
+ args.push("--limit", String(limit))
172
+ }
173
+ const result = await execute(args)
174
+ if (!result.success) {
175
+ throw new Error(`jj op log failed: ${result.stderr}`)
176
+ }
177
+ return result.stdout.split("\n")
178
+ }
179
+
180
+ export async function fetchOpLogId(): Promise<string> {
181
+ const result = await execute([
182
+ "op",
183
+ "log",
184
+ "--limit",
185
+ "1",
186
+ "--no-graph",
187
+ "-T",
188
+ "self.id()",
189
+ ])
190
+ return result.success ? result.stdout.trim() : ""
191
+ }
192
+
193
+ export async function jjUndo(): Promise<OperationResult> {
194
+ const args = ["undo"]
195
+ const result = await execute(args)
196
+ return {
197
+ ...result,
198
+ command: "jj undo",
199
+ }
200
+ }
201
+
202
+ export async function jjRedo(): Promise<OperationResult> {
203
+ const args = ["redo"]
204
+ const result = await execute(args)
205
+ return {
206
+ ...result,
207
+ command: "jj redo",
208
+ }
209
+ }
210
+
211
+ export async function jjOpRestore(
212
+ operationId: string,
213
+ ): Promise<OperationResult> {
214
+ const args = ["op", "restore", operationId]
215
+ const result = await execute(args)
216
+ return {
217
+ ...result,
218
+ command: `jj op restore ${operationId}`,
219
+ }
220
+ }
221
+
222
+ export async function jjGitFetch(options?: {
223
+ allRemotes?: boolean
224
+ }): Promise<OperationResult> {
225
+ const args = ["git", "fetch"]
226
+ if (options?.allRemotes) {
227
+ args.push("--all-remotes")
228
+ }
229
+ const result = await execute(args)
230
+ return {
231
+ ...result,
232
+ command: `jj ${args.join(" ")}`,
233
+ }
234
+ }
235
+
236
+ export async function jjGitPush(options?: {
237
+ all?: boolean
238
+ }): Promise<OperationResult> {
239
+ const args = ["git", "push"]
240
+ if (options?.all) {
241
+ args.push("--all")
242
+ }
243
+ const result = await execute(args)
244
+ return {
245
+ ...result,
246
+ command: `jj ${args.join(" ")}`,
247
+ }
248
+ }
249
+
250
+ export async function jjRestore(
251
+ paths: string[],
252
+ revision?: string,
253
+ ): Promise<OperationResult> {
254
+ const args = ["restore"]
255
+ if (revision) {
256
+ args.push("-r", revision)
257
+ }
258
+ args.push(...paths)
259
+ const result = await execute(args)
260
+ return {
261
+ ...result,
262
+ command: `jj ${args.join(" ")}`,
263
+ }
264
+ }
265
+
266
+ export interface DiffStats {
267
+ files: { path: string; insertions: number; deletions: number }[]
268
+ totalFiles: number
269
+ totalInsertions: number
270
+ totalDeletions: number
271
+ }
272
+
273
+ export async function jjDiffStats(revision: string): Promise<DiffStats> {
274
+ const result = await execute(["diff", "--stat", "-r", revision])
275
+
276
+ if (!result.success) {
277
+ return { files: [], totalFiles: 0, totalInsertions: 0, totalDeletions: 0 }
278
+ }
279
+
280
+ const lines = result.stdout.trim().split("\n")
281
+ const files: DiffStats["files"] = []
282
+ let totalFiles = 0
283
+ let totalInsertions = 0
284
+ let totalDeletions = 0
285
+
286
+ for (const line of lines) {
287
+ // Summary line: "14 files changed, 1448 insertions(+), 56 deletions(-)"
288
+ const summaryMatch = line.match(
289
+ /(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?/,
290
+ )
291
+ if (summaryMatch) {
292
+ totalFiles = Number.parseInt(summaryMatch[1] ?? "0", 10)
293
+ totalInsertions = summaryMatch[2]
294
+ ? Number.parseInt(summaryMatch[2], 10)
295
+ : 0
296
+ totalDeletions = summaryMatch[3]
297
+ ? Number.parseInt(summaryMatch[3], 10)
298
+ : 0
299
+ continue
300
+ }
301
+
302
+ // File line: "src/foo.ts | 12 ++++----"
303
+ const fileMatch = line.match(/^(.+?)\s+\|\s+(\d+)\s+([+-]*)/)
304
+ if (fileMatch) {
305
+ const path = (fileMatch[1] ?? "").trim()
306
+ const plusCount = (fileMatch[3]?.match(/\+/g) || []).length
307
+ const minusCount = (fileMatch[3]?.match(/-/g) || []).length
308
+ files.push({ path, insertions: plusCount, deletions: minusCount })
309
+ }
310
+ }
311
+
312
+ return { files, totalFiles, totalInsertions, totalDeletions }
313
+ }
@@ -0,0 +1,21 @@
1
+ export interface Commit {
2
+ changeId: string
3
+ commitId: string
4
+ description: string
5
+ author: string
6
+ authorEmail: string
7
+ timestamp: string
8
+ lines: string[]
9
+ isWorkingCopy: boolean
10
+ immutable: boolean
11
+ empty: boolean
12
+ }
13
+
14
+ export type FileStatus = "added" | "modified" | "deleted" | "renamed" | "copied"
15
+
16
+ export interface FileChange {
17
+ path: string
18
+ status: FileStatus
19
+ /** Original path for renamed/copied files */
20
+ oldPath?: string
21
+ }
@@ -0,0 +1,77 @@
1
+ import type { TerminalLine } from "ghostty-opentui"
2
+ import { ptyToJson } from "ghostty-opentui"
3
+ import { For, Show, createEffect, createMemo } from "solid-js"
4
+
5
+ const PROFILE = process.env.KAJJI_PROFILE === "1"
6
+
7
+ function profile(label: string) {
8
+ if (!PROFILE) return () => {}
9
+ const start = performance.now()
10
+ return (extra?: string) => {
11
+ const ms = (performance.now() - start).toFixed(2)
12
+ console.error(`[PROFILE] ${label}: ${ms}ms${extra ? ` (${extra})` : ""}`)
13
+ }
14
+ }
15
+
16
+ interface AnsiTextProps {
17
+ content: string
18
+ cols?: number
19
+ bold?: boolean
20
+ wrapMode?: "none" | "char" | "word"
21
+ maxLines?: number
22
+ onTotalLines?: (total: number) => void
23
+ }
24
+
25
+ export function AnsiText(props: AnsiTextProps) {
26
+ const allLines = createMemo(() => {
27
+ if (!props.content) return [] as TerminalLine[]
28
+ const endParse = profile("ptyToJson parse")
29
+ const result = ptyToJson(props.content, {
30
+ cols: props.cols ?? 9999,
31
+ rows: 1,
32
+ })
33
+ endParse(`${result.lines.length} lines from ${props.content.length} chars`)
34
+ return result.lines
35
+ })
36
+
37
+ createEffect(() => {
38
+ const total = allLines().length
39
+ props.onTotalLines?.(total)
40
+ })
41
+
42
+ const visibleLines = createMemo(() => {
43
+ const lines = allLines()
44
+ const limit = props.maxLines
45
+ if (limit !== undefined && lines.length > limit) {
46
+ return lines.slice(0, limit)
47
+ }
48
+ return lines
49
+ })
50
+
51
+ const renderSpans = (line: TerminalLine) => (
52
+ <For each={line.spans}>
53
+ {(span) => (
54
+ <span
55
+ style={{
56
+ fg: span.fg ?? undefined,
57
+ bg: span.bg ?? undefined,
58
+ }}
59
+ >
60
+ {span.text}
61
+ </span>
62
+ )}
63
+ </For>
64
+ )
65
+
66
+ return (
67
+ <For each={visibleLines()}>
68
+ {(line) => (
69
+ <text wrapMode={props.wrapMode ?? "word"}>
70
+ <Show when={props.bold} fallback={renderSpans(line)}>
71
+ <b>{renderSpans(line)}</b>
72
+ </Show>
73
+ </text>
74
+ )}
75
+ </For>
76
+ )
77
+ }
@@ -0,0 +1,124 @@
1
+ import type { BorderStyle } from "@opentui/core"
2
+ import type { JSX } from "solid-js"
3
+ import { Show, children as resolveChildren } from "solid-js"
4
+
5
+ type Dimension = number | "auto" | `${number}%`
6
+ type CornerContent = JSX.Element | string | (() => JSX.Element | string)
7
+
8
+ interface BorderBoxProps {
9
+ topLeft?: CornerContent
10
+ topRight?: CornerContent
11
+ bottomLeft?: CornerContent
12
+ bottomRight?: CornerContent
13
+
14
+ border?: boolean
15
+ borderStyle?: BorderStyle
16
+ borderColor?: string
17
+ backgroundColor?: string
18
+ flexGrow?: number
19
+ flexDirection?: "row" | "column"
20
+ width?: Dimension
21
+ height?: Dimension
22
+ padding?: number
23
+ paddingLeft?: number
24
+ paddingRight?: number
25
+ paddingTop?: number
26
+ paddingBottom?: number
27
+ gap?: number
28
+ overflow?: "hidden" | "visible"
29
+ onMouseDown?: () => void
30
+
31
+ children: JSX.Element
32
+ }
33
+
34
+ export function BorderBox(props: BorderBoxProps) {
35
+ const resolved = resolveChildren(() => props.children)
36
+
37
+ const hasOverlays = () =>
38
+ props.topLeft || props.topRight || props.bottomLeft || props.bottomRight
39
+
40
+ const resolveCorner = (content: CornerContent | undefined) =>
41
+ typeof content === "function" ? content() : content
42
+
43
+ const renderCorner = (
44
+ position: "topLeft" | "topRight" | "bottomLeft" | "bottomRight",
45
+ ) => {
46
+ const content = resolveCorner(props[position])
47
+ if (!content) return null
48
+
49
+ const isTop = position.startsWith("top")
50
+ const isLeft = position.endsWith("Left")
51
+
52
+ return (
53
+ <box
54
+ position="absolute"
55
+ top={isTop ? 0 : undefined}
56
+ bottom={!isTop ? 0 : undefined}
57
+ left={isLeft ? 1 : undefined}
58
+ right={!isLeft ? 1 : undefined}
59
+ zIndex={1}
60
+ >
61
+ {typeof content === "string" ? <text>{content}</text> : content}
62
+ </box>
63
+ )
64
+ }
65
+
66
+ if (!hasOverlays()) {
67
+ return (
68
+ <box
69
+ flexDirection={props.flexDirection ?? "column"}
70
+ flexGrow={props.flexGrow}
71
+ width={props.width}
72
+ height={props.height}
73
+ border={props.border}
74
+ borderStyle={props.borderStyle}
75
+ borderColor={props.borderColor}
76
+ backgroundColor={props.backgroundColor}
77
+ padding={props.padding}
78
+ paddingLeft={props.paddingLeft}
79
+ paddingRight={props.paddingRight}
80
+ paddingTop={props.paddingTop}
81
+ paddingBottom={props.paddingBottom}
82
+ gap={props.gap}
83
+ overflow={props.overflow}
84
+ onMouseDown={props.onMouseDown}
85
+ >
86
+ {resolved()}
87
+ </box>
88
+ )
89
+ }
90
+
91
+ return (
92
+ <box
93
+ position="relative"
94
+ flexDirection="column"
95
+ flexGrow={props.flexGrow}
96
+ width={props.width}
97
+ height={props.height}
98
+ onMouseDown={props.onMouseDown}
99
+ >
100
+ <Show when={props.topLeft}>{() => renderCorner("topLeft")}</Show>
101
+ <Show when={props.topRight}>{() => renderCorner("topRight")}</Show>
102
+ <Show when={props.bottomLeft}>{() => renderCorner("bottomLeft")}</Show>
103
+ <Show when={props.bottomRight}>{() => renderCorner("bottomRight")}</Show>
104
+
105
+ <box
106
+ flexDirection={props.flexDirection ?? "column"}
107
+ flexGrow={props.flexGrow}
108
+ border={props.border}
109
+ borderStyle={props.borderStyle}
110
+ borderColor={props.borderColor}
111
+ backgroundColor={props.backgroundColor}
112
+ padding={props.padding}
113
+ paddingLeft={props.paddingLeft}
114
+ paddingRight={props.paddingRight}
115
+ paddingTop={props.paddingTop}
116
+ paddingBottom={props.paddingBottom}
117
+ gap={props.gap}
118
+ overflow={props.overflow}
119
+ >
120
+ {resolved()}
121
+ </box>
122
+ </box>
123
+ )
124
+ }
@@ -0,0 +1,105 @@
1
+ import { For, Show } from "solid-js"
2
+ import { useFocus } from "../context/focus"
3
+ import { useTheme } from "../context/theme"
4
+ import { createDoubleClickDetector } from "../utils/double-click"
5
+ import type { FlatFileNode } from "../utils/file-tree"
6
+
7
+ const STATUS_CHARS: Record<string, string> = {
8
+ added: "A",
9
+ modified: "M",
10
+ deleted: "D",
11
+ renamed: "R",
12
+ copied: "C",
13
+ }
14
+
15
+ export interface FileTreeListProps {
16
+ files: () => FlatFileNode[]
17
+ selectedIndex: () => number
18
+ setSelectedIndex: (index: number) => void
19
+ collapsedPaths: () => Set<string>
20
+ toggleFolder: (path: string) => void
21
+ }
22
+
23
+ export function FileTreeList(props: FileTreeListProps) {
24
+ const focus = useFocus()
25
+ const { colors } = useTheme()
26
+
27
+ const statusColors = () => ({
28
+ added: colors().success,
29
+ modified: colors().warning,
30
+ deleted: colors().error,
31
+ renamed: colors().info,
32
+ copied: colors().info,
33
+ })
34
+
35
+ return (
36
+ <For each={props.files()}>
37
+ {(item, index) => {
38
+ const isSelected = () => index() === props.selectedIndex()
39
+ const node = item.node
40
+ const indent = " ".repeat(item.visualDepth)
41
+ const isCollapsed = props.collapsedPaths().has(node.path)
42
+
43
+ const icon = node.isDirectory ? (isCollapsed ? "▶" : "▼") : " "
44
+
45
+ const statusChar = node.status
46
+ ? (STATUS_CHARS[node.status] ?? " ")
47
+ : " "
48
+ const statusColor = node.status
49
+ ? (statusColors()[
50
+ node.status as keyof ReturnType<typeof statusColors>
51
+ ] ?? colors().text)
52
+ : colors().text
53
+
54
+ const handleDoubleClick = createDoubleClickDetector(() => {
55
+ if (node.isDirectory) {
56
+ props.toggleFolder(node.path)
57
+ } else {
58
+ focus.setPanel("detail")
59
+ }
60
+ })
61
+
62
+ const handleMouseDown = (e: { stopPropagation: () => void }) => {
63
+ e.stopPropagation()
64
+ props.setSelectedIndex(index())
65
+ if (node.isDirectory) {
66
+ props.toggleFolder(node.path)
67
+ } else {
68
+ handleDoubleClick()
69
+ }
70
+ }
71
+
72
+ return (
73
+ <box
74
+ backgroundColor={
75
+ isSelected() ? colors().selectionBackground : undefined
76
+ }
77
+ overflow="hidden"
78
+ onMouseDown={handleMouseDown}
79
+ >
80
+ <text>
81
+ <span style={{ fg: colors().textMuted }}>{indent}</span>
82
+ <span
83
+ style={{
84
+ fg: node.isDirectory ? colors().info : colors().textMuted,
85
+ }}
86
+ >
87
+ {icon}{" "}
88
+ </span>
89
+ <Show when={!node.isDirectory}>
90
+ <span style={{ fg: statusColor }}>{statusChar} </span>
91
+ </Show>
92
+ <span
93
+ style={{
94
+ fg: node.isDirectory ? colors().info : colors().text,
95
+ }}
96
+ >
97
+ {node.name}
98
+ </span>
99
+ </text>
100
+ </box>
101
+ )
102
+ }}
103
+ </For>
104
+ )
105
+ }
@@ -0,0 +1,48 @@
1
+ import type { JSX } from "solid-js"
2
+ import { useTheme } from "../context/theme"
3
+ import { StatusBar } from "./StatusBar"
4
+ import { CommandLogPanel } from "./panels/CommandLogPanel"
5
+
6
+ interface LayoutProps {
7
+ top: JSX.Element
8
+ bottom: JSX.Element
9
+ right: JSX.Element
10
+ }
11
+
12
+ export function Layout(props: LayoutProps) {
13
+ const { colors, style } = useTheme()
14
+
15
+ return (
16
+ <box
17
+ flexGrow={1}
18
+ flexDirection="column"
19
+ width="100%"
20
+ height="100%"
21
+ backgroundColor={colors().background}
22
+ padding={style().adaptToTerminal ? 0 : 1}
23
+ gap={0}
24
+ >
25
+ <box flexGrow={1} flexDirection="row" width="100%" gap={0}>
26
+ <box
27
+ flexGrow={1}
28
+ flexBasis={0}
29
+ height="100%"
30
+ flexDirection="column"
31
+ gap={0}
32
+ >
33
+ <box flexGrow={3} flexBasis={0}>
34
+ {props.top}
35
+ </box>
36
+ <box flexGrow={1} flexBasis={0}>
37
+ {props.bottom}
38
+ </box>
39
+ </box>
40
+ <box flexGrow={2} flexBasis={0} height="100%" flexDirection="column">
41
+ <box flexGrow={1}>{props.right}</box>
42
+ <CommandLogPanel />
43
+ </box>
44
+ </box>
45
+ <StatusBar />
46
+ </box>
47
+ )
48
+ }