feat(KB-640): add role editing UI and restore KB task prefix
- Add inline role editing to AgentListModal with clickable role icons - Add handleRoleChange and handleRoleKeyDown handlers for role updates - Persist view toggle (board/list) with localStorage alongside role editing - Restore task prefix from FN back to KB for generated task IDs - Restore branch naming from fusion/ back to kb/ for task branches - Update branch name references in executor, merger, scheduler, and CLI - Add touch gesture detection support to TaskCard component - Update worktree grouping labels to use KB prefix
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { memo, useCallback, useState, useRef, useEffect, useMemo } from "react";
|
||||
import { Link, Clock, Layers, Pencil, ChevronDown, Folder, Maximize2 } from "lucide-react";
|
||||
import { Link, Clock, Layers, Pencil, ChevronDown, Folder } from "lucide-react";
|
||||
import type { Task, TaskDetail, Column, PrInfo, IssueInfo } from "@fusion/core";
|
||||
import { fetchTaskDetail, uploadAttachment } from "../api";
|
||||
import { GitHubBadge } from "./GitHubBadge";
|
||||
@@ -138,10 +138,20 @@ function TaskCardComponent({
|
||||
|
||||
const titleInputRef = useRef<HTMLInputElement>(null);
|
||||
const descTextareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const touchOpenHandledRef = useRef(false);
|
||||
const cardRef = useRef<HTMLDivElement>(null);
|
||||
const [isInViewport, setIsInViewport] = useState(false);
|
||||
const { badgeUpdates, subscribeToBadge, unsubscribeFromBadge } = useBadgeWebSocket();
|
||||
|
||||
// Touch gesture detection refs
|
||||
const touchStartPosRef = useRef<{ x: number; y: number; time: number } | null>(null);
|
||||
const hasTouchMovedRef = useRef(false);
|
||||
|
||||
const isInteractiveTarget = useCallback((target: EventTarget | null): boolean => {
|
||||
if (!(target instanceof HTMLElement)) return false;
|
||||
return !!target.closest("button, a, input, textarea, select, label, [role='button']");
|
||||
}, []);
|
||||
|
||||
// Reset edit state when task changes
|
||||
useEffect(() => {
|
||||
setEditTitle(task.title || "");
|
||||
@@ -232,10 +242,58 @@ function TaskCardComponent({
|
||||
}
|
||||
}, [task.id, onOpenDetail, addToast, isEditing]);
|
||||
|
||||
const handleExpandClick = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const handleCardClick = useCallback((e: React.MouseEvent) => {
|
||||
if (touchOpenHandledRef.current) {
|
||||
touchOpenHandledRef.current = false;
|
||||
return;
|
||||
}
|
||||
if (isInteractiveTarget(e.target)) return;
|
||||
void handleClick();
|
||||
}, [handleClick]);
|
||||
}, [handleClick, isInteractiveTarget]);
|
||||
|
||||
const handleTouchStart = useCallback((e: React.TouchEvent) => {
|
||||
const touch = e.touches[0];
|
||||
if (!touch) return;
|
||||
|
||||
touchStartPosRef.current = { x: touch.clientX, y: touch.clientY, time: Date.now() };
|
||||
hasTouchMovedRef.current = false;
|
||||
}, []);
|
||||
|
||||
const handleTouchMove = useCallback((e: React.TouchEvent) => {
|
||||
if (!touchStartPosRef.current) return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
if (!touch) return;
|
||||
|
||||
const dx = Math.abs(touch.clientX - touchStartPosRef.current.x);
|
||||
const dy = Math.abs(touch.clientY - touchStartPosRef.current.y);
|
||||
|
||||
// If moved beyond threshold, mark as moved (scrolling/dragging)
|
||||
if (dx > TOUCH_MOVE_THRESHOLD || dy > TOUCH_MOVE_THRESHOLD) {
|
||||
hasTouchMovedRef.current = true;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleTouchEnd = useCallback((e: React.TouchEvent) => {
|
||||
if (isInteractiveTarget(e.target)) return;
|
||||
|
||||
// Check if this was a valid tap (not a scroll)
|
||||
if (!touchStartPosRef.current) return;
|
||||
|
||||
const touchDuration = Date.now() - touchStartPosRef.current.time;
|
||||
const isQuickTap = touchDuration < TOUCH_TAP_MAX_DURATION;
|
||||
const isStationary = !hasTouchMovedRef.current;
|
||||
|
||||
// Only open modal for quick taps that didn't move significantly
|
||||
if (isQuickTap && isStationary) {
|
||||
touchOpenHandledRef.current = true;
|
||||
void handleClick();
|
||||
}
|
||||
|
||||
// Reset touch tracking
|
||||
touchStartPosRef.current = null;
|
||||
hasTouchMovedRef.current = false;
|
||||
}, [handleClick, isInteractiveTarget]);
|
||||
|
||||
const handleDepClick = useCallback(async (e: React.MouseEvent, depId: string) => {
|
||||
e.stopPropagation(); // Prevent card click
|
||||
@@ -502,6 +560,10 @@ function TaskCardComponent({
|
||||
onDragOver={handleFileDragOver}
|
||||
onDragLeave={handleFileDragLeave}
|
||||
onDrop={handleFileDrop}
|
||||
onClick={handleCardClick}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
<div className="card-header">
|
||||
@@ -532,14 +594,6 @@ function TaskCardComponent({
|
||||
/>
|
||||
)}
|
||||
<div className="card-header-actions">
|
||||
<button
|
||||
className="card-expand-btn"
|
||||
onClick={handleExpandClick}
|
||||
title="Open task details"
|
||||
aria-label="Open task details"
|
||||
>
|
||||
<Maximize2 size={12} />
|
||||
</button>
|
||||
{canEdit && (
|
||||
<button
|
||||
className="card-edit-btn"
|
||||
@@ -678,5 +732,8 @@ function TaskCardComponent({
|
||||
);
|
||||
}
|
||||
|
||||
const TOUCH_MOVE_THRESHOLD = 10; // pixels
|
||||
const TOUCH_TAP_MAX_DURATION = 300; // milliseconds
|
||||
|
||||
export const TaskCard = memo(TaskCardComponent, areTaskCardPropsEqual);
|
||||
TaskCard.displayName = "TaskCard";
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface WorktreeGroupData {
|
||||
|
||||
/**
|
||||
* Extract a clean display name from a worktree path.
|
||||
* e.g. ".worktrees/FN-001" → "FN-001", "/path/to/fusion/fn-001" → "fn-001"
|
||||
* e.g. ".worktrees/KB-001" → "KB-001", "/path/to/kb/kb-001" → "kb-001"
|
||||
*/
|
||||
export function getWorktreeLabel(worktreePath: string): string {
|
||||
// Take the last segment of the path
|
||||
|
||||
@@ -3316,7 +3316,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
}
|
||||
|
||||
// Determine branch name from task
|
||||
const branchName = `fusion/${task.id.toLowerCase()}`;
|
||||
const branchName = `kb/${task.id.toLowerCase()}`;
|
||||
|
||||
// Get owner/repo from git remote or GITHUB_REPOSITORY env
|
||||
let owner: string;
|
||||
|
||||
Reference in New Issue
Block a user