From e6aebdc316b72d401bd1f354db3b57b5ce885560 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Mon, 22 Jun 2026 04:37:28 -0700 Subject: [PATCH] feat(dashboard): full issue/PR preview, floating dock pop-out, header dedups, gm one-row, single-line quick entry - GitHub import preview shows the full issue/PR body (markdown) + metadata (list already returned full bodies; removed client truncation). - Task-detail main view: constrain width (no right cutoff); move 'Back to board' into the gray header far right. - Dock pop-out: smoother touch drag (touch-action:none + captured-element listeners); popping out closes the dock but keeps the floating modal; modal survives dock dismiss. - Remove duplicate inner headers in Git Manager / Activity Log / Dev Server / Secrets (dock + pop-out chrome already titles them); keep the title on mobile narrow; relocate gm Refresh into the section tab strip. - Git Manager tabs: one scrollable row of compact icon-only tabs (max visible, scroll if needed). - List quick entry is single-line (not tall) via singleLine prop; Board unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/dashboard/app/App.tsx | 16 +-- packages/dashboard/app/api/legacy.ts | 14 ++- .../app/components/ActivityLogModal.css | 18 +++ .../app/components/DevServerView.css | 29 +++++ .../app/components/GitHubImportModal.css | 75 ++++++++++++ .../app/components/GitHubImportModal.tsx | 79 ++++++++++--- .../app/components/GitManagerModal.tsx | 40 +++---- .../dashboard/app/components/ListView.tsx | 1 + .../app/components/QuickEntryBox.css | 20 ++++ .../app/components/QuickEntryBox.tsx | 32 ++++-- .../dashboard/app/components/RightDock.css | 5 + .../app/components/RightDockExpandModal.tsx | 61 ++++++---- .../dashboard/app/components/ScriptsModal.css | 108 ++++++++++++++++-- .../dashboard/app/components/SecretsView.css | 28 +++++ .../app/components/TaskDetailModal.css | 54 +++++++++ .../app/components/TaskDetailModal.tsx | 20 ++++ .../__tests__/GitHubImportModal.test.tsx | 62 ++++++++-- .../__tests__/QuickEntryBox.test.tsx | 26 +++++ .../components/__tests__/RightDock.test.tsx | 68 ++++++++++- .../__tests__/navigation-history.test.tsx | 14 ++- .../app/components/useRightDockController.tsx | 19 ++- packages/dashboard/app/styles.css | 39 ++----- packages/dashboard/src/github.ts | 27 ++++- 23 files changed, 716 insertions(+), 139 deletions(-) diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx index beca443601..109e3e5d46 100644 --- a/packages/dashboard/app/App.tsx +++ b/packages/dashboard/app/App.tsx @@ -14,7 +14,6 @@ import { Board } from "./components/Board"; import { TaskCard } from "./components/TaskCard"; import { ListView } from "./components/ListView"; import { TaskDetailContent } from "./components/TaskDetailModal"; -import { ArrowLeft } from "lucide-react"; import { ProjectOverview } from "./components/ProjectOverview"; import { MissionManager } from "./components/MissionManager"; import { MailboxView } from "./components/MailboxView"; @@ -2043,22 +2042,17 @@ function AppInner() { return (
-
- -
setMainPanelDetailTask(value)} onMoveTask={moveTask} onDeleteTask={deleteTask} diff --git a/packages/dashboard/app/api/legacy.ts b/packages/dashboard/app/api/legacy.ts index 9e8f16ca43..3fb5bc89db 100644 --- a/packages/dashboard/app/api/legacy.ts +++ b/packages/dashboard/app/api/legacy.ts @@ -2339,12 +2339,19 @@ export function clearApiKey(provider: string): Promise<{ success: boolean }> { // --- GitHub Import API --- /** GitHub issue returned by the fetch endpoint */ +/* +FNXC:GitHubImport 2026-06-22-18:30: +The Import Tasks preview pane renders the FULL issue (full body + metadata), so the list response carries the complete body plus author/state. +The GitHub issue-list endpoint already returns the full (untruncated) `body`; no per-item detail fetch is needed. `author`/`state` are surfaced for the preview metadata row. +*/ export interface GitHubIssue { number: number; title: string; body: string | null; html_url: string; labels: Array<{ name: string }>; + state?: "open" | "closed"; + author?: string | null; } /** Fetch open GitHub issues from a repository */ @@ -2394,7 +2401,10 @@ export function apiBatchImportGitHubIssues( // --- GitHub Pull Request Import API --- -/** GitHub pull request returned by the fetch endpoint */ +/* +FNXC:GitHubImport 2026-06-22-18:30: +The PR-list endpoint already returns the full (untruncated) `body`; the import preview renders it in full with no per-item detail fetch. `state`/`author` surface PR metadata in the preview. +*/ export interface GitHubPull { number: number; title: string; @@ -2402,6 +2412,8 @@ export interface GitHubPull { html_url: string; headBranch: string; baseBranch: string; + state?: "open" | "closed" | "merged"; + author?: string | null; } /** Fetch open GitHub pull requests from a repository */ diff --git a/packages/dashboard/app/components/ActivityLogModal.css b/packages/dashboard/app/components/ActivityLogModal.css index 32c260daa8..4a132bf153 100644 --- a/packages/dashboard/app/components/ActivityLogModal.css +++ b/packages/dashboard/app/components/ActivityLogModal.css @@ -33,6 +33,24 @@ The embedded root is a plain flow box that fills the dock; the inner panel sheds container-name: activity-log-embedded; } +/* +FNXC:RightDockEmbedded 2026-06-22-19:05: +In the right dock the tab strip already labels the view, and in the pop-out the RightDockExpandModal supplies its own header — so the embedded variant's inner "Activity Log" header row (.activity-log-header) is redundant chrome there. Hide it by default in the embedded variant. The header stays in the DOM (not unmounted) so query-by-text/test hooks still resolve; only display is suppressed. The body's flex column fills the freed space since the header was flex-shrink:0. +*/ +.activity-log-modal--embedded .activity-log-header { + display: none; +} + +/* +FNXC:RightDockEmbedded 2026-06-22-19:05: +On real mobile-narrow the view goes full-screen with no dock tab strip or pop-out header, so it must own its own title again. The viewport @media (max-width:768px) fires only on a true narrow viewport (never inside the desktop dock/pop-out, where the @container query drives layout instead), so restoring the header here brings the title back exactly when the chrome is gone. +*/ +@media (max-width: 768px) { + .activity-log-modal--embedded .activity-log-header { + display: flex; + } +} + /* FNXC:RightDockEmbedded 2026-06-22-00:00: Mirror the phone-width (@media max-width:768px) activity-log layout-stacking rules for the narrow dock, scoped to the diff --git a/packages/dashboard/app/components/DevServerView.css b/packages/dashboard/app/components/DevServerView.css index c5c382f970..c519ec02ea 100644 --- a/packages/dashboard/app/components/DevServerView.css +++ b/packages/dashboard/app/components/DevServerView.css @@ -20,6 +20,35 @@ Header migrated to the shared ViewHeader (.view-header), which supplies the --sp FNXC:DevServer 2026-06-22-01:00: .dev-server-header-title now wraps just the status badge inside ViewHeader's actions slot; the mobile flex-wrap rule keeps it from overflowing on narrow widths. */ +/* +FNXC:RightDockEmbedded 2026-06-22-19:05: +DevServerView is a right-dock tool with no --embedded variant; it renders directly inside the dock body +(.right-dock__body) and inside the pop-out (.right-dock-expand-modal__body). In both, the chrome already labels the +view — the dock tab strip names it, and the pop-out's RightDockExpandModal supplies its own header — so the view's own +shared ViewHeader (.view-header) is redundant title chrome there. Hide it in those two host contexts. The header stays +in the DOM; only display is suppressed. The base view is a flex column, so the panels fill the freed space (the header +slot was auto-height, not a fixed reserve). The standalone full-page and Settings-section renders are NOT inside these +ancestors, so their header stays visible. +*/ +.right-dock__body .dev-server-view > .view-header, +.right-dock-expand-modal__body .dev-server-view > .view-header { + display: none; +} + +/* +FNXC:RightDockEmbedded 2026-06-22-19:05: +On real mobile-narrow the view goes full-screen with no dock tab strip or pop-out header, so it must own its title +again. The viewport @media (max-width:768px) fires only on a true narrow viewport (never in the desktop dock/pop-out, +where the @container right-dock-body query drives layout instead), so restoring the header here brings the title back +exactly when the surrounding chrome is gone. +*/ +@media (max-width: 768px) { + .right-dock__body .dev-server-view > .view-header, + .right-dock-expand-modal__body .dev-server-view > .view-header { + display: flex; + } +} + .dev-server-header-title { display: flex; align-items: center; diff --git a/packages/dashboard/app/components/GitHubImportModal.css b/packages/dashboard/app/components/GitHubImportModal.css index a888e782e8..f149af8295 100644 --- a/packages/dashboard/app/components/GitHubImportModal.css +++ b/packages/dashboard/app/components/GitHubImportModal.css @@ -677,6 +677,81 @@ word-break: break-word; } +/* +FNXC:GitHubImport 2026-06-22-18:30: +Full-body preview metadata row (state badge, author, GitHub link) plus the markdown body wrapper. +The markdown variant must NOT pre-wrap/clamp — MailboxMessageContent emits real block elements (p, ul, pre, table), so reset the plain-text white-space and let the body take full height; the preview pane already owns the vertical scroll. +*/ +.preview-metadata { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-sm); + font-size: 12px; + color: var(--text-muted); +} + +.preview-state-badge { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: var(--radius-sm); + font-size: 11px; + font-weight: 600; + text-transform: capitalize; + background: var(--surface); + border: 1px solid var(--border); + color: var(--text); +} + +.preview-state-badge--open { + color: var(--success, var(--accent)); + border-color: color-mix(in srgb, var(--success, var(--accent)) 40%, transparent); +} + +.preview-state-badge--closed { + color: var(--danger, var(--text-muted)); + border-color: color-mix(in srgb, var(--danger, var(--text-muted)) 40%, transparent); +} + +.preview-state-badge--merged { + color: var(--accent); + border-color: color-mix(in srgb, var(--accent) 40%, transparent); +} + +.preview-author { + color: var(--text-muted); +} + +.preview-url { + color: var(--accent); + text-decoration: none; +} + +.preview-url:hover { + text-decoration: underline; +} + +.preview-labels { + display: flex; + flex-wrap: wrap; + gap: var(--space-xs); +} + +.preview-body--markdown { + white-space: normal; + word-break: break-word; + color: var(--text); +} + +.preview-body--markdown :where(p, ul, ol, pre, table, blockquote, h1, h2, h3, h4) { + margin: 0 0 var(--space-sm); +} + +.preview-body--markdown :where(p, ul, ol, pre, table, blockquote, h1, h2, h3, h4):last-child { + margin-bottom: 0; +} + /* Back button - hidden on desktop by default */ .github-import-back-button { display: none; diff --git a/packages/dashboard/app/components/GitHubImportModal.tsx b/packages/dashboard/app/components/GitHubImportModal.tsx index 1e66588eb1..34fe32d204 100644 --- a/packages/dashboard/app/components/GitHubImportModal.tsx +++ b/packages/dashboard/app/components/GitHubImportModal.tsx @@ -15,6 +15,7 @@ import { } from "../api"; import { Loader2, RefreshCw, ArrowLeft, GitPullRequest, CircleDot } from "lucide-react"; import { GithubIcon } from "./GithubIcon"; +import { MailboxMessageContent } from "./MailboxMessageContent"; import { useModalResizePersist } from "../hooks/useModalResizePersist"; import { useMobileScrollLock } from "../hooks/useMobileScrollLock"; import { useOverlayDismiss } from "../hooks/useOverlayDismiss"; @@ -48,15 +49,12 @@ function clampListPaneWidth(width: number) { return Math.max(GITHUB_IMPORT_LIST_PANE_MIN_WIDTH, Math.min(GITHUB_IMPORT_LIST_PANE_MAX_WIDTH, width)); } -function formatPreviewBody(body: string | null | undefined, isMobile: boolean) { - if (!body) { - return null; - } - if (isMobile) { - return body; - } - return body.slice(0, 200) + (body.length > 200 ? "…" : ""); -} +/* +FNXC:GitHubImport 2026-06-22-18:30: +The Import-from-GitHub preview pane must show the FULL selected issue/PR, not a truncated snapshot. +The list endpoint already returns the complete (untruncated) body, so no per-item detail fetch is needed — the prior 200-char desktop slice in formatPreviewBody was the only thing truncating the preview, and it has been removed. +The full body renders as GitHub-flavored markdown via the shared MailboxMessageContent component; the preview pane is already scrollable (prior fix), so the body takes full height with no line clamping. +*/ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId, presentation = "modal" }: GitHubImportModalProps) { const { isEmbedded, scrollLockEnabled, resizePersistEnabled, escapeEnabled } = useEmbeddedPresentation(presentation); @@ -832,13 +830,43 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId,
{/* Issue preview */} + {/* + FNXC:GitHubImport 2026-06-22-18:30: + Full-issue preview: complete title, full body rendered as markdown, and key metadata (number, state, author, labels, URL). No body truncation/clamping. + */} {activeTab === "issues" && selectedIssue ? (
{t("git.previewIssueMeta", "Issue #{{number}}", { number: selectedIssue.number })}
{selectedIssue.title}
-
- {formatPreviewBody(selectedIssue.body, isMobile) || t("git.noDescription", "(no description)")} +
+ {selectedIssue.state && ( + {selectedIssue.state} + )} + {selectedIssue.author && ( + {t("git.previewAuthor", "by {{author}}", { author: selectedIssue.author })} + )} + + {t("git.viewOnGitHub", "View on GitHub")} +
+ {selectedIssue.labels.length > 0 && ( + + {selectedIssue.labels.map((l) => ( + {l.name} + ))} + + )} + {selectedIssue.body ? ( + + ) : ( +
+ {t("git.noDescription", "(no description)")} +
+ )}
) : activeTab === "issues" ? (
@@ -850,16 +878,39 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId, ) : null} {/* Pull request preview */} + {/* + FNXC:GitHubImport 2026-06-22-18:30: + Full-PR preview: complete title, full body as markdown, and key metadata (number, state, author, base/head branches, URL). No body truncation/clamping. + */} {activeTab === "pulls" && selectedPull ? (
{t("git.previewPullMeta", "Pull Request #{{number}}", { number: selectedPull.number })}
{selectedPull.title}
+
+ {selectedPull.state && ( + {selectedPull.state} + )} + {selectedPull.author && ( + {t("git.previewAuthor", "by {{author}}", { author: selectedPull.author })} + )} + + {t("git.viewOnGitHub", "View on GitHub")} + +
{t("git.branchLabel", "Branch:")} {selectedPull.headBranch} → {selectedPull.baseBranch}
-
- {formatPreviewBody(selectedPull.body, isMobile) || t("git.noDescription", "(no description)")} -
+ {selectedPull.body ? ( + + ) : ( +
+ {t("git.noDescription", "(no description)")} +
+ )}
) : activeTab === "pulls" ? (
diff --git a/packages/dashboard/app/components/GitManagerModal.tsx b/packages/dashboard/app/components/GitManagerModal.tsx index 93e927cd03..eb2e62a5ee 100644 --- a/packages/dashboard/app/components/GitManagerModal.tsx +++ b/packages/dashboard/app/components/GitManagerModal.tsx @@ -959,6 +959,21 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj ); })} + {/* + FNXC:GitManager 2026-06-22-19:00: + Refresh relocated from the (now-removed) internal gray .modal-header into the section nav strip so it is reachable on every section ("each page") in BOTH the right-dock embedded view (wrapping tab strip) and the popped-out modal. The dock tab strip and RightDockExpandModal already supply a header, so the internal title+refresh row was a duplicate header and is removed. Same fetchSectionData + loading spinner state as before. + */} + {/* Content Area */} @@ -1121,23 +1136,6 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj return (
-
-

- - {t("git.modalTitle", "Git Manager")} -

-
- -
-
-
{gitBody}
@@ -1155,14 +1153,6 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj {t("git.modalTitle", "Git Manager")}
- diff --git a/packages/dashboard/app/components/ListView.tsx b/packages/dashboard/app/components/ListView.tsx index bd96e1e1a4..f05b9c1388 100644 --- a/packages/dashboard/app/components/ListView.tsx +++ b/packages/dashboard/app/components/ListView.tsx @@ -2032,6 +2032,7 @@ export function ListView({ projectId={projectId} autoExpand={false} defaultExpanded={false} + singleLine /* FNXC:QuickEntry 2026-06-22-19:25: List view uses the compact single-line quick-add so the box stays one line tall. */ favoriteProviders={favoriteProviders} favoriteModels={favoriteModels} onToggleFavorite={onToggleFavorite} diff --git a/packages/dashboard/app/components/QuickEntryBox.css b/packages/dashboard/app/components/QuickEntryBox.css index e7a7f1d22c..e1f6429b57 100644 --- a/packages/dashboard/app/components/QuickEntryBox.css +++ b/packages/dashboard/app/components/QuickEntryBox.css @@ -32,6 +32,26 @@ min-height: 80px; } +/* +FNXC:QuickEntry 2026-06-22-19:25: +List view renders quick-add as a COMPACT single-line input so the box isn't tall. +Clamp the textarea to exactly one line (min-height == max-height == one line), forbid auto-grow/manual resize, and scroll overflow instead of growing. +Tighten container vertical padding so the overall box is just the one-line input height. +Board/columns omit `.quick-entry--single-line`, keeping the tall 80px + auto-grow behavior. +*/ +.quick-entry--single-line { + padding-top: var(--space-xs); + padding-bottom: var(--space-xs); +} + +.quick-entry-box.quick-entry--single-line .quick-entry-input, +.quick-entry-box.quick-entry--single-line .quick-entry-input--expanded { + min-height: 36px; + max-height: 36px; + overflow-y: auto; + resize: none; +} + @media (max-width: 768px) { .quick-entry-input--expanded { min-height: 60px; diff --git a/packages/dashboard/app/components/QuickEntryBox.tsx b/packages/dashboard/app/components/QuickEntryBox.tsx index 536adad6a6..76e84e44ac 100644 --- a/packages/dashboard/app/components/QuickEntryBox.tsx +++ b/packages/dashboard/app/components/QuickEntryBox.tsx @@ -53,6 +53,11 @@ interface QuickEntryBoxProps { Initial disclosure (expanded controls) state. List view passes false so quick-add starts COLLAPSED; Board/columns keep the default true so quick-add stays OPEN. This is independent of autoExpand (which only governs expand-on-focus). */ defaultExpanded?: boolean; + /* + FNXC:QuickEntry 2026-06-22-19:25: + List view renders quick-add as a COMPACT single-line input so the box isn't tall. When true, the textarea stays one line: isExpanded initializes false, focus does NOT auto-expand it, and auto-resize-to-scrollHeight is short-circuited (capped to the one-line min-height). Board/columns omit singleLine, preserving the tall 80px + auto-grow behavior. singleLine governs only textarea height, not the disclosure/controls panel (which List already collapses via defaultExpanded={false}). + */ + singleLine?: boolean; /** * Favorited provider IDs from shared app-level state. * When provided (alongside availableModels), the component uses these @@ -96,7 +101,7 @@ function parseModelSelection(value: string): { provider?: string; modelId?: stri }; } -export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, onPlanningMode, onSubtaskBreakdown, workflowId, projectId, autoExpand = true, defaultExpanded = true, favoriteProviders: parentFavoriteProviders, favoriteModels: parentFavoriteModels, onToggleFavorite: parentToggleFavorite, onToggleModelFavorite: parentToggleModelFavorite, onOpenTask }: QuickEntryBoxProps) { +export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, onPlanningMode, onSubtaskBreakdown, workflowId, projectId, autoExpand = true, defaultExpanded = true, singleLine = false, favoriteProviders: parentFavoriteProviders, favoriteModels: parentFavoriteModels, onToggleFavorite: parentToggleFavorite, onToggleModelFavorite: parentToggleModelFavorite, onOpenTask }: QuickEntryBoxProps) { const { t } = useTranslation("app"); const [description, setDescription] = useState(() => { if (typeof window !== "undefined") { @@ -107,7 +112,8 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, const [isSubmitting, setIsSubmitting] = useState(false); const [postSubmitFocusRequest, setPostSubmitFocusRequest] = useState(0); // isExpanded controls textarea height styling (auto-resize) - const [isExpanded, setIsExpanded] = useState(true); + // FNXC:QuickEntry 2026-06-22-19:25: singleLine (List view) starts collapsed so the textarea is one line, not the tall 80px variant. + const [isExpanded, setIsExpanded] = useState(!singleLine); // isDisclosureExpanded controls visibility of the controls panel (Deps, Models, etc.) // Starts expanded by default — controls visible immediately const [isDisclosureExpanded, setIsDisclosureExpanded] = useState(defaultExpanded); @@ -325,11 +331,12 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, }, []); // Resize when description changes (not in fullscreen mode since CSS handles it) + // FNXC:QuickEntry 2026-06-22-19:25: singleLine (List view) must stay one line — skip auto-resize-to-scrollHeight so the textarea never grows tall with content; CSS clamps it to the one-line height. useEffect(() => { - if (isExpanded) { + if (isExpanded && !singleLine) { autoResize(); } - }, [description, isExpanded, autoResize]); + }, [description, isExpanded, autoResize, singleLine]); const requestFocusAfterSuccessfulSubmit = useCallback(() => { setPostSubmitFocusRequest((request) => request + 1); @@ -686,7 +693,10 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, if (e.shiftKey) { // Allow Shift+Enter to insert a newline in any quick-entry state // Don't prevent default - let the newline be inserted - setIsExpanded(true); + // FNXC:QuickEntry 2026-06-22-19:25: singleLine (List view) stays one line even on Shift+Enter — do not expand the textarea. + if (!singleLine) { + setIsExpanded(true); + } return; } // Enter without Shift submits @@ -763,6 +773,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, projectId, setIsDisclosureExpanded, duplicateMatches, + singleLine, ], ); @@ -776,10 +787,11 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, const handleFocus = useCallback(() => { // Auto-expand on focus when autoExpand prop is true (default) - if (autoExpand) { + // FNXC:QuickEntry 2026-06-22-19:25: never auto-expand the textarea on focus when singleLine (List view) — it must stay one line. + if (autoExpand && !singleLine) { setIsExpanded(true); } - }, [autoExpand]); + }, [autoExpand, singleLine]); const toggleDep = useCallback((id: string) => { setDependencies((prev) => @@ -1478,13 +1490,13 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, return ( <> -
+