Skip to content
Merged
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
61 changes: 61 additions & 0 deletions apps/desktop/plans/20260320-desktop-task-create-tiptap-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Desktop Task Create: Linear-Style TipTap Plan

## Goal

Add a desktop-only create-task flow in the Tasks view that feels like Linear and uses the same editor surface as task editing.

## Scope

- Desktop only
- No web work
- No package extraction unless it becomes necessary later

## Key Decision

Create and edit should share one desktop task composer. Do not build a separate create modal with a second editor implementation.

## Existing Anchors

- Current task editor: `apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/TaskMarkdownRenderer/TaskMarkdownRenderer.tsx`
- Bubble toolbar: `apps/desktop/src/renderer/components/MarkdownRenderer/components/TipTapMarkdownRenderer/components/BubbleMenuToolbar/BubbleMenuToolbar.tsx`
- Tasks top bar entry point: `apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx`
- Existing metadata patterns:
- status: `apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/StatusProperty/StatusProperty.tsx`
- priority: `apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/shared/PriorityMenuItems.tsx`
- assignee: `apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/AssigneeFilter/AssigneeFilter.tsx`

## Plan

1. Extract a shared desktop `TaskComposer` under the desktop tasks feature.
Start from the current `TaskMarkdownRenderer`, bubble menu, and existing metadata controls.

2. Migrate the existing task detail editor to `TaskComposer` in `edit` mode.
This keeps behavior aligned before adding create.

3. Add a `CreateTaskDialog` launched from `TasksTopBar`.
The dialog should be compact and Linear-style: title first, metadata row, TipTap description, submit via `Cmd/Ctrl+Enter`.

4. Add a desktop-facing create path that owns slug/default status resolution.
Do not change the existing low-level `task.create` contract in place if desktop/mobile sync depends on it.

5. On successful create, close the dialog, refresh task data, and navigate to the new task.

## Non-Goals for V1

- Web create flow
- Slash commands
- Image handling
- Full editor/package sharing outside the desktop task feature

## Validation

```bash
bun run typecheck
bun run lint
```

Manual checks:

1. Create a task from the tasks top bar.
2. Edit an existing task with the refactored composer.
3. Confirm create and edit feel like the same surface.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import "highlight.js/styles/github-dark.css";
import "./task-markdown.css";

