feat(web): add quick chat entry points on mobile (#1751)

* feat(web): add quick chat entry points on mobile

Quick chat was unreachable on touch devices: every trigger was
desktop-only (keyboard shortcut, command palette, app sidebar) and the
full-screen dialog had no touch dismissal.

- add quick chat button to the mobile kanban/tasks header
- add quick chat button to the tablet header (640-767px has no sidebar)
- add chat button to the mobile session task-switcher sheet
- add a mobile-only close control to the quick chat tab bar
- cover the touch entry points with a mobile Playwright spec

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(web): address review feedback on mobile quick chat spec

- reuse the shared assertNoDocumentHorizontalOverflow layout helper in
  both quick chat mobile specs instead of local duplicates
- assert the task switcher sheet is dismissed after opening quick chat

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
João Salavisa
2026-07-17 13:05:43 +01:00
committed by GitHub
parent e145d48f22
commit 14d65a2abd
7 changed files with 207 additions and 38 deletions
@@ -1,7 +1,7 @@
"use client";
import { useCallback, useMemo, useState, memo } from "react";
import { IconPlus } from "@tabler/icons-react";
import { IconMessageCircle, IconPlus } from "@tabler/icons-react";
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@kandev/ui/sheet";
import { Button } from "@kandev/ui/button";
import { TaskSwitcher } from "../task-switcher";
@@ -22,6 +22,7 @@ import { SidebarLinkDialogs } from "../task-session-sidebar-dialogs";
import { useSidebarLinkActions } from "../task-session-sidebar-link-actions";
import { useSidebarTaskLinking } from "../task-session-sidebar-task-linking";
import { useSheetData, useSheetActions } from "./session-task-switcher-sheet-hooks";
import { useQuickChatLauncher } from "@/hooks/use-quick-chat-launcher";
type SessionTaskSwitcherSheetProps = {
open: boolean;
@@ -131,6 +132,58 @@ export function MobileTaskList({
);
}
function TaskSwitcherSheetHeader({
workspaceId,
workspaces,
onWorkspaceChange,
onQuickChat,
onNewTask,
}: {
workspaceId: string | null;
workspaces: Array<{ id: string; name: string }>;
onWorkspaceChange: (workspaceId: string) => void;
onQuickChat: () => void;
onNewTask: () => void;
}) {
return (
<SheetHeader className="p-4 pb-2 border-b border-border">
<div className="flex items-center justify-between">
<SheetTitle className="text-base">Tasks</SheetTitle>
<div className="flex items-center gap-2">
{workspaceId && (
<Button
size="sm"
variant="outline"
className="h-7 gap-1 cursor-pointer"
onClick={onQuickChat}
data-testid="mobile-sheet-quick-chat"
>
<IconMessageCircle className="h-4 w-4" />
Chat
</Button>
)}
<Button
size="sm"
variant="outline"
className="h-7 gap-1 cursor-pointer"
onClick={onNewTask}
>
<IconPlus className="h-4 w-4" />
New
</Button>
</div>
</div>
<div className="pt-2">
<WorkspaceSwitcher
workspaces={workspaces}
activeWorkspaceId={workspaceId}
onSelect={onWorkspaceChange}
/>
</div>
</SheetHeader>
);
}
export const SessionTaskSwitcherSheet = memo(function SessionTaskSwitcherSheet({
open,
onOpenChange,
@@ -141,6 +194,11 @@ export const SessionTaskSwitcherSheet = memo(function SessionTaskSwitcherSheet({
const data = useSheetData(workspaceId);
const actions = useSheetActions(workspaceId, onOpenChange);
const linking = useMobileTaskLinking(workspaceId);
const openQuickChat = useQuickChatLauncher(workspaceId);
const handleQuickChat = useCallback(() => {
onOpenChange(false);
openQuickChat();
}, [onOpenChange, openQuickChat]);
return (
<Sheet open={open} onOpenChange={onOpenChange}>
@@ -149,27 +207,13 @@ export const SessionTaskSwitcherSheet = memo(function SessionTaskSwitcherSheet({
side="left"
className="w-[85vw] max-w-sm p-0 flex flex-col"
>
<SheetHeader className="p-4 pb-2 border-b border-border">
<div className="flex items-center justify-between">
<SheetTitle className="text-base">Tasks</SheetTitle>
<Button
size="sm"
variant="outline"
className="h-7 gap-1 cursor-pointer"
onClick={() => setDialogOpen(true)}
>
<IconPlus className="h-4 w-4" />
New
</Button>
</div>
<div className="pt-2">
<WorkspaceSwitcher
workspaces={data.workspaces.map((w) => ({ id: w.id, name: w.name }))}
activeWorkspaceId={workspaceId}
onSelect={actions.handleWorkspaceChange}
/>
</div>
</SheetHeader>
<TaskSwitcherSheetHeader
workspaceId={workspaceId}
workspaces={data.workspaces.map((w) => ({ id: w.id, name: w.name }))}
onWorkspaceChange={actions.handleWorkspaceChange}
onQuickChat={handleQuickChat}
onNewTask={() => setDialogOpen(true)}
/>
<SidebarFilterBar />