Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 182d7aef0a |
@@ -21,7 +21,7 @@ import {
|
||||
import { useAuthenticated } from "hooks";
|
||||
import { useSearchParamsKey } from "hooks/useSearchParamsKey";
|
||||
import { EditIcon, EllipsisIcon, PanelLeftIcon, TrashIcon } from "lucide-react";
|
||||
import type { Task } from "modules/tasks/tasks";
|
||||
import { getCleanTaskName, type Task } from "modules/tasks/tasks";
|
||||
import { type FC, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { Link as RouterLink, useNavigate, useParams } from "react-router";
|
||||
@@ -179,66 +179,89 @@ const TaskSidebarMenuItem: FC<TaskSidebarMenuItemProps> = ({ task }) => {
|
||||
const isActive = task.workspace.name === workspace;
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const cleanTaskName = getCleanTaskName(task.workspace.name);
|
||||
const truncatedPrompt =
|
||||
task.prompt.length > 100 ? `${task.prompt.slice(0, 100)}...` : task.prompt;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
className={cn(
|
||||
"overflow-visible group w-full justify-start text-content-secondary",
|
||||
"transition-none hover:bg-surface-tertiary gap-2 has-[[data-state=open]]:bg-surface-tertiary",
|
||||
{
|
||||
"text-content-primary bg-surface-quaternary hover:bg-surface-quaternary has-[[data-state=open]]:bg-surface-quaternary":
|
||||
isActive,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<RouterLink
|
||||
to={{
|
||||
pathname: `/tasks/${task.workspace.owner_name}/${task.workspace.name}`,
|
||||
search: window.location.search,
|
||||
}}
|
||||
>
|
||||
<TaskSidebarMenuItemStatus task={task} />
|
||||
<span className="truncate">{task.workspace.name}</span>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="subtle"
|
||||
className={`
|
||||
ml-auto -mr-2 opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100
|
||||
focus-visible:opacity-100 data-[state=open]:opacity-100
|
||||
`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
className={cn(
|
||||
"overflow-visible group w-full justify-start text-content-secondary",
|
||||
"transition-none hover:bg-surface-tertiary gap-2 has-[[data-state=open]]:bg-surface-tertiary",
|
||||
{
|
||||
"text-content-primary bg-surface-quaternary hover:bg-surface-quaternary has-[[data-state=open]]:bg-surface-quaternary":
|
||||
isActive,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<RouterLink
|
||||
to={{
|
||||
pathname: `/tasks/${task.workspace.owner_name}/${task.workspace.name}`,
|
||||
search: window.location.search,
|
||||
}}
|
||||
>
|
||||
<EllipsisIcon />
|
||||
<span className="sr-only">Task options</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<TaskSidebarMenuItemStatus task={task} />
|
||||
<span className="truncate">{cleanTaskName}</span>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="subtle"
|
||||
className={`
|
||||
ml-auto -mr-2 opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100
|
||||
focus-visible:opacity-100 data-[state=open]:opacity-100
|
||||
`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<EllipsisIcon />
|
||||
<span className="sr-only">Task options</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
className="text-content-destructive focus:text-content-destructive"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsDeleteDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<TrashIcon />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</RouterLink>
|
||||
</Button>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
className="text-content-destructive focus:text-content-destructive"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsDeleteDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<TrashIcon />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</RouterLink>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="max-w-md">
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<div className="text-xs text-content-secondary">
|
||||
Workspace name
|
||||
</div>
|
||||
<div className="text-sm font-medium">{task.workspace.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-content-secondary">Prompt</div>
|
||||
<div className="text-sm">{truncatedPrompt}</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<TaskDeleteDialog
|
||||
open={isDeleteDialogOpen}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { getCleanTaskName } from "./tasks";
|
||||
|
||||
describe("getCleanTaskName", () => {
|
||||
it("should remove task- prefix and identifier suffix", () => {
|
||||
expect(getCleanTaskName("task-fix-login-bug-abc123")).toBe("Fix Login Bug");
|
||||
expect(getCleanTaskName("task-add-feature-xyz789")).toBe("Add Feature");
|
||||
expect(getCleanTaskName("task-update-docs-12345678")).toBe("Update Docs");
|
||||
});
|
||||
|
||||
it("should handle workspace names without task- prefix", () => {
|
||||
expect(getCleanTaskName("fix-login-bug-abc123")).toBe("Fix Login Bug");
|
||||
expect(getCleanTaskName("simple-name-123")).toBe("Simple Name");
|
||||
});
|
||||
|
||||
it("should handle short identifiers with numbers", () => {
|
||||
expect(getCleanTaskName("task-test-feature-a1b2c3")).toBe("Test Feature");
|
||||
expect(getCleanTaskName("task-debug-issue-99")).toBe("Debug Issue");
|
||||
});
|
||||
|
||||
it("should handle UUID-like identifiers", () => {
|
||||
expect(getCleanTaskName("task-implement-auth-a1b2c3d4")).toBe(
|
||||
"Implement Auth",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle names without identifiers", () => {
|
||||
expect(getCleanTaskName("task-simple")).toBe("Simple");
|
||||
expect(getCleanTaskName("simple")).toBe("Simple");
|
||||
});
|
||||
|
||||
it("should title case the result", () => {
|
||||
expect(getCleanTaskName("task-fix-login-bug-123")).toBe("Fix Login Bug");
|
||||
expect(getCleanTaskName("task-ADD-NEW-FEATURE-xyz")).toBe(
|
||||
"Add New Feature",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle single word names", () => {
|
||||
expect(getCleanTaskName("task-refactor")).toBe("Refactor");
|
||||
expect(getCleanTaskName("deploy")).toBe("Deploy");
|
||||
});
|
||||
|
||||
it("should remove last part if it could be an identifier", () => {
|
||||
// The function removes the last part if it could potentially be an identifier
|
||||
// This is conservative behavior to ensure clean names
|
||||
expect(getCleanTaskName("task-update-user-profile")).toBe("Update User");
|
||||
expect(getCleanTaskName("task-create-new-issue")).toBe("Create New");
|
||||
});
|
||||
});
|
||||
@@ -25,3 +25,46 @@ export function getTaskApps(task: Task): WorkspaceAppWithAgent[] {
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a clean, human-readable task name from a workspace name.
|
||||
* Workspace names typically follow the pattern: task-{name}-{identifier}
|
||||
* This function removes the "task-" prefix and the identifier suffix,
|
||||
* leaving just the middle portion with dashes converted to spaces and title cased.
|
||||
*
|
||||
* Examples:
|
||||
* - "task-fix-login-bug-abc123" -> "Fix Login Bug"
|
||||
* - "task-add-feature-xyz789" -> "Add Feature"
|
||||
* - "simple-name" -> "Simple Name" (fallback for non-standard names)
|
||||
*
|
||||
* @param workspaceName - The full workspace name
|
||||
* @returns A cleaned, human-readable task name
|
||||
*/
|
||||
export function getCleanTaskName(workspaceName: string): string {
|
||||
// Remove "task-" prefix if present
|
||||
const cleaned = workspaceName.startsWith("task-")
|
||||
? workspaceName.slice(5)
|
||||
: workspaceName;
|
||||
|
||||
// Split by dashes
|
||||
const parts = cleaned.split("-");
|
||||
|
||||
// If we have multiple parts, remove the last one (likely an identifier)
|
||||
// Only do this if the last part looks like an identifier (short alphanumeric)
|
||||
if (parts.length > 2) {
|
||||
const lastPart = parts[parts.length - 1];
|
||||
// Check if last part is likely an identifier (short, contains numbers, or is a UUID-like string)
|
||||
if (
|
||||
lastPart.length <= 8 ||
|
||||
/\d/.test(lastPart) ||
|
||||
/^[a-f0-9]{8,}$/i.test(lastPart)
|
||||
) {
|
||||
parts.pop();
|
||||
}
|
||||
}
|
||||
|
||||
// Join remaining parts with spaces and title case each word
|
||||
return parts
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
LaptopMinimalIcon,
|
||||
TerminalIcon,
|
||||
} from "lucide-react";
|
||||
import type { Task } from "modules/tasks/tasks";
|
||||
import { getCleanTaskName, type Task } from "modules/tasks/tasks";
|
||||
import type { FC } from "react";
|
||||
import { Link as RouterLink } from "react-router";
|
||||
import { TaskStatusLink } from "./TaskStatusLink";
|
||||
@@ -21,6 +21,10 @@ import { TaskStatusLink } from "./TaskStatusLink";
|
||||
type TaskTopbarProps = { task: Task };
|
||||
|
||||
export const TaskTopbar: FC<TaskTopbarProps> = ({ task }) => {
|
||||
const cleanTaskName = getCleanTaskName(task.workspace.name);
|
||||
const truncatedPrompt =
|
||||
task.prompt.length > 100 ? `${task.prompt.slice(0, 100)}...` : task.prompt;
|
||||
|
||||
return (
|
||||
<header className="flex flex-shrink-0 items-center p-3 border-solid border-border border-0 border-b">
|
||||
<TooltipProvider>
|
||||
@@ -37,9 +41,29 @@ export const TaskTopbar: FC<TaskTopbarProps> = ({ task }) => {
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<h1 className="m-0 pl-2 text-base font-medium truncate">
|
||||
{task.workspace.name}
|
||||
</h1>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<h1 className="m-0 pl-2 text-base font-medium truncate cursor-help">
|
||||
{cleanTaskName}
|
||||
</h1>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-md">
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<div className="text-xs text-content-secondary">
|
||||
Workspace name
|
||||
</div>
|
||||
<div className="text-sm font-medium">{task.workspace.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-content-secondary">Prompt</div>
|
||||
<div className="text-sm">{truncatedPrompt}</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
{task.workspace.latest_app_status?.uri && (
|
||||
<div className="flex items-center gap-2 flex-wrap ml-4">
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import { RotateCcwIcon, TrashIcon } from "lucide-react";
|
||||
import { TaskDeleteDialog } from "modules/tasks/TaskDeleteDialog/TaskDeleteDialog";
|
||||
import type { Task } from "modules/tasks/tasks";
|
||||
import { getCleanTaskName, type Task } from "modules/tasks/tasks";
|
||||
import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus";
|
||||
import { WorkspaceStatus } from "modules/workspaces/WorkspaceStatus/WorkspaceStatus";
|
||||
import { type FC, type ReactNode, useState } from "react";
|
||||
@@ -119,24 +119,52 @@ const TaskRow: FC<TaskRowProps> = ({ task }) => {
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const templateDisplayName =
|
||||
task.workspace.template_display_name ?? task.workspace.template_name;
|
||||
const cleanTaskName = getCleanTaskName(task.workspace.name);
|
||||
const truncatedPrompt =
|
||||
task.prompt.length > 100 ? `${task.prompt.slice(0, 100)}...` : task.prompt;
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow key={task.workspace.id} className="relative" hover>
|
||||
<TableCell>
|
||||
<RouterLink
|
||||
to={`/tasks/${task.workspace.owner_name}/${task.workspace.name}`}
|
||||
className="absolute inset-0"
|
||||
>
|
||||
<span className="sr-only">Access task</span>
|
||||
</RouterLink>
|
||||
<AvatarData
|
||||
title={
|
||||
<>
|
||||
<span className="block max-w-[520px] overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{task.prompt}
|
||||
</span>
|
||||
<RouterLink
|
||||
to={`/tasks/${task.workspace.owner_name}/${task.workspace.name}`}
|
||||
className="absolute inset-0"
|
||||
>
|
||||
<span className="sr-only">Access task</span>
|
||||
</RouterLink>
|
||||
</>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<RouterLink
|
||||
to={`/tasks/${task.workspace.owner_name}/${task.workspace.name}`}
|
||||
className="block max-w-[520px] overflow-hidden text-ellipsis whitespace-nowrap cursor-help relative z-10 no-underline text-inherit"
|
||||
>
|
||||
{cleanTaskName}
|
||||
</RouterLink>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-md">
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<div className="text-xs text-content-secondary">
|
||||
Workspace name
|
||||
</div>
|
||||
<div className="text-sm font-medium">
|
||||
{task.workspace.name}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-content-secondary">
|
||||
Prompt
|
||||
</div>
|
||||
<div className="text-sm">{truncatedPrompt}</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
}
|
||||
subtitle={templateDisplayName}
|
||||
avatar={
|
||||
|
||||
Reference in New Issue
Block a user