Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions apps/twig/src/main/services/workspace/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ import { z } from "zod";
export const workspaceModeSchema = z
.enum(["worktree", "local", "cloud", "root"])
.transform((val) => (val === "root" ? "local" : val));

export const workspaceNameSchema = z
.string()
.min(1, "Workspace name is required")
.max(50, "Workspace name must be 50 characters or less")
.regex(/^[a-zA-Z]/, "Workspace name must begin with a letter")
.regex(
/^[a-zA-Z0-9-]+$/,
"Workspace name can only contain letters, numbers, and dashes",
);

export const worktreeInfoSchema = z.object({
worktreePath: z.string(),
worktreeName: z.string(),
Expand Down Expand Up @@ -62,6 +73,17 @@ export const createWorkspaceInput = z.object({
mode: workspaceModeSchema,
branch: z.string().optional(),
useExistingBranch: z.boolean().optional(),
customName: workspaceNameSchema.optional(),
});

export const checkNameAvailableInput = z.object({
name: workspaceNameSchema,
mainRepoPath: z.string().min(2, "Repository path is required"),
});

export const checkNameAvailableOutput = z.object({
available: z.boolean(),
reason: z.string().optional(),
});

export const deleteWorkspaceInput = z.object({
Expand Down Expand Up @@ -214,3 +236,5 @@ export type IsLocalBackgroundedInput = z.infer<typeof isLocalBackgroundedInput>;
export type GetLocalWorktreePathInput = z.infer<
typeof getLocalWorktreePathInput
>;
export type CheckNameAvailableInput = z.infer<typeof checkNameAvailableInput>;
export type CheckNameAvailableOutput = z.infer<typeof checkNameAvailableOutput>;
16 changes: 15 additions & 1 deletion apps/twig/src/main/services/workspace/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,9 +313,10 @@ export class WorkspaceService extends TypedEventEmitter<WorkspaceServiceEvents>
mode,
branch,
useExistingBranch,
customName,
} = options;
log.info(
`Creating workspace for task ${taskId} in ${mainRepoPath} (mode: ${mode}, useExistingBranch: ${useExistingBranch})`,
`Creating workspace for task ${taskId} in ${mainRepoPath} (mode: ${mode}, useExistingBranch: ${useExistingBranch}${customName ? `, customName: ${customName}` : ""})`,
);

if (mode === "cloud") {
Expand Down Expand Up @@ -510,6 +511,7 @@ export class WorkspaceService extends TypedEventEmitter<WorkspaceServiceEvents>
// Standard mode: create new twig/ branch
worktree = await worktreeManager.createWorktree({
baseBranch: branch ?? undefined,
customName,
});
log.info(
`Created worktree: ${worktree.worktreeName} at ${worktree.worktreePath}`,
Expand Down Expand Up @@ -1107,6 +1109,18 @@ export class WorkspaceService extends TypedEventEmitter<WorkspaceServiceEvents>
return result;
}

async checkNameAvailable(
name: string,
mainRepoPath: string,
): Promise<{ available: boolean; reason?: string }> {
const worktreeBasePath = getWorktreeLocation();
const worktreeManager = new WorktreeManager({
mainRepoPath,
worktreeBasePath,
});
return worktreeManager.isNameAvailable(name);
}

private async cleanupWorktree(
taskId: string,
mainRepoPath: string,
Expand Down
9 changes: 9 additions & 0 deletions apps/twig/src/main/trpc/routers/workspace.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { container } from "../../di/container.js";
import { MAIN_TOKENS } from "../../di/tokens.js";
import {
checkNameAvailableInput,
checkNameAvailableOutput,
createWorkspaceInput,
createWorkspaceOutput,
deleteWorkspaceInput,
Expand Down Expand Up @@ -99,6 +101,13 @@ export const workspaceRouter = router({
.output(getWorktreeTasksOutput)
.query(({ input }) => getService().getWorktreeTasks(input.worktreePath)),

checkNameAvailable: publicProcedure
.input(checkNameAvailableInput)
.output(checkNameAvailableOutput)
.query(({ input }) =>
getService().checkNameAvailable(input.name, input.mainRepoPath),
),

onTerminalCreated: subscribe(WorkspaceServiceEvent.TerminalCreated),
onError: subscribe(WorkspaceServiceEvent.Error),
onWarning: subscribe(WorkspaceServiceEvent.Warning),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,13 @@ export function SettingsView() {
autoConvertLongText,
sendMessagesWith,
allowBypassPermissions,
customWorkspaceNames,
setCursorGlow,
setDesktopNotifications,
setAutoConvertLongText,
setSendMessagesWith,
setAllowBypassPermissions,
setCustomWorkspaceNames,
} = useSettingsStore();
const terminalLayoutMode = useTerminalSettingsStore(
(state) => state.terminalLayoutMode,
Expand Down Expand Up @@ -339,6 +341,18 @@ export function SettingsView() {
setShowBypassWarning(false);
}, [setAllowBypassPermissions]);

const handleCustomWorkspaceNamesChange = useCallback(
(checked: boolean) => {
track(ANALYTICS_EVENTS.SETTING_CHANGED, {
setting_name: "custom_workspace_names",
new_value: checked,
old_value: customWorkspaceNames,
});
setCustomWorkspaceNames(checked);
},
[customWorkspaceNames, setCustomWorkspaceNames],
);

const terminalFontSelection = TERMINAL_FONT_PRESETS.some(
(preset) => preset.value === terminalFontFamily,
)
Expand Down Expand Up @@ -681,7 +695,7 @@ export function SettingsView() {
<Flex direction="column" gap="3">
<Heading size="3">Workspace storage</Heading>
<Card>
<Flex direction="column" gap="3">
<Flex direction="column" gap="4">
<Flex direction="column" gap="2">
<Text size="1" weight="medium">
Workspace location
Expand All @@ -697,6 +711,23 @@ export function SettingsView() {
task. Workspaces are organized by repository name.
</Text>
</Flex>

<Flex align="center" justify="between">
<Flex direction="column" gap="1">
<Text size="1" weight="medium">
Custom workspace names
</Text>
<Text size="1" color="gray">
Name workspaces manually instead of using auto-generated
names
</Text>
</Flex>
<Switch
checked={customWorkspaceNames}
onCheckedChange={handleCustomWorkspaceNamesChange}
size="1"
/>
</Flex>
</Flex>
</Card>
</Flex>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface SettingsStore {
autoConvertLongText: boolean;
sendMessagesWith: SendMessagesWith;
allowBypassPermissions: boolean;
customWorkspaceNames: boolean;

setDefaultRunMode: (mode: DefaultRunMode) => void;
setLastUsedRunMode: (mode: "local" | "cloud") => void;
Expand All @@ -26,6 +27,7 @@ interface SettingsStore {
setAutoConvertLongText: (enabled: boolean) => void;
setSendMessagesWith: (mode: SendMessagesWith) => void;
setAllowBypassPermissions: (enabled: boolean) => void;
setCustomWorkspaceNames: (enabled: boolean) => void;
}

export const useSettingsStore = create<SettingsStore>()(
Expand All @@ -40,6 +42,7 @@ export const useSettingsStore = create<SettingsStore>()(
autoConvertLongText: true,
sendMessagesWith: "enter",
allowBypassPermissions: false,
customWorkspaceNames: false,

setDefaultRunMode: (mode) => set({ defaultRunMode: mode }),
setLastUsedRunMode: (mode) => set({ lastUsedRunMode: mode }),
Expand All @@ -54,6 +57,8 @@ export const useSettingsStore = create<SettingsStore>()(
setSendMessagesWith: (mode) => set({ sendMessagesWith: mode }),
setAllowBypassPermissions: (enabled) =>
set({ allowBypassPermissions: enabled }),
setCustomWorkspaceNames: (enabled) =>
set({ customWorkspaceNames: enabled }),
}),
{
name: "settings-storage",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@ import { useTaskCreation } from "../hooks/useTaskCreation";
import { SuggestedTasks } from "./SuggestedTasks";
import { TaskInputEditor } from "./TaskInputEditor";
import { type WorkspaceMode, WorkspaceModeSelect } from "./WorkspaceModeSelect";
import { WorkspaceNameInput } from "./WorkspaceNameInput";

const DOT_FILL = "var(--gray-6)";

export function TaskInput() {
const { view } = useNavigationStore();
const { lastUsedDirectory } = useTaskDirectoryStore();
const { lastUsedLocalWorkspaceMode, allowBypassPermissions } =
useSettingsStore();
const {
lastUsedLocalWorkspaceMode,
allowBypassPermissions,
customWorkspaceNames,
} = useSettingsStore();

const editorRef = useRef<MessageEditorHandle>(null);
const containerRef = useRef<HTMLDivElement>(null);
Expand All @@ -35,6 +39,11 @@ export function TaskInput() {
);
const [editorIsEmpty, setEditorIsEmpty] = useState(true);
const [executionMode, setExecutionMode] = useState<ExecutionMode>("default");
const [workspaceName, setWorkspaceName] = useState("");
const [isWorkspaceNameValid, setIsWorkspaceNameValid] = useState(false);

const showWorkspaceNameInput =
customWorkspaceNames && workspaceMode === "worktree";

// Reset to default mode if bypass was disabled while in bypass mode
useEffect(() => {
Expand Down Expand Up @@ -74,6 +83,8 @@ export function TaskInput() {
branch: null,
editorIsEmpty,
executionMode: executionMode === "default" ? undefined : executionMode,
workspaceName: showWorkspaceNameInput ? workspaceName : undefined,
isWorkspaceNameValid: showWorkspaceNameInput ? isWorkspaceNameValid : true,
});

return (
Expand Down Expand Up @@ -147,6 +158,16 @@ export function TaskInput() {
onChange={setWorkspaceMode}
size="1"
/>
{showWorkspaceNameInput && (
<WorkspaceNameInput
value={workspaceName}
onChange={setWorkspaceName}
onValidChange={setIsWorkspaceNameValid}
directoryPath={selectedDirectory}
size="1"
placeholder="workspace-name"
/>
)}
</Flex>

<TaskInputEditor
Expand Down
Loading
Loading