Skip to content

Commit 9df116a

Browse files
authored
feat: Add repository button + move settings to left sidebar (#302)
1 parent 512541f commit 9df116a

File tree

8 files changed

+176
-102
lines changed

8 files changed

+176
-102
lines changed

apps/array/src/main/services/dock-badge/service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export class DockBadgeService {
1919

2020
this.hasBadge = true;
2121
if (process.platform === "darwin" || process.platform === "linux") {
22-
app.setBadgeCount(1);
22+
app.dock.setBadge("•");
2323
}
2424
log.info("Dock badge shown");
2525
}
@@ -29,7 +29,7 @@ export class DockBadgeService {
2929

3030
this.hasBadge = false;
3131
if (process.platform === "darwin" || process.platform === "linux") {
32-
app.setBadgeCount(0);
32+
app.dock.setBadge("");
3333
}
3434
log.info("Dock badge cleared");
3535
}

apps/array/src/main/services/folders/schemas.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const registeredFolderSchema = z.object({
1111
export const getFoldersOutput = z.array(registeredFolderSchema);
1212

1313
export const addFolderInput = z.object({
14-
folderPath: z.string(),
14+
folderPath: z.string().min(2, "Folder path must be a valid directory path"),
1515
});
1616

1717
export const addFolderOutput = registeredFolderSchema;

apps/array/src/main/services/folders/service.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,20 @@ const log = logger.scope("folders-service");
2121
@injectable()
2222
export class FoldersService {
2323
async getFolders(): Promise<RegisteredFolder[]> {
24-
return foldersStore.get("folders", []);
24+
const folders = foldersStore.get("folders", []);
25+
// Filter out any folders with empty names (from invalid paths like "/")
26+
return folders.filter((f) => f.name && f.path);
2527
}
2628

2729
async addFolder(folderPath: string): Promise<RegisteredFolder> {
30+
// Validate the path before proceeding
31+
const folderName = path.basename(folderPath);
32+
if (!folderPath || !folderName) {
33+
throw new Error(
34+
`Invalid folder path: "${folderPath}" - path must have a valid directory name`,
35+
);
36+
}
37+
2838
const isRepo = await isGitRepository(folderPath);
2939

3040
if (!isRepo) {
@@ -65,7 +75,7 @@ export class FoldersService {
6575
const newFolder: RegisteredFolder = {
6676
id: generateId("folder", 7),
6777
path: folderPath,
68-
name: path.basename(folderPath),
78+
name: folderName,
6979
lastAccessed: new Date().toISOString(),
7080
createdAt: new Date().toISOString(),
7181
};

apps/array/src/main/services/workspace/schemas.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,11 @@ export const scriptExecutionResultSchema = z.object({
5151
// Input schemas
5252
export const createWorkspaceInput = z.object({
5353
taskId: z.string(),
54-
mainRepoPath: z.string(),
54+
mainRepoPath: z
55+
.string()
56+
.min(2, "Repository path must be a valid directory path"),
5557
folderId: z.string(),
56-
folderPath: z.string(),
58+
folderPath: z.string().min(2, "Folder path must be a valid directory path"),
5759
mode: workspaceModeSchema,
5860
branch: z.string().optional(),
5961
});

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

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { StatusBarMenu } from "@components/StatusBarMenu";
2-
import { GearIcon } from "@radix-ui/react-icons";
3-
import { Badge, Box, Code, Flex, IconButton, Kbd } from "@radix-ui/themes";
4-
import { useNavigationStore } from "@stores/navigationStore";
2+
import { Badge, Box, Code, Flex, Kbd } from "@radix-ui/themes";
53
import { useStatusBarStore } from "@stores/statusBarStore";
64

75
import { IS_DEV } from "@/constants/environment";
@@ -12,7 +10,6 @@ interface StatusBarProps {
1210

1311
export function StatusBar({ showKeyHints = true }: StatusBarProps) {
1412
const { statusText, keyHints } = useStatusBarStore();
15-
const { toggleSettings } = useNavigationStore();
1613

1714
return (
1815
<Box className="flex flex-row items-center justify-between border-gray-6 border-t bg-gray-2 px-4 py-2">
@@ -46,15 +43,6 @@ export function StatusBar({ showKeyHints = true }: StatusBarProps) {
4643
)}
4744

4845
<Flex align="center" gap="2">
49-
<IconButton
50-
size="1"
51-
variant="ghost"
52-
color="gray"
53-
onClick={toggleSettings}
54-
title="Settings"
55-
>
56-
<GearIcon />
57-
</IconButton>
5846
{IS_DEV && (
5947
<Badge size="1">
6048
<Code size="1" variant="ghost">
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Plus } from "@phosphor-icons/react";
2+
import { GearIcon } from "@radix-ui/react-icons";
3+
import { Box, Button, Flex, IconButton } from "@radix-ui/themes";
4+
import { useRegisteredFoldersStore } from "@renderer/stores/registeredFoldersStore";
5+
import { trpcVanilla } from "@renderer/trpc";
6+
import { useNavigationStore } from "@stores/navigationStore";
7+
import { useCallback } from "react";
8+
9+
export function SidebarFooter() {
10+
const addFolder = useRegisteredFoldersStore((state) => state.addFolder);
11+
const { toggleSettings } = useNavigationStore();
12+
13+
const handleAddRepository = useCallback(async () => {
14+
const selectedPath = await trpcVanilla.os.selectDirectory.query();
15+
if (selectedPath) {
16+
await addFolder(selectedPath);
17+
}
18+
}, [addFolder]);
19+
20+
return (
21+
<Box
22+
style={{
23+
position: "absolute",
24+
bottom: 0,
25+
left: 0,
26+
right: 0,
27+
borderTop: "1px solid var(--gray-6)",
28+
background: "var(--color-background)",
29+
padding: "12px",
30+
}}
31+
>
32+
<Flex align="center" gap="2" justify="between">
33+
<Button
34+
size="1"
35+
variant="ghost"
36+
color="gray"
37+
onClick={handleAddRepository}
38+
>
39+
<Plus size={14} weight="bold" />
40+
Add repository
41+
</Button>
42+
43+
<IconButton
44+
size="1"
45+
variant="ghost"
46+
color="gray"
47+
onClick={toggleSettings}
48+
title="Settings"
49+
>
50+
<GearIcon />
51+
</IconButton>
52+
</Flex>
53+
</Box>
54+
);
55+
}

apps/array/src/renderer/features/sidebar/components/SidebarMenu.tsx

Lines changed: 85 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { useTaskViewedStore } from "../stores/taskViewedStore";
2020
import { HomeItem } from "./items/HomeItem";
2121
import { NewTaskItem } from "./items/NewTaskItem";
2222
import { TaskItem } from "./items/TaskItem";
23+
import { SidebarFooter } from "./SidebarFooter";
2324
import { SortableFolderSection } from "./SortableFolderSection";
2425

2526
function SidebarMenuComponent() {
@@ -143,85 +144,91 @@ function SidebarMenuComponent() {
143144
onOpenChange={setRenameDialogOpen}
144145
/>
145146

146-
<Box
147-
style={{
148-
flexGrow: 1,
149-
overflowY: "auto",
150-
overflowX: "hidden",
151-
}}
152-
>
153-
<Flex direction="column" py="2">
154-
<HomeItem
155-
isActive={sidebarData.isHomeActive}
156-
onClick={handleHomeClick}
157-
/>
158-
159-
<DragDropProvider
160-
onDragOver={handleDragOver}
161-
sensors={[
162-
PointerSensor.configure({
163-
activationConstraints: {
164-
distance: { value: 5 },
165-
},
166-
}),
167-
]}
168-
>
169-
{sidebarData.folders.map((folder, index) => {
170-
const isExpanded = !collapsedSections.has(folder.id);
171-
return (
172-
<SortableFolderSection
173-
key={folder.id}
174-
id={folder.id}
175-
index={index}
176-
label={folder.name}
177-
icon={
178-
isExpanded ? (
179-
<FolderOpenIcon size={14} weight="regular" />
180-
) : (
181-
<FolderIcon size={14} weight="regular" />
182-
)
183-
}
184-
isExpanded={isExpanded}
185-
onToggle={() => toggleSection(folder.id)}
186-
onContextMenu={(e) => handleFolderContextMenu(folder.id, e)}
187-
>
188-
<NewTaskItem onClick={() => handleFolderNewTask(folder.id)} />
189-
{folder.tasks.map((task) => (
190-
<TaskItem
191-
key={task.id}
192-
id={task.id}
193-
label={task.title}
194-
isActive={sidebarData.activeTaskId === task.id}
195-
worktreeName={
196-
workspaces[task.id]?.worktreeName ?? undefined
197-
}
198-
worktreePath={
199-
workspaces[task.id]?.worktreePath ??
200-
workspaces[task.id]?.folderPath
201-
}
202-
workspaceMode={taskStates[task.id]?.workspaceMode}
203-
lastActivityAt={task.lastActivityAt}
204-
isGenerating={task.isGenerating}
205-
isUnread={task.isUnread}
206-
onClick={() => handleTaskClick(task.id)}
207-
onContextMenu={(e) => handleTaskContextMenu(task.id, e)}
147+
<Box height="100%" position="relative">
148+
<Box
149+
style={{
150+
height: "100%",
151+
overflowY: "auto",
152+
overflowX: "hidden",
153+
paddingBottom: "52px",
154+
}}
155+
>
156+
<Flex direction="column" py="2">
157+
<HomeItem
158+
isActive={sidebarData.isHomeActive}
159+
onClick={handleHomeClick}
160+
/>
161+
162+
<DragDropProvider
163+
onDragOver={handleDragOver}
164+
sensors={[
165+
PointerSensor.configure({
166+
activationConstraints: {
167+
distance: { value: 5 },
168+
},
169+
}),
170+
]}
171+
>
172+
{sidebarData.folders.map((folder, index) => {
173+
const isExpanded = !collapsedSections.has(folder.id);
174+
return (
175+
<SortableFolderSection
176+
key={folder.id}
177+
id={folder.id}
178+
index={index}
179+
label={folder.name}
180+
icon={
181+
isExpanded ? (
182+
<FolderOpenIcon size={14} weight="regular" />
183+
) : (
184+
<FolderIcon size={14} weight="regular" />
185+
)
186+
}
187+
isExpanded={isExpanded}
188+
onToggle={() => toggleSection(folder.id)}
189+
onContextMenu={(e) => handleFolderContextMenu(folder.id, e)}
190+
>
191+
<NewTaskItem
192+
onClick={() => handleFolderNewTask(folder.id)}
208193
/>
209-
))}
210-
</SortableFolderSection>
211-
);
212-
})}
213-
<DragOverlay>
214-
{(source) =>
215-
source?.type === "folder" ? (
216-
<div className="flex w-full items-center gap-1 rounded bg-gray-2 px-2 py-1 font-mono text-[12px] text-gray-11 shadow-lg">
217-
<FolderIcon size={14} weight="regular" />
218-
<span className="font-medium">{source.data?.label}</span>
219-
</div>
220-
) : null
221-
}
222-
</DragOverlay>
223-
</DragDropProvider>
224-
</Flex>
194+
{folder.tasks.map((task) => (
195+
<TaskItem
196+
key={task.id}
197+
id={task.id}
198+
label={task.title}
199+
isActive={sidebarData.activeTaskId === task.id}
200+
worktreeName={
201+
workspaces[task.id]?.worktreeName ?? undefined
202+
}
203+
worktreePath={
204+
workspaces[task.id]?.worktreePath ??
205+
workspaces[task.id]?.folderPath
206+
}
207+
workspaceMode={taskStates[task.id]?.workspaceMode}
208+
lastActivityAt={task.lastActivityAt}
209+
isGenerating={task.isGenerating}
210+
isUnread={task.isUnread}
211+
onClick={() => handleTaskClick(task.id)}
212+
onContextMenu={(e) => handleTaskContextMenu(task.id, e)}
213+
/>
214+
))}
215+
</SortableFolderSection>
216+
);
217+
})}
218+
<DragOverlay>
219+
{(source) =>
220+
source?.type === "folder" ? (
221+
<div className="flex w-full items-center gap-1 rounded bg-gray-2 px-2 py-1 font-mono text-[12px] text-gray-11 shadow-lg">
222+
<FolderIcon size={14} weight="regular" />
223+
<span className="font-medium">{source.data?.label}</span>
224+
</div>
225+
) : null
226+
}
227+
</DragOverlay>
228+
</DragDropProvider>
229+
</Flex>
230+
</Box>
231+
<SidebarFooter />
225232
</Box>
226233
</>
227234
);

apps/array/src/renderer/features/task-detail/components/TaskLogsPanel.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,17 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) {
4040
const events = session?.events ?? [];
4141
const isPromptPending = session?.isPromptPending ?? false;
4242

43-
const hasAttemptedConnect = useRef(false);
43+
const isConnecting = useRef(false);
44+
4445
useEffect(() => {
45-
if (hasAttemptedConnect.current) return;
4646
if (!repoPath) return;
47-
if (session) return;
47+
if (isConnecting.current) return;
48+
49+
if (session?.status === "connected" || session?.status === "connecting") {
50+
return;
51+
}
4852

49-
hasAttemptedConnect.current = true;
53+
isConnecting.current = true;
5054

5155
const isNewSession = !task.latest_run?.id;
5256
const hasInitialPrompt = isNewSession && task.description;
@@ -55,12 +59,20 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) {
5559
markActivity(task.id);
5660
}
5761

62+
log.info("Connecting to task session", {
63+
taskId: task.id,
64+
hasLatestRun: !!task.latest_run,
65+
sessionStatus: session?.status ?? "none",
66+
});
67+
5868
connectToTask({
5969
task,
6070
repoPath,
6171
initialPrompt: hasInitialPrompt
6272
? [{ type: "text", text: task.description }]
6373
: undefined,
74+
}).finally(() => {
75+
isConnecting.current = false;
6476
});
6577
}, [task, repoPath, session, connectToTask, markActivity]);
6678

0 commit comments

Comments
 (0)