import { cn } from "@superset/ui/utils";
import { Extension } from "@tiptap/core";
import { Blockquote } from "@tiptap/extension-blockquote";
import { Bold } from "@tiptap/extension-bold";
Expand All @@ -24,9 +25,15 @@ import TaskItem from "@tiptap/extension-task-item";
import TaskList from "@tiptap/extension-task-list";
import { Text } from "@tiptap/extension-text";
import { Underline } from "@tiptap/extension-underline";
import { EditorContent, ReactNodeViewRenderer, useEditor } from "@tiptap/react";
import {
type Editor,
EditorContent,
ReactNodeViewRenderer,
useEditor,
} from "@tiptap/react";
import { BubbleMenu } from "@tiptap/react/menus";
import { common, createLowlight } from "lowlight";
import { useEffect } from "react";
import { BubbleMenuToolbar } from "renderer/components/MarkdownRenderer/components/TipTapMarkdownRenderer/components/BubbleMenuToolbar";
import { env } from "renderer/env.renderer";
import { Markdown } from "tiptap-markdown";
Expand Down Expand Up @@ -117,14 +124,34 @@ const KeyboardHandler = Extension.create({

interface TaskMarkdownRendererProps {
content: string;
onSave: (markdown: string) => void;
onSave?: (markdown: string) => void;
onChange?: (markdown: string) => void;
placeholder?: string;
autoFocus?: boolean;
className?: string;
editorClassName?: string;
onModEnter?: () => void;
}

function getMarkdown(editor: Editor | null): string {
const storage = editor?.storage as
| Record<string, { getMarkdown?: () => string }>
| undefined;
return storage?.markdown?.getMarkdown?.() ?? "";
}

export function TaskMarkdownRenderer({
content,
onSave,
onChange,
placeholder = "Add description...",
autoFocus = false,
className,
editorClassName,
onModEnter,
}: TaskMarkdownRendererProps) {
const editor = useEditor({
autofocus: autoFocus ? "end" : false,
extensions: [
Document,
Text,
Expand Down Expand Up @@ -198,7 +225,7 @@ export function TaskMarkdownRenderer({
Placeholder.configure({
placeholder: ({ node }) => {
if (node.type.name === "paragraph") {
return "Add description...";
return placeholder;
}
return "";
},
Expand All @@ -217,21 +244,35 @@ export function TaskMarkdownRenderer({
content,
editorProps: {
attributes: {
class: "focus:outline-none min-h-[100px]",
class: cn("focus:outline-none min-h-[100px]", editorClassName),
},
handleKeyDown: (_, event) => {
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
onModEnter?.();
return true;
}
Comment on lines +250 to +253
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Only consume Cmd/Ctrl+Enter when onModEnter exists; currently the shortcut is swallowed even when no callback is provided.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/TaskMarkdownRenderer/TaskMarkdownRenderer.tsx, line 250:

<comment>Only consume Cmd/Ctrl+Enter when `onModEnter` exists; currently the shortcut is swallowed even when no callback is provided.</comment>

<file context>
@@ -217,21 +244,35 @@ export function TaskMarkdownRenderer({
+				class: cn("focus:outline-none min-h-[100px]", editorClassName),
 			},
+			handleKeyDown: (_, event) => {
+				if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
+					onModEnter?.();
+					return true;
</file context>
Suggested change
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
onModEnter?.();
return true;
}
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
if (!onModEnter) return false;
onModEnter();
return true;
}
Fix with Cubic

return false;
},
Comment on lines +249 to +255
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the TaskMarkdownRenderer component
fd "TaskMarkdownRenderer" --type f

Repository: superset-sh/superset

Length of output: 193


🏁 Script executed:

# Search for the prop interface/type definition and usages
rg -A 15 "interface.*TaskMarkdownRenderer.*Props|type.*TaskMarkdownRenderer.*Props" apps/desktop/src

Repository: superset-sh/superset

Length of output: 2605


🏁 Script executed:

# Search for usages of TaskMarkdownRenderer to see where onModEnter is/isn't provided
rg "TaskMarkdownRenderer" apps/desktop/src --type tsx --type ts -B 2 -A 5

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

# Search for usages of TaskMarkdownRenderer
rg "TaskMarkdownRenderer" apps/desktop/src -A 5 -B 2

Repository: superset-sh/superset

Length of output: 9181


🏁 Script executed:

# Get the full component to understand the current implementation
cat -n apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/TaskMarkdownRenderer/TaskMarkdownRenderer.tsx | head -280 | tail -50

Repository: superset-sh/superset

Length of output: 218


🏁 Script executed:

# Read the TaskMarkdownRenderer component file
cat -n apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/\$taskId/components/TaskMarkdownRenderer/TaskMarkdownRenderer.tsx | sed -n '240,260p'

Repository: superset-sh/superset

Length of output: 654


🏁 Script executed:

# Get more context around the handleKeyDown implementation
cat -n apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/\$taskId/components/TaskMarkdownRenderer/TaskMarkdownRenderer.tsx | sed -n '240,270p'

Repository: superset-sh/superset

Length of output: 923


Only consume Mod+Enter when a handler is actually provided.

In the task detail view (page.tsx), onModEnter is omitted, but the current implementation still returns true for Mod+Enter, preventing the editor's default behavior. Gate this interception on onModEnter so only the create dialog overrides it.

Suggested fix
 			handleKeyDown: (_, event) => {
-				if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
-					onModEnter?.();
+				if (
+					(event.metaKey || event.ctrlKey) &&
+					event.key === "Enter" &&
+					onModEnter
+				) {
+					onModEnter();
 					return true;
 				}
 				return false;
 			},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
handleKeyDown: (_, event) => {
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
onModEnter?.();
return true;
}
return false;
},
handleKeyDown: (_, event) => {
if (
(event.metaKey || event.ctrlKey) &&
event.key === "Enter" &&
onModEnter
) {
onModEnter();
return true;
}
return false;
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/`$taskId/components/TaskMarkdownRenderer/TaskMarkdownRenderer.tsx
around lines 249 - 255, The keydown handler in TaskMarkdownRenderer
(handleKeyDown) currently consumes Mod+Enter by returning true even when
onModEnter is undefined, blocking the editor's default behavior; update
handleKeyDown to first check that onModEnter is provided before calling
onModEnter() and returning true (i.e., only call onModEnter?.() and return true
when onModEnter is a function), otherwise let the event fall through by
returning false—this ensures the interception is gated on the onModEnter prop in
TaskMarkdownRenderer/handleKeyDown.

},
onUpdate: ({ editor }) => {
onChange?.(getMarkdown(editor));
},
onBlur: ({ editor }) => {
const storage = editor.storage as unknown as Record<
string,
{ getMarkdown?: () => string }
>;
const markdown = storage.markdown?.getMarkdown?.() ?? "";
onSave(markdown);
onSave?.(getMarkdown(editor));
},
});

useEffect(() => {
if (!editor || editor.isFocused) return;
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: External content updates can be permanently missed if they arrive while the editor is focused, because blur does not retrigger this effect.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/TaskMarkdownRenderer/TaskMarkdownRenderer.tsx, line 266:

<comment>External `content` updates can be permanently missed if they arrive while the editor is focused, because blur does not retrigger this effect.</comment>

<file context>
@@ -217,21 +244,35 @@ export function TaskMarkdownRenderer({
 	});
 
+	useEffect(() => {
+		if (!editor || editor.isFocused) return;
+
+		const currentMarkdown = getMarkdown(editor);
</file context>
Fix with Cubic


const currentMarkdown = getMarkdown(editor);
if (currentMarkdown === content) return;

editor.commands.setContent(content, { emitUpdate: false });
}, [content, editor]);

return (
<div className="w-full">
<div className={cn("w-full", className)}>
{editor && (
<BubbleMenu
editor={editor}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { ScrollArea } from "@superset/ui/scroll-area";
import { Separator } from "@superset/ui/separator";
import { eq, or } from "@tanstack/db";
import { useLiveQuery } from "@tanstack/react-db";
import { useQuery } from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useMemo } from "react";
import { HiArrowLeft } from "react-icons/hi2";
import { LuExternalLink } from "react-icons/lu";
import { apiTrpcClient } from "renderer/lib/api-trpc-client";
import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider";
import type { TaskWithStatus } from "../components/TasksView/hooks/useTasksTable";
import { Route as TasksLayoutRoute } from "../layout";
Expand All @@ -28,6 +30,10 @@ function TaskDetailPage() {
const { tab, assignee, search } = TasksLayoutRoute.useSearch();
const navigate = useNavigate();
const collections = useCollections();
const isUuidTaskId =
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
taskId,
);

const backSearch = useMemo(() => {
const s: Record<string, string> = {};
Expand Down Expand Up @@ -62,6 +68,17 @@ function TaskDetailPage() {
if (!taskData || taskData.length === 0) return null;
return taskData[0];
}, [taskData]);
const taskFallbackQuery = useQuery({
queryKey: ["task-detail-fallback", taskId, isUuidTaskId ? "id" : "slug"],
queryFn: () =>
isUuidTaskId
? apiTrpcClient.task.byId.query(taskId)
: apiTrpcClient.task.bySlug.query(taskId),
enabled: !task,
retry: false,
});
Comment on lines +71 to +79
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fallback query currently starts before the primary lookup has conclusively missed.

On Line 77, enabled: !task is also true while the live query is still unresolved, so the fallback RPC can fire eagerly. Gate this on an explicit primary “miss” state to avoid avoidable network calls.

💡 Suggested change
-    enabled: !task,
+    enabled: taskData !== undefined && taskData.length === 0,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/`$taskId/page.tsx
around lines 71 - 79, The fallback query (taskFallbackQuery) is firing while the
primary lookup that sets task is still pending; change the enabled condition to
only run after the primary lookup has conclusively missed by gating on the
primary query's settled/miss state (e.g., use the primary lookup hook that
populates task — the "task" query — and set enabled: primaryQuery.isSettled &&
!task or enabled: primaryQuery.isFetched && !task or enabled:
primaryQuery.isSuccess === false && !task); update the enabled expression for
taskFallbackQuery accordingly and keep using isUuidTaskId to choose
apiTrpcClient.task.byId.query / apiTrpcClient.task.bySlug.query.

const isTaskSyncing = !task && !!taskFallbackQuery.data;
const isTaskLoading = !task && taskFallbackQuery.isPending;

const handleBack = () => {
navigate({ to: "/tasks", search: backSearch });
Expand All @@ -82,6 +99,16 @@ function TaskDetailPage() {
};

if (!task) {
if (isTaskLoading || isTaskSyncing) {
return (
<div className="flex-1 flex items-center justify-center">
<span className="text-muted-foreground">
{isTaskSyncing ? "Syncing task..." : "Loading task..."}
</span>
</div>
);
}

return (
<div className="flex-1 flex items-center justify-center">
<span className="text-muted-foreground">Task not found</span>
Expand Down
Loading
Loading