Skip to content

Commit 18eeb61

Browse files
authored
feat: suggested tasks (#157)
1 parent 5710794 commit 18eeb61

File tree

7 files changed

+336
-10
lines changed

7 files changed

+336
-10
lines changed

apps/array/src/renderer/features/folder-picker/components/FolderPicker.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,14 @@ const MAX_RECENT_ITEMS = 5;
3232
const SEARCH_DEBOUNCE_MS = 100;
3333
const MAX_LIST_HEIGHT = "300px";
3434

35+
const displayRepoName = (path: string): string => {
36+
// Extract just the last segment (repository/directory name)
37+
const segments = path.split("/");
38+
return segments[segments.length - 1] || path;
39+
};
40+
3541
const displayPath = (path: string): string => {
42+
// Show full path with ~ for home directory
3643
const homePattern = /^\/Users\/[^/]+|^\/home\/[^/]+/;
3744
const match = path.match(homePattern);
3845
return match ? path.replace(match[0], "~") : path;
@@ -54,7 +61,7 @@ export function FolderPicker({
5461

5562
const { recentDirectories, addRecentDirectory } = useFolderPickerStore();
5663

57-
const displayValue = value ? displayPath(value) : placeholder;
64+
const displayValue = value ? displayRepoName(value) : placeholder;
5865
const totalItems = recentPreview.length + directoryPreview.length;
5966

6067
useHotkeys(
@@ -165,7 +172,7 @@ export function FolderPicker({
165172
onSelect={() => handleSelect(path)}
166173
>
167174
<Text
168-
size="2"
175+
size="1"
169176
style={{
170177
overflow: "hidden",
171178
textOverflow: "ellipsis",
@@ -253,7 +260,7 @@ export function FolderPicker({
253260
)}
254261

255262
{recentPreview.length > 0 && (
256-
<Command.Group heading="Recent Directories">
263+
<Command.Group>
257264
{recentPreview.map((path, idx) => renderItem(path, idx))}
258265
</Command.Group>
259266
)}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { ArrowsClockwiseIcon } from "@phosphor-icons/react";
2+
import { Box, Flex, IconButton, Text } from "@radix-ui/themes";
3+
import type { Editor } from "@tiptap/react";
4+
import { useSuggestedTasksStore } from "../stores/suggestedTasksStore";
5+
6+
interface SuggestedTasksProps {
7+
editor: Editor | null;
8+
}
9+
10+
export function SuggestedTasks({ editor }: SuggestedTasksProps) {
11+
const suggestions = useSuggestedTasksStore((state) => state.getSuggestions());
12+
const rotateSuggestions = useSuggestedTasksStore(
13+
(state) => state.rotateSuggestions,
14+
);
15+
const incrementUsage = useSuggestedTasksStore(
16+
(state) => state.incrementUsage,
17+
);
18+
19+
const handleSuggestionClick = (suggestionTitle: string, prompt: string) => {
20+
if (!editor) return;
21+
22+
incrementUsage(suggestionTitle);
23+
editor.commands.setContent(prompt);
24+
editor.commands.focus("end");
25+
};
26+
27+
if (suggestions.length === 0) return null;
28+
29+
return (
30+
<Box mt="3">
31+
<Flex align="center" justify="between" mb="2">
32+
<Text size="1" color="gray" weight="medium">
33+
Suggested tasks
34+
</Text>
35+
<IconButton
36+
size="1"
37+
variant="ghost"
38+
onClick={rotateSuggestions}
39+
title="Show different suggestions"
40+
>
41+
<ArrowsClockwiseIcon size={14} />
42+
</IconButton>
43+
</Flex>
44+
45+
<Flex direction="column" gap="2">
46+
{suggestions.map((suggestion, index) => {
47+
const IconComponent = suggestion.icon;
48+
return (
49+
<button
50+
type="button"
51+
key={`${suggestion.title}-${index}`}
52+
onClick={() =>
53+
handleSuggestionClick(suggestion.title, suggestion.prompt)
54+
}
55+
className="group relative flex cursor-pointer items-start gap-2 rounded border border-gray-6 bg-gray-2 p-2 text-left transition-colors hover:border-orange-6 hover:bg-accent-2"
56+
>
57+
<Flex direction="column" gap="1" style={{ flex: 1 }}>
58+
<Text size="1" weight="medium" className="text-gray-12">
59+
{suggestion.title}
60+
</Text>
61+
<Text size="1" color="gray" className="leading-snug">
62+
{suggestion.description}
63+
</Text>
64+
</Flex>
65+
<IconComponent
66+
size={18}
67+
className="text-gray-9 group-hover:text-accent-9"
68+
/>
69+
</button>
70+
);
71+
})}
72+
</Flex>
73+
</Box>
74+
);
75+
}

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

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ import { useTaskDirectoryStore } from "@stores/taskDirectoryStore";
55
import { useCallback, useState } from "react";
66
import { useEditorSetup } from "../hooks/useEditorSetup";
77
import { useTaskCreation } from "../hooks/useTaskCreation";
8+
import { SuggestedTasks } from "./SuggestedTasks";
89
import { TaskInputEditor } from "./TaskInputEditor";
910

11+
const DOT_FILL = "var(--gray-6)";
12+
1013
export function TaskInput() {
1114
const { lastUsedDirectory } = useTaskDirectoryStore();
1215
const [selectedDirectory, setSelectedDirectory] = useState(
@@ -51,11 +54,53 @@ export function TaskInput() {
5154
});
5255

5356
return (
54-
<Flex align="center" justify="center" height="100%">
57+
<Flex
58+
align="center"
59+
justify="center"
60+
height="100%"
61+
style={{ position: "relative" }}
62+
>
63+
<svg
64+
aria-hidden="true"
65+
style={{
66+
position: "absolute",
67+
bottom: 0,
68+
left: 0,
69+
width: "100%",
70+
height: "100.333%",
71+
pointerEvents: "none",
72+
opacity: 0.4,
73+
maskImage: "linear-gradient(to top, black 0%, transparent 100%)",
74+
WebkitMaskImage:
75+
"linear-gradient(to top, black 0%, transparent 100%)",
76+
}}
77+
>
78+
<defs>
79+
<pattern
80+
id="dot-pattern"
81+
patternUnits="userSpaceOnUse"
82+
width="8"
83+
height="8"
84+
>
85+
<circle cx="0" cy="0" r="1" fill={DOT_FILL} />
86+
<circle cx="0" cy="8" r="1" fill={DOT_FILL} />
87+
<circle cx="8" cy="8" r="1" fill={DOT_FILL} />
88+
<circle cx="8" cy="0" r="1" fill={DOT_FILL} />
89+
<circle cx="4" cy="4" r="1" fill={DOT_FILL} />
90+
</pattern>
91+
</defs>
92+
<rect width="100%" height="100%" fill="url(#dot-pattern)" />
93+
</svg>
5594
<Flex
5695
direction="column"
5796
gap="4"
58-
style={{ fontFamily: "monospace", width: "100%", maxWidth: "600px" }}
97+
style={{
98+
fontFamily: "monospace",
99+
width: "100%",
100+
maxWidth: "600px",
101+
position: "relative",
102+
zIndex: 1,
103+
}}
59104
>
60105
<Box style={{ width: "50%" }}>
61106
<FolderPicker
@@ -79,6 +124,8 @@ export function TaskInput() {
79124
Create task
80125
</Button>
81126
</Flex>
127+
128+
<SuggestedTasks editor={editor} />
82129
</Flex>
83130
</Flex>
84131
);

apps/array/src/renderer/features/task-detail/hooks/useTaskData.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ export function useTaskData({ taskId, initialTask }: UseTaskDataParams) {
3939
}
4040

4141
// Fall back to deriving from workspace + repository (legacy behavior)
42-
if (!task.repository_config || !defaultWorkspace) return null;
42+
if (
43+
!task.repository_config ||
44+
!defaultWorkspace ||
45+
!task.repository_config.repository
46+
)
47+
return null;
4348
const expandedWorkspace = expandTildePath(defaultWorkspace);
4449
return `${expandedWorkspace}/${task.repository_config.repository}`;
4550
}, [taskState.repoPath, task.repository_config, defaultWorkspace]);
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import type { Icon } from "@phosphor-icons/react";
2+
import {
3+
BugIcon,
4+
CodeIcon,
5+
FlaskIcon,
6+
LightbulbIcon,
7+
ListChecksIcon,
8+
MagnifyingGlassIcon,
9+
PencilIcon,
10+
SparkleIcon,
11+
} from "@phosphor-icons/react";
12+
import { create } from "zustand";
13+
import { persist } from "zustand/middleware";
14+
15+
export interface Suggestion {
16+
title: string;
17+
description: string;
18+
prompt: string;
19+
icon: Icon;
20+
}
21+
22+
export const ALL_SUGGESTIONS: Suggestion[] = [
23+
{
24+
title: "Fix a small todo",
25+
description: "Search for a todo comment and implement it",
26+
prompt:
27+
"Search the codebase for TODO comments, select one that seems straightforward to implement, and complete the task.",
28+
icon: BugIcon,
29+
},
30+
{
31+
title: "Remove dead code",
32+
description: "Clean up unused functions and imports",
33+
prompt:
34+
"Identify and remove unused functions, variables, imports, or files that are no longer referenced in the codebase.",
35+
icon: CodeIcon,
36+
},
37+
{
38+
title: "Add missing tests",
39+
description: "Write tests for uncovered functionality",
40+
prompt:
41+
"Identify functions or modules that lack test coverage and write tests for them.",
42+
icon: FlaskIcon,
43+
},
44+
{
45+
title: "Improve error handling",
46+
description: "Add robust error handling where missing",
47+
prompt:
48+
"Find areas of the codebase that lack proper error handling and implement appropriate error catching and messaging.",
49+
icon: MagnifyingGlassIcon,
50+
},
51+
{
52+
title: "Consolidate duplicate logic",
53+
description: "Extract repeated code into reusable functions",
54+
prompt:
55+
"Scan for code duplication and refactor repeated logic into shared utility functions or modules.",
56+
icon: SparkleIcon,
57+
},
58+
{
59+
title: "Update inline documentation",
60+
description: "Add or improve code comments and docstrings",
61+
prompt:
62+
"Review the codebase and add missing documentation, improve unclear comments, and ensure complex logic is well-explained.",
63+
icon: PencilIcon,
64+
},
65+
{
66+
title: "Optimize slow operations",
67+
description: "Improve performance of inefficient code",
68+
prompt:
69+
"Identify performance bottlenecks such as inefficient loops, heavy operations, or resource-intensive processes and optimize them.",
70+
icon: LightbulbIcon,
71+
},
72+
{
73+
title: "Standardize naming conventions",
74+
description: "Make variable and function names consistent",
75+
prompt:
76+
"Review naming patterns across the codebase and update inconsistent variable, function, or file names to follow a unified convention.",
77+
icon: ListChecksIcon,
78+
},
79+
];
80+
81+
const SUGGESTIONS_TO_SHOW = 3;
82+
83+
interface SuggestedTasksStore {
84+
currentTitles: string[];
85+
usageCounts: Record<string, number>;
86+
rotateSuggestions: () => void;
87+
incrementUsage: (title: string) => void;
88+
getSuggestions: () => Suggestion[];
89+
}
90+
91+
export const useSuggestedTasksStore = create<SuggestedTasksStore>()(
92+
persist(
93+
(set, get) => ({
94+
currentTitles: ALL_SUGGESTIONS.slice(0, SUGGESTIONS_TO_SHOW).map(
95+
(s) => s.title,
96+
),
97+
usageCounts: {},
98+
99+
getSuggestions: () => {
100+
const { currentTitles } = get();
101+
return currentTitles
102+
.map((title) => ALL_SUGGESTIONS.find((s) => s.title === title))
103+
.filter((s): s is Suggestion => s !== undefined);
104+
},
105+
106+
rotateSuggestions: () => {
107+
const { currentTitles } = get();
108+
const currentSet = new Set(currentTitles);
109+
const available = ALL_SUGGESTIONS.filter(
110+
(s) => !currentSet.has(s.title),
111+
);
112+
113+
if (available.length === 0) {
114+
set({ currentTitles: [...currentTitles.slice(1), currentTitles[0]] });
115+
return;
116+
}
117+
118+
const randomIndex = Math.floor(Math.random() * available.length);
119+
const newTitle = available[randomIndex].title;
120+
121+
set({ currentTitles: [...currentTitles.slice(1), newTitle] });
122+
},
123+
124+
incrementUsage: (title: string) => {
125+
const { usageCounts } = get();
126+
const updated = {
127+
...usageCounts,
128+
[title]: (usageCounts[title] || 0) + 1,
129+
};
130+
131+
const sorted = ALL_SUGGESTIONS.slice().sort((a, b) => {
132+
const countA = updated[a.title] || 0;
133+
const countB = updated[b.title] || 0;
134+
return countB - countA;
135+
});
136+
137+
set({
138+
usageCounts: updated,
139+
currentTitles: sorted
140+
.slice(0, SUGGESTIONS_TO_SHOW)
141+
.map((s) => s.title),
142+
});
143+
},
144+
}),
145+
{
146+
name: "suggested-tasks-storage",
147+
},
148+
),
149+
);

apps/array/src/renderer/features/task-detail/stores/taskExecutionStore.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -922,9 +922,11 @@ export const useTaskExecutionStore = create<TaskExecutionStore>()(
922922
if (taskState.repoPath) return;
923923

924924
// 1. Check taskDirectoryStore first
925-
const repoKey = task.repository_config
926-
? `${task.repository_config.organization}/${task.repository_config.repository}`
927-
: undefined;
925+
const repoKey =
926+
task.repository_config?.organization &&
927+
task.repository_config?.repository
928+
? `${task.repository_config.organization}/${task.repository_config.repository}`
929+
: undefined;
928930

929931
const storedDirectory = useTaskDirectoryStore
930932
.getState()
@@ -945,7 +947,8 @@ export const useTaskExecutionStore = create<TaskExecutionStore>()(
945947
}
946948

947949
// 2. Fallback to deriving from workspace (existing logic)
948-
if (!task.repository_config) return;
950+
if (!task.repository_config || !task.repository_config.repository)
951+
return;
949952

950953
const { defaultWorkspace } = useAuthStore.getState();
951954
if (!defaultWorkspace) return;

0 commit comments

Comments
 (0)