From 8e4134e0f989b94e338643a54893a2487ce27f5c Mon Sep 17 00:00:00 2001 From: idevlab Date: Tue, 1 Sep 2026 14:42:41 +0800 Subject: [PATCH 1/2] feat(desktop): complete taskboard attention interactions --- apps/desktop/src/App.tsx | 186 +++++++--- apps/desktop/src/components/ui/command.tsx | 11 +- apps/desktop/src/i18n/strings.ts | 52 +++ apps/desktop/src/palette/CommandPalette.tsx | 125 +++++-- apps/desktop/src/palette/merge.ts | 19 + apps/desktop/src/session/QuestionDialog.tsx | 102 ++++-- .../desktop/src/taskboard/TaskBoardHeader.tsx | 41 ++- .../taskboard/TaskBoardInspectorSurface.tsx | 53 +++ apps/desktop/src/taskboard/TaskBoardList.tsx | 24 +- apps/desktop/src/taskboard/TaskBoardPage.tsx | 189 ++++++---- .../src/taskboard/TaskBoardPendingInput.tsx | 84 +++++ apps/desktop/src/taskboard/TaskInspector.tsx | 39 ++- .../src/taskboard/TaskInspectorAgent.tsx | 245 ++++++------- .../src/taskboard/TaskInspectorSummary.tsx | 131 ++++--- apps/desktop/src/taskboard/TaskListItem.tsx | 1 + apps/desktop/src/taskboard/task-board.css | 8 + .../src/taskboard/taskBoardContinuation.ts | 80 +++++ .../src/taskboard/useTaskBoardActions.ts | 35 +- .../desktop/src/taskboard/useTaskBoardData.ts | 27 +- .../src/taskboard/useTaskBoardKeyboard.ts | 94 +++++ .../src/taskboard/useTaskBoardSelection.ts | 4 +- .../src/taskboard/useTaskBoardTranscript.ts | 47 +++ .../src/taskboard/useTaskBoardViewport.ts | 57 +++ .../src/taskboard/useTaskPullRequests.ts | 12 +- apps/desktop/src/taskboard/workspaceTypes.ts | 30 +- .../tests/commandPaletteRendered.test.tsx | 53 +++ apps/desktop/tests/sessionState.test.ts | 24 +- apps/desktop/tests/taskBoard.test.ts | 75 ++++ apps/desktop/tests/taskBoardRendered.test.tsx | 327 +++++++++++++++++- .../change.md | 84 ++++- .../evidence/task-board-dark.png | Bin 63755 -> 43760 bytes .../evidence/task-board-light.png | Bin 65504 -> 71641 bytes .../evidence/task-board-narrow.png | Bin 54638 -> 38552 bytes 33 files changed, 1855 insertions(+), 404 deletions(-) create mode 100644 apps/desktop/src/taskboard/TaskBoardInspectorSurface.tsx create mode 100644 apps/desktop/src/taskboard/TaskBoardPendingInput.tsx create mode 100644 apps/desktop/src/taskboard/taskBoardContinuation.ts create mode 100644 apps/desktop/src/taskboard/useTaskBoardKeyboard.ts create mode 100644 apps/desktop/src/taskboard/useTaskBoardTranscript.ts create mode 100644 apps/desktop/src/taskboard/useTaskBoardViewport.ts diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index e14a3e86..5912ada3 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -242,6 +242,7 @@ import { ProjectIcon } from "./projects/ProjectIcon"; import { SourceControlModal } from "./git/SourceControl"; import { workspaceStateForCwd, type WorkspaceLoadState } from "./git/state"; import { CommandPalette, type Command } from "./palette/CommandPalette"; +import { currentFirstSessionHits } from "./palette/merge"; import { RemoteModal } from "./remote/Remote"; import { IssuesModal } from "./issues/Issues"; import { PreviewModal } from "./editor/Preview"; @@ -420,6 +421,11 @@ import { unlinkTaskPullRequest, type BoardTask, } from "./taskboard/taskBoard"; +import { + continueTaskBoardPrompt, + taskBoardTranscriptPreview, +} from "./taskboard/taskBoardContinuation"; +import type { TaskBoardTranscriptPreview } from "./taskboard/workspaceTypes"; import { actionForEvent, @@ -983,7 +989,7 @@ export default function App() { // Split whichever pane is passed (focusing it first, so the reducer's focused-relative split // lands on that pane), seeding an empty draft for the new leaf. const splitPaneById = useCallback( - (targetId: string, edge: PaneEdge) => { + (targetId: string, edge: PaneEdge): string => { const id = nextPaneId(); const nextContents = { ...paneContentsRef.current, @@ -1001,6 +1007,7 @@ export default function App() { activeSessionRef.current = null; activeSessionProvenanceRef.current = null; setPaneLayout((layout) => splitFocused(focusPane(layout, targetId), edge, id)); + return id; }, [nextPaneId, updatePaneTranscriptState], ); @@ -2703,6 +2710,13 @@ export default function App() { ], [archivedSessions, runningSessions, sessions], ); + const loadTaskBoardTranscript = useCallback( + async (sessionId: string): Promise => { + const page = await getTranscriptPage(sessionId, null, 12); + return taskBoardTranscriptPreview(page.entries); + }, + [], + ); const quickQuotaProvider = useMemo(() => { const focused = [...sessions, ...archivedSessions].find( @@ -4541,44 +4555,70 @@ export default function App() { [createSession, setDocMode, setTaskContext, t], ); + const answerPermissionRequest = useCallback( + async (request: PermissionQueueItem, optionId: string | null): Promise => { + try { + const accepted = await answerPermission( + request.session, + request.requestId, + optionId, + ); + setPermissionQueue((previous) => + permissionQueueAfterAnswer( + previous, + request.session, + request.requestId, + accepted, + ), + ); + if (!accepted) toast(t("taskboard.answerNotAccepted"), "error"); + return accepted; + } catch { + toast(t("taskboard.answerFailed"), "error"); + return false; + } + }, + [t, toast], + ); + + const answerElicitationRequest = useCallback( + async (request: PermissionQueueItem, value: ElicitationAnswer): Promise => { + try { + const accepted = await answerElicitation( + request.session, + request.requestId, + value, + ); + setPermissionQueue((previous) => + permissionQueueAfterAnswer( + previous, + request.session, + request.requestId, + accepted, + ), + ); + if (!accepted) toast(t("taskboard.answerNotAccepted"), "error"); + return accepted; + } catch { + toast(t("taskboard.answerFailed"), "error"); + return false; + } + }, + [t, toast], + ); + const answer = useCallback( async (optionId: string | null) => { - if (!permission) return; - const accepted = await answerPermission( - permission.session, - permission.requestId, - optionId, - ); - setPermissionQueue((previous) => - permissionQueueAfterAnswer( - previous, - permission.session, - permission.requestId, - accepted, - ), - ); + if (permission) await answerPermissionRequest(permission, optionId); }, - [permission], + [answerPermissionRequest, permission], ); const answerQuestion = useCallback( async (value: ElicitationAnswer) => { - if (!permission) return; - const accepted = await answerElicitation( - permission.session, - permission.requestId, - value, - ); - setPermissionQueue((previous) => - permissionQueueAfterAnswer( - previous, - permission.session, - permission.requestId, - accepted, - ), - ); + if (permission) await answerElicitationRequest(permission, value); }, - [permission], + [answerElicitationRequest, permission], ); /** @@ -4910,6 +4950,36 @@ export default function App() { return () => dispose?.(); }, [selectSession]); + const splitTaskBoardSession = useCallback( + (sessionId: string, edge: "right" | "bottom") => { + const existingPane = paneBoundToSession(paneContentsRef.current, sessionId); + setShowTaskBoard(false); + if (existingPane) { + void selectSession(sessionId, existingPane); + return; + } + const paneId = splitPaneById(focusedPaneRef.current, edge); + void selectSession(sessionId, paneId); + }, + [selectSession, splitPaneById], + ); + + const forkTaskBoardSession = useCallback( + (sessionId: string, throughSeq: number, title: string) => { + const paneId = focusedPaneRef.current; + const insertSession = paneEditorRefsFor(paneId).insertSessionRef.current; + if (!Number.isSafeInteger(throughSeq) || throughSeq <= 0 || !insertSession) { + toast(t("turn.forkFailed"), "error"); + return; + } + setShowTaskBoard(false); + if (!createTaskDraft()) return; + insertSession({ id: sessionId, title, throughSeq }); + toast(t("turn.forked"), "success"); + }, + [createTaskDraft, paneEditorRefsFor, t, toast], + ); + const activatePaneById = useCallback( (paneId: string) => { const session = paneContentsRef.current[paneId]?.sessionId ?? null; @@ -5052,7 +5122,11 @@ export default function App() { const searchPaletteCommands = useCallback( async (query: string): Promise => { const hits = await searchSessions(query, 12); - return hits.map((hit) => { + const uniqueHits = currentFirstSessionHits( + [...new Map(hits.map((hit) => [hit.session_id, hit])).values()], + activeSession, + ); + return uniqueHits.map((hit) => { const stored = sessions.find((session) => session.id === hit.session_id) ?? archivedSessions.find((session) => session.id === hit.session_id); @@ -5067,13 +5141,24 @@ export default function App() { category: "session", label: hit.title, detail: `${t(hit.role === "user" ? "palette.you" : "palette.agent")}: ${hit.snippet}`, - hint: hit.archived ? t("palette.archived") : project, + hint: hit.session_id === activeSession + ? t("palette.current") + : hit.archived + ? t("palette.archived") + : project, keywords: `${sourcePath} ${hit.cwd}`, + preview: { + title: hit.title, + body: hit.snippet, + context: project, + current: hit.session_id === activeSession, + archived: hit.archived, + }, run: () => void selectSession(hit.session_id), }; }); }, - [projects, sessions, archivedSessions, selectSession, t], + [activeSession, projects, sessions, archivedSessions, selectSession, t], ); // Skills depend on the workspace: harness skill directories (.claude/skills …) are rescanned @@ -7755,19 +7840,36 @@ export default function App() { {showTaskBoard && ( { setShowTaskBoard(false); void selectSession(id); }} onAskSession={(id, prompt) => { - setShowTaskBoard(false); - void selectSession(id).then(() => { - clearEditorRef.current?.(); - setDocMode(true); - setTimeout(() => { - void insertMarkdownRef.current?.(prompt, "replace"); - focusEditorRef.current?.(); - }, 0); + const paneId = + paneBoundToSession(paneContentsRef.current, id) ?? + focusedPaneRef.current; + return continueTaskBoardPrompt({ + target: { paneId, sessionId: id }, + prompt, + selectSession: (sessionId, targetPaneId) => + selectSession(sessionId, targetPaneId, true, true), + isTargetActive: () => + focusedPaneRef.current === paneId && + paneContentsRef.current[paneId]?.sessionId === id, + openDocumentMode: () => setDocMode(true), + insertMarkdown: (markdown, mode) => + paneEditorRefsFor(paneId).insertMarkdownRef.current?.( + markdown, + mode, + ) ?? Promise.resolve(), + // Keep TaskBoard active; the draft is staged until the Session is opened. + focusEditor: () => undefined, }); }} onStartTask={startBoardTask} diff --git a/apps/desktop/src/components/ui/command.tsx b/apps/desktop/src/components/ui/command.tsx index 73c78056..77d6d2a7 100644 --- a/apps/desktop/src/components/ui/command.tsx +++ b/apps/desktop/src/components/ui/command.tsx @@ -33,6 +33,8 @@ function CommandDialog({ children, className, showCloseButton = true, + commandValue, + onCommandValueChange, ...props }: Omit, "children"> & { children: React.ReactNode @@ -40,6 +42,8 @@ function CommandDialog({ description?: string className?: string showCloseButton?: boolean + commandValue?: string + onCommandValueChange?: (value: string) => void }) { return ( @@ -51,7 +55,12 @@ function CommandDialog({ {title} {description} - + {children} diff --git a/apps/desktop/src/i18n/strings.ts b/apps/desktop/src/i18n/strings.ts index c71f2f8e..d2640f84 100644 --- a/apps/desktop/src/i18n/strings.ts +++ b/apps/desktop/src/i18n/strings.ts @@ -462,6 +462,9 @@ export const en = { "palette.you": "You", "palette.agent": "Agent", "palette.archived": "Archived", + "palette.current": "Current", + "palette.preview": "Session search preview", + "palette.readOnlyPreview": "Read-only preview", // composer "composer.placeholder": "Write your prompt — / for skills, @ for files", @@ -1981,6 +1984,7 @@ export const en = { "taskboard.title": "Task board", "taskboard.description": "Plan, advance, and deliver your work", "taskboard.allTasks": "All tasks", + "taskboard.views": "Task views", "taskboard.workspaceDescription": "Plan, execute, and track work with Sessions. Each Session owns at most one current pull request.", "taskboard.breadcrumb": "Task board breadcrumb", "taskboard.titleHeader": "Title", @@ -1997,11 +2001,13 @@ export const en = { "taskboard.currentSession": "Current", "taskboard.archivedSession": "Archived", "taskboard.openPullRequestCount": "{count} open pull requests", + "taskboard.openPullRequestCountPending": "Open pull request count is still loading", "taskboard.noSessions": "No Sessions", "taskboard.noSessionsDescription": "Start this Task to create its first execution Session.", "taskboard.noCheckout": "No checkout", "taskboard.worktreeDiscarded": "Worktree discarded", "taskboard.noPullRequest": "No PR", + "taskboard.checkingPullRequest": "Checking PR…", "taskboard.noPullRequestForSession": "This Session's checkout has no current pull request.", "taskboard.inspector": "Task inspector", "taskboard.inspectorViews": "Inspector views", @@ -2010,8 +2016,10 @@ export const en = { "taskboard.inspector.insights": "Insights", "taskboard.showInspector": "Show inspector", "taskboard.hideInspector": "Hide inspector", + "taskboard.closeInspectorDrawer": "Close inspector and return to Task list", "taskboard.selectTask": "Select a Task to inspect it.", "taskboard.currentSessionTitle": "Current Session", + "taskboard.selectedSession": "Selected Session", "taskboard.taskLabel": "Task", "taskboard.activityLabel": "Activity", "taskboard.checkoutTitle": "Checkout", @@ -2027,6 +2035,23 @@ export const en = { "taskboard.askAgentPlaceholder": "Add a prompt and continue in this Session…", "taskboard.openSession": "Open Session", "taskboard.continueWithPrompt": "Continue with prompt", + "taskboard.promptFailed": "The prompt could not be staged. Your draft is still here.", + "taskboard.transcript.title": "Recent transcript", + "taskboard.transcript.loading": "Loading transcript…", + "taskboard.transcript.failed": "The transcript preview is unavailable.", + "taskboard.transcript.empty": "No durable transcript text yet.", + "taskboard.transcript.you": "You", + "taskboard.transcript.agent": "Agent", + "taskboard.attention.actionRequired": "Action required", + "taskboard.answerNotAccepted": "The request changed before Core accepted that answer. Your draft is still here.", + "taskboard.answerFailed": "Core could not accept that answer. Your draft is still here.", + "taskboard.forkFromPreview": "Fork from preview", + "taskboard.splitRight": "Split right", + "taskboard.splitBelow": "Split below", + "taskboard.keyboardHints": "Task list keyboard shortcuts", + "taskboard.keyboard.select": "Select", + "taskboard.keyboard.preview": "Preview", + "taskboard.keyboard.open": "Open", "taskboard.taskDetails": "Task details", "taskboard.noDescription": "No description", "taskboard.relationshipTitle": "Relationship", @@ -2048,6 +2073,7 @@ export const en = { "taskboard.lane.needsYou": "Needs you", "taskboard.lane.done": "Done", "taskboard.attentionSummary": "{count} tasks need your attention", + "taskboard.attentionEmpty": "Nothing needs your attention", "taskboard.cardAction": "{action}: {title}", "taskboard.readyToContinue": "Ready to continue", "taskboard.runningNow": "Running", @@ -3121,6 +3147,9 @@ export const zhCN: Record = { "palette.you": "你", "palette.agent": "代理", "palette.archived": "已归档", + "palette.current": "当前", + "palette.preview": "Session 搜索预览", + "palette.readOnlyPreview": "只读预览", "composer.placeholder": "写下你的提示词 — / 插入技能, @ 引用文件", "composer.documentInput": "提示词文档", @@ -4549,6 +4578,7 @@ export const zhCN: Record = { "taskboard.title": "任务看板", "taskboard.description": "规划、推进并交付你的工作", "taskboard.allTasks": "全部任务", + "taskboard.views": "任务视图", "taskboard.workspaceDescription": "通过 Session 规划、执行并跟踪工作;每个 Session 最多对应一个当前 pull request。", "taskboard.breadcrumb": "任务看板路径", "taskboard.titleHeader": "标题", @@ -4565,11 +4595,13 @@ export const zhCN: Record = { "taskboard.currentSession": "当前", "taskboard.archivedSession": "已归档", "taskboard.openPullRequestCount": "{count} 个打开的 pull request", + "taskboard.openPullRequestCountPending": "正在加载打开的 pull request 数量", "taskboard.noSessions": "暂无 Session", "taskboard.noSessionsDescription": "开始这个任务以创建第一个执行 Session。", "taskboard.noCheckout": "没有检出目录", "taskboard.worktreeDiscarded": "Worktree 已丢弃", "taskboard.noPullRequest": "无 PR", + "taskboard.checkingPullRequest": "正在检查 PR…", "taskboard.noPullRequestForSession": "这个 Session 的检出目录没有当前 pull request。", "taskboard.inspector": "任务检查器", "taskboard.inspectorViews": "检查器视图", @@ -4578,8 +4610,10 @@ export const zhCN: Record = { "taskboard.inspector.insights": "洞察", "taskboard.showInspector": "显示检查器", "taskboard.hideInspector": "隐藏检查器", + "taskboard.closeInspectorDrawer": "关闭检查器并返回任务列表", "taskboard.selectTask": "选择一个任务以查看详情。", "taskboard.currentSessionTitle": "当前 Session", + "taskboard.selectedSession": "选中的 Session", "taskboard.taskLabel": "任务", "taskboard.activityLabel": "状态", "taskboard.checkoutTitle": "检出目录", @@ -4595,6 +4629,23 @@ export const zhCN: Record = { "taskboard.askAgentPlaceholder": "添加提示并在这个 Session 中继续…", "taskboard.openSession": "打开 Session", "taskboard.continueWithPrompt": "带提示继续", + "taskboard.promptFailed": "无法暂存这条提示;你的草稿仍保留在这里。", + "taskboard.transcript.title": "最近对话", + "taskboard.transcript.loading": "正在加载对话…", + "taskboard.transcript.failed": "暂时无法预览对话。", + "taskboard.transcript.empty": "还没有可预览的持久化对话文本。", + "taskboard.transcript.you": "你", + "taskboard.transcript.agent": "Agent", + "taskboard.attention.actionRequired": "需要你操作", + "taskboard.answerNotAccepted": "在 Core 接受前,请求已发生变化;你的草稿仍保留在这里。", + "taskboard.answerFailed": "Core 无法接受这个答案;你的草稿仍保留在这里。", + "taskboard.forkFromPreview": "从预览分叉", + "taskboard.splitRight": "向右分屏", + "taskboard.splitBelow": "向下分屏", + "taskboard.keyboardHints": "任务列表键盘快捷键", + "taskboard.keyboard.select": "选择", + "taskboard.keyboard.preview": "预览", + "taskboard.keyboard.open": "打开", "taskboard.taskDetails": "任务详情", "taskboard.noDescription": "暂无描述", "taskboard.relationshipTitle": "关系", @@ -4616,6 +4667,7 @@ export const zhCN: Record = { "taskboard.lane.needsYou": "需要你处理", "taskboard.lane.done": "已完成", "taskboard.attentionSummary": "有 {count} 项任务需要你处理", + "taskboard.attentionEmpty": "当前没有需要你处理的任务", "taskboard.cardAction": "{action}:{title}", "taskboard.readyToContinue": "可继续处理", "taskboard.runningNow": "正在运行", diff --git a/apps/desktop/src/palette/CommandPalette.tsx b/apps/desktop/src/palette/CommandPalette.tsx index 9c7c0472..1b4f387b 100644 --- a/apps/desktop/src/palette/CommandPalette.tsx +++ b/apps/desktop/src/palette/CommandPalette.tsx @@ -16,6 +16,14 @@ import { mergeCommandResults } from "./merge"; export type CommandCategory = "action" | "session" | "setting"; +export interface CommandPreview { + title: string; + body: string; + context?: string; + current?: boolean; + archived?: boolean; +} + export interface Command { id: string; /** Stable entity identity lets a richer async result replace its metadata-only row. */ @@ -25,6 +33,7 @@ export interface Command { hint?: string; detail?: string; keywords?: string; + preview?: CommandPreview; run: () => void; } @@ -43,6 +52,7 @@ export function CommandPalette({ const [matches, setMatches] = useState([]); const [searchState, setSearchState] = useState<"idle" | "pending" | "loading" | "success" | "error">("idle"); const [filter, setFilter] = useState<"all" | CommandCategory>("all"); + const [selectedId, setSelectedId] = useState(commands[0]?.id ?? ""); useEffect(() => { const value = query.trim(); @@ -114,6 +124,19 @@ export function CommandPalette({ : searchState === "pending" || searchState === "loading" ? t("palette.searching") : null; + const listedCommands = useMemo( + () => groups.flatMap((group) => group.commands), + [groups], + ); + const selectedCommand = listedCommands.find((command) => command.id === selectedId) + ?? listedCommands[0] + ?? null; + + useEffect(() => { + if (selectedCommand && selectedCommand.id !== selectedId) { + setSelectedId(selectedCommand.id); + } + }, [selectedCommand, selectedId]); return ( ))} - - {searchStatus ? null : t("palette.empty")} - {groups.map((group) => ( - + + {searchStatus ? null : t("palette.empty")} + {groups.map((group) => ( + + {group.commands.map((command) => ( + setSelectedId(command.id)} + onMouseMove={() => setSelectedId(command.id)} + onSelect={() => { + onClose(); + command.run(); + }} + > + + {command.label} + {command.detail && ( + {command.detail} + )} + + {command.hint && {command.hint}} + + ))} + + ))} + {searchStatus && (filter === "all" || filter === "session") && ( +

+ {searchStatus} +

+ )} +
+ {selectedCommand?.preview ? ( + + ) : null} +
↑↓ {t("palette.navigate")} diff --git a/apps/desktop/src/palette/merge.ts b/apps/desktop/src/palette/merge.ts index 849b13f8..7a1b1229 100644 --- a/apps/desktop/src/palette/merge.ts +++ b/apps/desktop/src/palette/merge.ts @@ -14,3 +14,22 @@ export function mergeCommandResults(base: T[], matc const ids = new Set(remaining.map((command) => command.id)); return [...remaining, ...matches.filter((command) => !ids.has(command.id))]; } + +interface SessionSearchRankable { + session_id: string; + archived: boolean; +} + +/** Keep backend relevance stable except for the two product-level navigation priorities. */ +export function currentFirstSessionHits( + hits: readonly T[], + currentSession: string | null, +): T[] { + return [...hits].sort((left, right) => { + const leftCurrent = left.session_id === currentSession; + const rightCurrent = right.session_id === currentSession; + if (leftCurrent !== rightCurrent) return leftCurrent ? -1 : 1; + if (left.archived !== right.archived) return left.archived ? 1 : -1; + return 0; + }); +} diff --git a/apps/desktop/src/session/QuestionDialog.tsx b/apps/desktop/src/session/QuestionDialog.tsx index d95d92d2..a1768aa7 100644 --- a/apps/desktop/src/session/QuestionDialog.tsx +++ b/apps/desktop/src/session/QuestionDialog.tsx @@ -173,51 +173,91 @@ export function QuestionDialog({ }: { form: ElicitationForm; onAnswer: (answer: ElicitationAnswer) => void; +}) { + return ( + !open && onAnswer({ action: "cancel" })}> + + + + + ); +} + +/** + * The shared, stateful elicitation body. TaskBoard embeds this directly so an unanswered draft + * stays mounted when Core rejects or fails an answer; the chat wraps the same body in a dialog. + */ +export function QuestionForm({ + form, + onAnswer, + embedded = false, +}: { + form: ElicitationForm; + onAnswer: ( + answer: ElicitationAnswer, + ) => boolean | void | Promise; + embedded?: boolean; }) { const t = useT(); const [values, setValues] = useState({}); + const [answering, setAnswering] = useState(false); const questions = questionFields(form); // With one question the message *is* the question, so repeating it above the options would just // read as the same sentence twice. const single = questions.length === 1; + const answer = async (value: ElicitationAnswer) => { + if (answering) return; + setAnswering(true); + try { + await onAnswer(value); + } finally { + setAnswering(false); + } + }; + return ( - !open && onAnswer({ action: "cancel" })}> - - +
+ + {embedded ? ( +

+ + {form.message} +

+ ) : ( {form.message} -
+ )} + -
- {questions.map((field) => ( - - ))} -
+
+ {questions.map((field) => ( + + ))} +
- - - - - - -
+ + + + + +
); } diff --git a/apps/desktop/src/taskboard/TaskBoardHeader.tsx b/apps/desktop/src/taskboard/TaskBoardHeader.tsx index cf89c298..97eccc93 100644 --- a/apps/desktop/src/taskboard/TaskBoardHeader.tsx +++ b/apps/desktop/src/taskboard/TaskBoardHeader.tsx @@ -17,10 +17,16 @@ import type { Translate } from "@/i18n" import { PRIORITIES, type TaskPriority } from "./taskBoard" import { taskPriorityLabel } from "./TaskEditorDialog" +import type { TaskBoardView } from "./workspaceTypes" interface TaskBoardHeaderProps { t: Translate taskCount: number + pageTitle: string + pageDescription: string + view: TaskBoardView + attentionCount: number + onViewChange: (view: TaskBoardView) => void headerLeadingAction?: ReactNode inspectorOpen: boolean onShowInspector: () => void @@ -49,11 +55,12 @@ export function TaskBoardHeader(props: TaskBoardHeaderProps) {
{!props.inspectorOpen ? ( + +
void + onOpenChange: (open: boolean) => void +} + +export function TaskBoardInspectorSurface(props: TaskBoardInspectorSurfaceProps) { + const contents = ( + <> + + + + ) + + if (props.isNarrow) { + return ( + + + {props.t("taskboard.inspector")} + {contents} + + + ) + } + + return props.inspectorOpen ? ( + + ) : null +} diff --git a/apps/desktop/src/taskboard/TaskBoardList.tsx b/apps/desktop/src/taskboard/TaskBoardList.tsx index a86e2c45..5da9474e 100644 --- a/apps/desktop/src/taskboard/TaskBoardList.tsx +++ b/apps/desktop/src/taskboard/TaskBoardList.tsx @@ -5,11 +5,12 @@ import type { SidebarPullRequestStatus } from "@/sidebar/sidebarGitStatus" import { TaskListItem } from "./TaskListItem" import type { BoardTask, TaskStatus } from "./taskBoard" import { INITIAL_TASK_LIMIT } from "./workspaceModel" -import type { ProjectedTask } from "./workspaceTypes" +import type { ProjectedTask, TaskBoardView } from "./workspaceTypes" interface TaskBoardListProps { t: Translate locale: Locale + view: TaskBoardView projectedTasks: readonly ProjectedTask[] renderedTasks: readonly ProjectedTask[] remainingTaskCount: number @@ -39,7 +40,11 @@ export function TaskBoardList(props: TaskBoardListProps) { {t("taskboard.updatedHeader")} {props.renderedTasks.length > 0 ? ( -