import { useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from 'react' import type { FormEvent, KeyboardEvent, ReactNode, RefObject } from 'react' import type { Context } from '@deepseek-ai/cordis' import type { TypertClientRemote } from '@deepseek-ai/dsh-typert-protocol' import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type {} from '@deepseek-ai/dsh-client-ui-layout/client' import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client' import type { AutomationRule, AutomationRun, SavedWorkflow, TaskboardChangeWatchResult, TaskboardProject, TaskboardTask, TaskDetail as TaskDetailData, WorkflowDocument, } from '../domain/index.js' import { TASK_STATUSES } from '../domain/index.js' import type { TaskboardSnapshot } from '../service/index.js' import { addWorkflowTab, copyWorkflowNode, insertWorkflowNode, moveWorkflowNode, removeWorkflowNode, removeWorkflowTab, } from '../workflow/index.js' import { applyAutomationDefaults, BOARD_COLUMN_PAGE_SIZE, boardDropIntent, createdTaskId, descriptionComposerMode, humanQuickCreateRequest, isPreviewableAttachment, observeSnapshot, paginateBoardColumn, previewAutomationRuns, projectLabelCatalog, sortTaskList, TaskboardClientController, tasksForLabel, type TaskboardConnection, type TaskListSortKey } from './controller.js' import { PopoverShell, useExclusivePopover } from './popover.js' import { applyMarkdownEdit, parseMarkdown, type MarkdownBlock, type MarkdownEditAction, type MarkdownInline } from './markdown.js' import { bindTaskboardLocale, currentTaskboardLanguage, formatAutomationLog, formatOpenedAt, interpolate, priorityLabel, subscribeTaskboardLocale, TASKBOARD_LOCALE_NS, taskboardLocales, taskboardStrings, type TaskboardCopy, type TaskboardLocaleRuntime, } from './locales.js' import taskboardRemote from '../../generated/typert.remote-client.js' export { bindTaskboardLocale, taskboardStrings } from './locales.js' export const inject = ['slots', 'connection', 'sessions', 'workspaces', 'uiWorkspace', 'conversation', 'remote', 'locale'] interface InjectedProps { controller: TaskboardClientController } interface WorkspaceOption { readonly workspaceId: string readonly title: string readonly path: string } interface IWorkspaces { readonly list: { subscribe(listener: () => void): () => void getSnapshot(): { readonly items: readonly WorkspaceOption[] } } } interface ISessions { readonly list: { getSnapshot(): { readonly current?: string; readonly byId: Readonly> } subscribe(listener: () => void): () => void } refresh(): Promise open(sessionId: string): void scope(sessionId: string): unknown } interface ISlots { inject(key: string, callback: () => unknown): unknown register(options: { name: string; id: string }, component: unknown): unknown } interface UiWorkspace { connectWorkspace(workspaceId: string): Promise } interface PageInjectedProps extends InjectedProps { workspaces: IWorkspaces } interface TaskSessionNavigator { readonly list: { getSnapshot(): { readonly byId: Readonly> } } refresh(): Promise open(sessionId: string): void } /** A disposed automation Agent becomes a persisted cold Session. Refresh the native list before * selecting it: sessions.open intentionally rejects ids absent from the current list snapshot. */ export async function openTaskSession(navigator: TaskSessionNavigator, sessionId: string): Promise { if (navigator.list.getSnapshot().byId[sessionId] === undefined) await navigator.refresh() if (navigator.list.getSnapshot().byId[sessionId] === undefined) { throw new Error(`Session ${sessionId} is unavailable`) } navigator.open(sessionId) } /** Narrow structural face used at the plugin boundary; the service is provided by dsh-client-ui-conversation. */ interface ConversationDraftPort { readonly input: { for(ctx: unknown): { setDraft(text: string): void } } } type NavProps = PropsRuntime<'sidebar.footer.action'> & InjectedProps type PageProps = PropsRuntime<'shell.overlay'> & PageInjectedProps function useStrings(): TaskboardCopy { return taskboardStrings(useSyncExternalStore(subscribeTaskboardLocale, currentTaskboardLanguage, currentTaskboardLanguage)) } /** Kanban glyph in the DSH filled-outline family (same optical weight as the settings gear). */ function TaskboardIcon({ size }: { size: number }) { return ( ) } /** Filled floppy-disk glyph so the primary Save action stays recognizable at small sizes. */ function SaveIcon({ size }: { size: number }) { return ( ) } /** 14px stroke X matching the Harness settings/modal close glyph. */ function CloseIcon({ size }: { size: number }) { return ( ) } /** Downward chevron used as the board-column lazy-load affordance. */ function MoreIcon({ size }: { size: number }) { return ( ) } function actorName(value: string): string { return value.split(/[:/]/).pop() || value } function actorInitial(value: string): string { return (actorName(value).trim().slice(0, 1) || '?').toUpperCase() } function isClosedStatus(status: TaskboardTask['status']): boolean { return status === 'done' || status === 'canceled' } /** Backoff before the change poll is retried, and the idle delay before a truncated project's * search reaches SQLite. */ const WATCH_RETRY_MS = 2_000 const SEARCH_DEBOUNCE_MS = 250 function pause(ms: number, signal: AbortSignal): Promise { return new Promise(resolve => { const timer = window.setTimeout(() => { signal.removeEventListener('abort', onAbort); resolve() }, ms) function onAbort(): void { window.clearTimeout(timer); resolve() } signal.addEventListener('abort', onAbort, { once: true }) }) } const MARKDOWN_TOOLBAR: readonly { action: MarkdownEditAction; label: 'mdHeading' | 'mdBold' | 'mdItalic' | 'mdQuote' | 'mdCode' | 'mdLink' | 'mdBullet' | 'mdNumber'; glyph: string }[] = [ { action: 'heading', label: 'mdHeading', glyph: 'H' }, { action: 'bold', label: 'mdBold', glyph: 'B' }, { action: 'italic', label: 'mdItalic', glyph: 'I' }, { action: 'quote', label: 'mdQuote', glyph: '“' }, { action: 'code', label: 'mdCode', glyph: '' }, { action: 'link', label: 'mdLink', glyph: '[]' }, { action: 'ul', label: 'mdBullet', glyph: '•' }, { action: 'ol', label: 'mdNumber', glyph: '1.' }, ] function ComposerTabs({ mode, onChange }: { mode: 'write' | 'preview'; onChange: (mode: 'write' | 'preview') => void }) { const t = useStrings() return (
) } function MarkdownComposer({ value, onChange, mode, onModeChange, placeholder, emptyPreview, }: { value: string onChange: (value: string) => void mode: 'write' | 'preview' onModeChange: (mode: 'write' | 'preview') => void placeholder: string emptyPreview: ReactNode }) { const t = useStrings() const textareaRef = useRef(null) const run = (action: MarkdownEditAction): void => { const field = textareaRef.current const next = applyMarkdownEdit(value, field?.selectionStart ?? value.length, field?.selectionEnd ?? value.length, action) onChange(next.value) requestAnimationFrame(() => { textareaRef.current?.focus() textareaRef.current?.setSelectionRange(next.selectionStart, next.selectionEnd) }) } const onKeyDown = (event: KeyboardEvent): void => { if (!(event.metaKey || event.ctrlKey) || event.altKey) return const key = event.key.toLowerCase() const action = key === 'b' ? 'bold' : key === 'i' ? 'italic' : key === 'k' ? 'link' : key === 'e' ? 'code' : undefined if (action === undefined) return event.preventDefault() run(action) } return ( <>
{mode === 'write' && (
{MARKDOWN_TOOLBAR.map(item => ( ))}
)}
{mode === 'write' ?