Skip to content

Commit 4d845d1

Browse files
authored
feat: tab improvements and bug fixes (#162)
feat: tab improvements and bug fixes different ui for draggable and non draggable tabs use panelmessage static tabs and tab badges close, close others, close to the right functionality bg change swap tabs with cmd 1 to 9, close with cmd w ![image.png](https://app.graphite.com/user-attachments/assets/36d532f6-27c2-499f-b70e-caf17b3499d9.png) ![image.png](https://app.graphite.com/user-attachments/assets/ad75c4c3-c3b8-4eba-86a1-99d0e98e18a7.png)
1 parent 4d6c870 commit 4d845d1

36 files changed

+1184
-235
lines changed

apps/array/src/main/preload.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import type {
66
} from "../shared/types/oauth";
77
import type {
88
FolderContextMenuResult,
9+
SplitContextMenuResult,
10+
TabContextMenuResult,
911
TaskContextMenuResult,
1012
} from "./services/contextMenu.types.js";
1113

@@ -220,6 +222,13 @@ contextBridge.exposeInMainWorld("electronAPI", {
220222
ipcRenderer.invoke("get-changed-files-head", repoPath),
221223
getFileAtHead: (repoPath: string, filePath: string): Promise<string | null> =>
222224
ipcRenderer.invoke("get-file-at-head", repoPath, filePath),
225+
getDiffStats: (
226+
repoPath: string,
227+
): Promise<{
228+
filesChanged: number;
229+
linesAdded: number;
230+
linesRemoved: number;
231+
}> => ipcRenderer.invoke("get-diff-stats", repoPath),
223232
listDirectory: (
224233
dirPath: string,
225234
): Promise<
@@ -302,6 +311,8 @@ contextBridge.exposeInMainWorld("electronAPI", {
302311
ipcRenderer.invoke("shell:check", sessionId),
303312
shellDestroy: (sessionId: string): Promise<void> =>
304313
ipcRenderer.invoke("shell:destroy", sessionId),
314+
shellGetProcess: (sessionId: string): Promise<string | null> =>
315+
ipcRenderer.invoke("shell:get-process", sessionId),
305316
onShellData: (
306317
sessionId: string,
307318
listener: (data: string) => void,
@@ -328,6 +339,10 @@ contextBridge.exposeInMainWorld("electronAPI", {
328339
folderName: string,
329340
): Promise<FolderContextMenuResult> =>
330341
ipcRenderer.invoke("show-folder-context-menu", folderId, folderName),
342+
showTabContextMenu: (canClose: boolean): Promise<TabContextMenuResult> =>
343+
ipcRenderer.invoke("show-tab-context-menu", canClose),
344+
showSplitContextMenu: (): Promise<SplitContextMenuResult> =>
345+
ipcRenderer.invoke("show-split-context-menu"),
331346
folders: {
332347
getFolders: (): Promise<
333348
Array<{

apps/array/src/main/services/contextMenu.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@ import { Menu, type MenuItemConstructorOptions } from "electron";
22
import { createIpcService } from "../ipc/createIpcService.js";
33
import type {
44
FolderContextMenuResult,
5+
SplitContextMenuResult,
6+
TabContextMenuResult,
57
TaskContextMenuResult,
68
} from "./contextMenu.types.js";
79

810
export type {
911
FolderContextMenuAction,
1012
FolderContextMenuResult,
13+
SplitContextMenuResult,
14+
SplitDirection,
15+
TabContextMenuAction,
16+
TabContextMenuResult,
1117
TaskContextMenuAction,
1218
TaskContextMenuResult,
1319
} from "./contextMenu.types.js";
@@ -66,3 +72,64 @@ export const showFolderContextMenuService = createIpcService({
6672
});
6773
},
6874
});
75+
76+
export const showTabContextMenuService = createIpcService({
77+
channel: "show-tab-context-menu",
78+
handler: async (_event, canClose: boolean): Promise<TabContextMenuResult> => {
79+
return new Promise((resolve) => {
80+
const template: MenuItemConstructorOptions[] = [
81+
{
82+
label: "Close tab",
83+
accelerator: "CmdOrCtrl+W",
84+
enabled: canClose,
85+
click: () => resolve({ action: "close" }),
86+
},
87+
{ type: "separator" },
88+
{
89+
label: "Close other tabs",
90+
click: () => resolve({ action: "close-others" }),
91+
},
92+
{
93+
label: "Close tabs to the right",
94+
click: () => resolve({ action: "close-right" }),
95+
},
96+
];
97+
98+
const menu = Menu.buildFromTemplate(template);
99+
menu.popup({
100+
callback: () => resolve({ action: null }),
101+
});
102+
});
103+
},
104+
});
105+
106+
export const showSplitContextMenuService = createIpcService({
107+
channel: "show-split-context-menu",
108+
handler: async (_event): Promise<SplitContextMenuResult> => {
109+
return new Promise((resolve) => {
110+
const template: MenuItemConstructorOptions[] = [
111+
{
112+
label: "Split right",
113+
click: () => resolve({ direction: "right" }),
114+
},
115+
{
116+
label: "Split left",
117+
click: () => resolve({ direction: "left" }),
118+
},
119+
{
120+
label: "Split down",
121+
click: () => resolve({ direction: "down" }),
122+
},
123+
{
124+
label: "Split up",
125+
click: () => resolve({ direction: "up" }),
126+
},
127+
];
128+
129+
const menu = Menu.buildFromTemplate(template);
130+
menu.popup({
131+
callback: () => resolve({ direction: null }),
132+
});
133+
});
134+
},
135+
});

apps/array/src/main/services/contextMenu.types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@ export type TaskContextMenuAction = "rename" | "duplicate" | "delete" | null;
22

33
export type FolderContextMenuAction = "remove" | null;
44

5+
export type TabContextMenuAction =
6+
| "close"
7+
| "close-others"
8+
| "close-right"
9+
| null;
10+
11+
export type SplitDirection = "left" | "right" | "up" | "down" | null;
12+
513
export interface TaskContextMenuResult {
614
action: TaskContextMenuAction;
715
}
@@ -10,6 +18,14 @@ export interface FolderContextMenuResult {
1018
action: FolderContextMenuAction;
1119
}
1220

21+
export interface TabContextMenuResult {
22+
action: TabContextMenuAction;
23+
}
24+
25+
export interface SplitContextMenuResult {
26+
direction: SplitDirection;
27+
}
28+
1329
declare global {
1430
interface IElectronAPI {
1531
showTaskContextMenu: (

apps/array/src/main/services/git.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,64 @@ const getFileAtHead = async (
242242
}
243243
};
244244

245+
export interface DiffStats {
246+
filesChanged: number;
247+
linesAdded: number;
248+
linesRemoved: number;
249+
}
250+
251+
const getDiffStats = async (directoryPath: string): Promise<DiffStats> => {
252+
try {
253+
// git diff --numstat HEAD shows: added\tremoved\tfilename
254+
const { stdout } = await execAsync("git diff --numstat HEAD", {
255+
cwd: directoryPath,
256+
});
257+
258+
let linesAdded = 0;
259+
let linesRemoved = 0;
260+
let filesChanged = 0;
261+
262+
for (const line of stdout.trim().split("\n").filter(Boolean)) {
263+
const parts = line.split("\t");
264+
if (parts.length >= 2) {
265+
// Binary files show "-" for added/removed
266+
const added = parts[0] === "-" ? 0 : parseInt(parts[0], 10);
267+
const removed = parts[1] === "-" ? 0 : parseInt(parts[1], 10);
268+
linesAdded += added;
269+
linesRemoved += removed;
270+
filesChanged++;
271+
}
272+
}
273+
274+
// Also count untracked files
275+
const { stdout: statusOutput } = await execAsync("git status --porcelain", {
276+
cwd: directoryPath,
277+
});
278+
279+
for (const line of statusOutput.trim().split("\n").filter(Boolean)) {
280+
const statusCode = line.substring(0, 2);
281+
if (statusCode === "??") {
282+
filesChanged++;
283+
// Count lines in untracked file
284+
const filePath = line.substring(3);
285+
try {
286+
const { stdout: wcOutput } = await execAsync(
287+
`wc -l < "${filePath}"`,
288+
{ cwd: directoryPath },
289+
);
290+
linesAdded += parseInt(wcOutput.trim(), 10) || 0;
291+
} catch {
292+
// File might be binary or inaccessible
293+
}
294+
}
295+
}
296+
297+
return { filesChanged, linesAdded, linesRemoved };
298+
} catch {
299+
return { filesChanged: 0, linesAdded: 0, linesRemoved: 0 };
300+
}
301+
};
302+
245303
export const findReposDirectory = async (): Promise<string | null> => {
246304
const platform = os.platform();
247305

@@ -548,4 +606,14 @@ export function registerGitIpc(
548606
return getFileAtHead(directoryPath, filePath);
549607
},
550608
);
609+
610+
ipcMain.handle(
611+
"get-diff-stats",
612+
async (
613+
_event: IpcMainInvokeEvent,
614+
directoryPath: string,
615+
): Promise<DiffStats> => {
616+
return getDiffStats(directoryPath);
617+
},
618+
);
551619
}

apps/array/src/main/services/shell.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,19 @@ export function registerShellIpc(): void {
163163
sessions.delete(sessionId);
164164
},
165165
);
166+
167+
// Get foreground process name
168+
ipcMain.handle(
169+
"shell:get-process",
170+
async (
171+
_event: IpcMainInvokeEvent,
172+
sessionId: string,
173+
): Promise<string | null> => {
174+
const session = sessions.get(sessionId);
175+
if (!session) {
176+
return null;
177+
}
178+
return session.pty.process;
179+
},
180+
);
166181
}

apps/array/src/renderer/components/MainLayout.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { MainSidebar } from "@components/MainSidebar";
22
import { StatusBar } from "@components/StatusBar";
33
import { UpdatePrompt } from "@components/UpdatePrompt";
4-
import { TopBar } from "@components/ui/topnav/TopBar";
54
import { CommandMenu } from "@features/command/components/CommandMenu";
65
import { usePanelLayoutStore } from "@features/panels/store/panelLayoutStore";
76
import { SettingsView } from "@features/settings/components/SettingsView";
@@ -82,7 +81,6 @@ export function MainLayout() {
8281

8382
return (
8483
<Flex direction="column" height="100vh">
85-
<TopBar onSearchClick={() => setCommandMenuOpen(true)} />
8684
<Flex flexGrow="1" overflow="hidden">
8785
<MainSidebar />
8886

apps/array/src/renderer/components/ui/sidebar/Sidebar.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { SIDEBAR_BORDER } from "@components/ui/sidebar/Context";
2+
import { SidebarTrigger } from "@components/ui/sidebar/SidebarTrigger";
23
import { Box, Flex } from "@radix-ui/themes";
34
import { useSidebarStore } from "@stores/sidebarStore";
45
import React from "react";
@@ -65,6 +66,18 @@ export const Sidebar: React.FC<{ children: React.ReactNode }> = ({
6566
height: "100%",
6667
}}
6768
>
69+
<Flex
70+
align="center"
71+
className="drag"
72+
px="2"
73+
style={{
74+
height: "40px",
75+
minHeight: "40px",
76+
borderBottom: SIDEBAR_BORDER,
77+
}}
78+
>
79+
<SidebarTrigger />
80+
</Flex>
6881
{children}
6982
</Flex>
7083
{open && (

apps/array/src/renderer/components/ui/sidebar/SidebarTreeItem.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,10 @@ export function SidebarTreeItem({
7171
)}
7272
{line.tooltip ? (
7373
<Tooltip content={line.tooltip}>
74-
<span style={{ color: line.customColor, fontSize: "13px" }}>
75-
{line.label}
76-
</span>
74+
<span style={{ color: line.customColor }}>{line.label}</span>
7775
</Tooltip>
7876
) : (
79-
<span style={{ color: line.customColor, fontSize: "13px" }}>
80-
{line.label}
81-
</span>
77+
<span style={{ color: line.customColor }}>{line.label}</span>
8278
)}
8379
{line.hasChildren && (
8480
<span style={{ display: "flex", alignItems: "center" }}>

apps/array/src/renderer/components/ui/topnav/TopBar.tsx

Lines changed: 0 additions & 47 deletions
This file was deleted.

apps/array/src/renderer/features/code-editor/components/DiffEditorPanel.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function DiffEditorPanel({
2121

2222
const { data: changedFiles = [] } = useQuery({
2323
queryKey: ["changed-files-head", repoPath],
24-
queryFn: () => window.electronAPI.getChangedFilesHead(repoPath!),
24+
queryFn: () => window.electronAPI.getChangedFilesHead(repoPath as string),
2525
enabled: !!repoPath,
2626
staleTime: Infinity,
2727
});
@@ -34,14 +34,16 @@ export function DiffEditorPanel({
3434

3535
const { data: modifiedContent, isLoading: loadingModified } = useQuery({
3636
queryKey: ["repo-file", repoPath, filePath],
37-
queryFn: () => window.electronAPI.readRepoFile(repoPath!, filePath),
37+
queryFn: () =>
38+
window.electronAPI.readRepoFile(repoPath as string, filePath),
3839
enabled: !!repoPath && !isDeleted,
3940
staleTime: Infinity,
4041
});
4142

4243
const { data: originalContent, isLoading: loadingOriginal } = useQuery({
4344
queryKey: ["file-at-head", repoPath, originalPath],
44-
queryFn: () => window.electronAPI.getFileAtHead(repoPath!, originalPath),
45+
queryFn: () =>
46+
window.electronAPI.getFileAtHead(repoPath as string, originalPath),
4547
enabled: !!repoPath && !isNew,
4648
staleTime: Infinity,
4749
});

0 commit comments

Comments
 (0)