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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
||||
<PageErrorBoundary>
|
||||
<div className="task-detail-main-panel">
|
||||
<div className="task-detail-main-panel-back-row">
|
||||
<button
|
||||
type="button"
|
||||
className="task-detail-main-panel-back-btn"
|
||||
onClick={closeTaskDetailMainPanel}
|
||||
>
|
||||
<ArrowLeft size={16} aria-hidden="true" />
|
||||
<span>{t("app.taskDetail.backToBoard", "Back to board")}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="task-detail-main-panel-body">
|
||||
<TaskDetailContent
|
||||
task={liveDetailTask}
|
||||
projectId={currentProject?.id}
|
||||
tasks={tasks}
|
||||
embedded
|
||||
/*
|
||||
FNXC:TaskDetail 2026-06-22-18:40:
|
||||
Board-card detail (full main panel) renders its "Back to board" affordance inside TaskDetailContent's gray header (far right, across from the task id) instead of a separate back-row above the content. The prop only renders the header back button when both embedded and onBackToBoard are present, so ListView split-pane and modal usages stay unaffected.
|
||||
*/
|
||||
onBackToBoard={closeTaskDetailMainPanel}
|
||||
onOpenDetail={(value) => setMainPanelDetailTask(value)}
|
||||
onMoveTask={moveTask}
|
||||
onDeleteTask={deleteTask}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
<div className="github-import-pane-content">
|
||||
{/* 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 ? (
|
||||
<div className="issue-preview" data-testid="github-import-preview-card">
|
||||
<div className="preview-meta">{t("git.previewIssueMeta", "Issue #{{number}}", { number: selectedIssue.number })}</div>
|
||||
<div className="preview-title">{selectedIssue.title}</div>
|
||||
<div className="preview-body">
|
||||
{formatPreviewBody(selectedIssue.body, isMobile) || t("git.noDescription", "(no description)")}
|
||||
<div className="preview-metadata">
|
||||
{selectedIssue.state && (
|
||||
<span className={`preview-state-badge preview-state-badge--${selectedIssue.state}`}>{selectedIssue.state}</span>
|
||||
)}
|
||||
{selectedIssue.author && (
|
||||
<span className="preview-author">{t("git.previewAuthor", "by {{author}}", { author: selectedIssue.author })}</span>
|
||||
)}
|
||||
<a className="preview-url" href={selectedIssue.html_url} target="_blank" rel="noopener noreferrer">
|
||||
{t("git.viewOnGitHub", "View on GitHub")}
|
||||
</a>
|
||||
</div>
|
||||
{selectedIssue.labels.length > 0 && (
|
||||
<span className="preview-labels">
|
||||
{selectedIssue.labels.map((l) => (
|
||||
<span key={l.name} className="label-chip">{l.name}</span>
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
{selectedIssue.body ? (
|
||||
<MailboxMessageContent
|
||||
className="preview-body preview-body--markdown"
|
||||
content={selectedIssue.body}
|
||||
testId="github-import-preview-body"
|
||||
/>
|
||||
) : (
|
||||
<div className="preview-body" data-testid="github-import-preview-body">
|
||||
{t("git.noDescription", "(no description)")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : activeTab === "issues" ? (
|
||||
<div className="github-import-state github-import-state--idle" data-testid="github-import-preview-empty">
|
||||
@@ -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 ? (
|
||||
<div className="issue-preview" data-testid="github-import-preview-card">
|
||||
<div className="preview-meta">{t("git.previewPullMeta", "Pull Request #{{number}}", { number: selectedPull.number })}</div>
|
||||
<div className="preview-title">{selectedPull.title}</div>
|
||||
<div className="preview-metadata">
|
||||
{selectedPull.state && (
|
||||
<span className={`preview-state-badge preview-state-badge--${selectedPull.state}`}>{selectedPull.state}</span>
|
||||
)}
|
||||
{selectedPull.author && (
|
||||
<span className="preview-author">{t("git.previewAuthor", "by {{author}}", { author: selectedPull.author })}</span>
|
||||
)}
|
||||
<a className="preview-url" href={selectedPull.html_url} target="_blank" rel="noopener noreferrer">
|
||||
{t("git.viewOnGitHub", "View on GitHub")}
|
||||
</a>
|
||||
</div>
|
||||
<div className="preview-branch">
|
||||
<strong>{t("git.branchLabel", "Branch:")}</strong> {selectedPull.headBranch} → {selectedPull.baseBranch}
|
||||
</div>
|
||||
<div className="preview-body">
|
||||
{formatPreviewBody(selectedPull.body, isMobile) || t("git.noDescription", "(no description)")}
|
||||
</div>
|
||||
{selectedPull.body ? (
|
||||
<MailboxMessageContent
|
||||
className="preview-body preview-body--markdown"
|
||||
content={selectedPull.body}
|
||||
testId="github-import-preview-body"
|
||||
/>
|
||||
) : (
|
||||
<div className="preview-body" data-testid="github-import-preview-body">
|
||||
{t("git.noDescription", "(no description)")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : activeTab === "pulls" ? (
|
||||
<div className="github-import-state github-import-state--idle" data-testid="github-import-preview-empty">
|
||||
|
||||
@@ -959,6 +959,21 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{/*
|
||||
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.
|
||||
*/}
|
||||
<button
|
||||
type="button"
|
||||
className="gm-nav-refresh"
|
||||
onClick={fetchSectionData}
|
||||
disabled={loading}
|
||||
title={t("git.refresh", "Refresh")}
|
||||
aria-label={t("git.refresh", "Refresh")}
|
||||
>
|
||||
<RefreshCw size={16} className={loading ? "spin" : ""} />
|
||||
<span className="gm-nav-label">{t("git.refresh", "Refresh")}</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{/* Content Area */}
|
||||
@@ -1121,23 +1136,6 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj
|
||||
return (
|
||||
<div className="git-manager-embedded right-dock-embedded-view">
|
||||
<div className="gm-modal gm-modal--embedded" ref={modalRef} style={keyboardStyle}>
|
||||
<div className="modal-header">
|
||||
<h3>
|
||||
<FolderGit2 size={18} style={{ marginRight: 8, verticalAlign: "middle" }} />
|
||||
{t("git.modalTitle", "Git Manager")}
|
||||
</h3>
|
||||
<div className="gm-header-actions">
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={fetchSectionData}
|
||||
disabled={loading}
|
||||
title={t("git.refresh", "Refresh")}
|
||||
>
|
||||
<RefreshCw size={14} className={loading ? "spin" : ""} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="gm-layout">
|
||||
{gitBody}
|
||||
</div>
|
||||
@@ -1155,14 +1153,6 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj
|
||||
{t("git.modalTitle", "Git Manager")}
|
||||
</h3>
|
||||
<div className="gm-header-actions">
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={fetchSectionData}
|
||||
disabled={loading}
|
||||
title={t("git.refresh", "Refresh")}
|
||||
>
|
||||
<RefreshCw size={14} className={loading ? "spin" : ""} />
|
||||
</button>
|
||||
<button className="modal-close" onClick={handleClose} aria-label={t("git.close", "Close")}>
|
||||
<X size={18} />
|
||||
</button>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<div className={`quick-entry-box ${isDisclosureExpanded ? "quick-entry-box--expanded" : "quick-entry-box--collapsed"}`} data-testid="quick-entry-box">
|
||||
<div className={`quick-entry-box ${isDisclosureExpanded ? "quick-entry-box--expanded" : "quick-entry-box--collapsed"}${singleLine ? " quick-entry--single-line" : ""}`} data-testid="quick-entry-box">
|
||||
<div className="description-with-refine">
|
||||
<div className="quick-entry-main-row">
|
||||
<div className="quick-entry-textarea-wrap">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className={`quick-entry-input ${isExpanded ? "quick-entry-input--expanded" : ""}`}
|
||||
className={`quick-entry-input ${isExpanded && !singleLine ? "quick-entry-input--expanded" : ""}`}
|
||||
placeholder={isSubmitting ? t("tasks.creating", "Creating...") : t("tasks.addTaskPlaceholder", "Add a task...")}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
@@ -1494,7 +1506,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
onBlur={handleBlur}
|
||||
disabled={isSubmitting || isDisabled}
|
||||
data-testid="quick-entry-input"
|
||||
rows={2}
|
||||
rows={singleLine ? 1 : 2}
|
||||
aria-controls="quick-entry-controls"
|
||||
aria-expanded={isDisclosureExpanded}
|
||||
/>
|
||||
|
||||
@@ -196,10 +196,15 @@ Floating panel positioned by state-driven inline `left/top/width/height`. min/ma
|
||||
/*
|
||||
FNXC:RightDock 2026-06-22-17:40:
|
||||
Header is the drag handle; grab/grabbing cursor and non-selectable text signal and protect the drag.
|
||||
|
||||
FNXC:RightDock 2026-06-22-18:50:
|
||||
Touch dragging was janky because the browser claimed the header's touch stream for scroll/pan gestures. `touch-action: none` on the drag handle (matching the resize handles) hands the whole gesture to our pointer handlers so a finger drag stays smooth and never scrolls the page behind it. `cursor: grab/grabbing` is desktop-only signal; `touch-action` is what makes touch work. A comfortable `min-height` makes the header a forgiving touch target.
|
||||
*/
|
||||
.right-dock-expand-modal__header--draggable {
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.right-dock-expand-modal__header--draggable:active {
|
||||
|
||||
@@ -147,11 +147,16 @@ export function RightDockExpandModal({
|
||||
/*
|
||||
FNXC:RightDock 2026-06-22-17:40:
|
||||
Header drag: pointerdown on the title bar moves the panel via state-driven `position: fixed; left/top`. Pointer capture keeps the drag alive past the header bounds, updates are rAF-batched so the move stays smooth, and the panel is clamped on-screen. Clicks on the close button are excluded so dragging never swallows the close.
|
||||
|
||||
FNXC:RightDock 2026-06-22-18:50:
|
||||
Touch smoothness fix: listen for pointermove/up on the CAPTURED element (`captureTarget` = event.currentTarget) rather than `document`. `setPointerCapture` redirects every move for this pointerId to that element, so element-scoped listeners receive the full stream even when the finger drifts off the header — and they pair cleanly with `touch-action: none` (CSS) without a separate non-passive document listener. clientX/clientY are read from the captured pointer's move events. Raw moves are coalesced into a single rAF (`frame`) so we set left/top at most once per frame and never thrash layout on a flood of touch-move events.
|
||||
*/
|
||||
const handleFloatingDragPointerDown = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if ((event.target as HTMLElement).closest("button")) return;
|
||||
event.preventDefault();
|
||||
event.currentTarget.setPointerCapture?.(event.pointerId);
|
||||
const captureTarget = event.currentTarget;
|
||||
const pointerId = event.pointerId;
|
||||
captureTarget.setPointerCapture?.(pointerId);
|
||||
const startX = event.clientX;
|
||||
const startY = event.clientY;
|
||||
const startPosition = position;
|
||||
@@ -163,6 +168,7 @@ export function RightDockExpandModal({
|
||||
let frame = 0;
|
||||
|
||||
const handlePointerMove = (moveEvent: PointerEvent) => {
|
||||
if (moveEvent.pointerId !== pointerId) return;
|
||||
latest = { x: startPosition.x + moveEvent.clientX - startX, y: startPosition.y + moveEvent.clientY - startY };
|
||||
if (frame) return;
|
||||
frame = requestAnimationFrame(() => {
|
||||
@@ -170,29 +176,31 @@ export function RightDockExpandModal({
|
||||
setPositionState(clampExpandPosition(latest, currentSize));
|
||||
});
|
||||
};
|
||||
const handlePointerUp = () => {
|
||||
const detachListeners = () => {
|
||||
captureTarget.releasePointerCapture?.(pointerId);
|
||||
captureTarget.removeEventListener("pointermove", handlePointerMove);
|
||||
captureTarget.removeEventListener("pointerup", handlePointerUp);
|
||||
captureTarget.removeEventListener("pointercancel", handlePointerUp);
|
||||
};
|
||||
function handlePointerUp() {
|
||||
if (frame) cancelAnimationFrame(frame);
|
||||
persistPosition(latest, currentSize);
|
||||
document.body.style.userSelect = previousUserSelect;
|
||||
document.removeEventListener("pointermove", handlePointerMove);
|
||||
document.removeEventListener("pointerup", handlePointerUp);
|
||||
document.removeEventListener("pointercancel", handlePointerUp);
|
||||
detachListeners();
|
||||
dragTeardownRef.current = null;
|
||||
};
|
||||
}
|
||||
|
||||
// FNXC:RightDock 2026-06-22-17:40: Close/unmount-mid-drag teardown cancels the rAF and drops the listeners without persisting a partial move.
|
||||
dragTeardownRef.current = () => {
|
||||
if (frame) cancelAnimationFrame(frame);
|
||||
document.body.style.userSelect = previousUserSelect;
|
||||
document.removeEventListener("pointermove", handlePointerMove);
|
||||
document.removeEventListener("pointerup", handlePointerUp);
|
||||
document.removeEventListener("pointercancel", handlePointerUp);
|
||||
detachListeners();
|
||||
dragTeardownRef.current = null;
|
||||
};
|
||||
|
||||
document.addEventListener("pointermove", handlePointerMove);
|
||||
document.addEventListener("pointerup", handlePointerUp);
|
||||
document.addEventListener("pointercancel", handlePointerUp);
|
||||
captureTarget.addEventListener("pointermove", handlePointerMove);
|
||||
captureTarget.addEventListener("pointerup", handlePointerUp);
|
||||
captureTarget.addEventListener("pointercancel", handlePointerUp);
|
||||
}, [persistPosition, position, size]);
|
||||
|
||||
/*
|
||||
@@ -202,7 +210,9 @@ export function RightDockExpandModal({
|
||||
const handleFloatingResizePointerDown = useCallback((event: ReactPointerEvent<HTMLDivElement>, direction: ExpandResizeDirection) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.currentTarget.setPointerCapture?.(event.pointerId);
|
||||
const captureTarget = event.currentTarget;
|
||||
const pointerId = event.pointerId;
|
||||
captureTarget.setPointerCapture?.(pointerId);
|
||||
const startX = event.clientX;
|
||||
const startY = event.clientY;
|
||||
const startSize = size;
|
||||
@@ -215,6 +225,7 @@ export function RightDockExpandModal({
|
||||
let frame = 0;
|
||||
|
||||
const handlePointerMove = (moveEvent: PointerEvent) => {
|
||||
if (moveEvent.pointerId !== pointerId) return;
|
||||
const dx = moveEvent.clientX - startX;
|
||||
const dy = moveEvent.clientY - startY;
|
||||
const nextSize = clampExpandSize({
|
||||
@@ -234,30 +245,32 @@ export function RightDockExpandModal({
|
||||
setPositionState(clampExpandPosition(latestPosition, latestSize));
|
||||
});
|
||||
};
|
||||
const handlePointerUp = () => {
|
||||
const detachListeners = () => {
|
||||
captureTarget.releasePointerCapture?.(pointerId);
|
||||
captureTarget.removeEventListener("pointermove", handlePointerMove);
|
||||
captureTarget.removeEventListener("pointerup", handlePointerUp);
|
||||
captureTarget.removeEventListener("pointercancel", handlePointerUp);
|
||||
};
|
||||
function handlePointerUp() {
|
||||
if (frame) cancelAnimationFrame(frame);
|
||||
persistSize(latestSize);
|
||||
persistPosition(latestPosition, latestSize);
|
||||
document.body.style.userSelect = previousUserSelect;
|
||||
document.removeEventListener("pointermove", handlePointerMove);
|
||||
document.removeEventListener("pointerup", handlePointerUp);
|
||||
document.removeEventListener("pointercancel", handlePointerUp);
|
||||
detachListeners();
|
||||
dragTeardownRef.current = null;
|
||||
};
|
||||
}
|
||||
|
||||
// FNXC:RightDock 2026-06-22-17:40: Close/unmount-mid-resize teardown.
|
||||
dragTeardownRef.current = () => {
|
||||
if (frame) cancelAnimationFrame(frame);
|
||||
document.body.style.userSelect = previousUserSelect;
|
||||
document.removeEventListener("pointermove", handlePointerMove);
|
||||
document.removeEventListener("pointerup", handlePointerUp);
|
||||
document.removeEventListener("pointercancel", handlePointerUp);
|
||||
detachListeners();
|
||||
dragTeardownRef.current = null;
|
||||
};
|
||||
|
||||
document.addEventListener("pointermove", handlePointerMove);
|
||||
document.addEventListener("pointerup", handlePointerUp);
|
||||
document.addEventListener("pointercancel", handlePointerUp);
|
||||
captureTarget.addEventListener("pointermove", handlePointerMove);
|
||||
captureTarget.addEventListener("pointerup", handlePointerUp);
|
||||
captureTarget.addEventListener("pointercancel", handlePointerUp);
|
||||
}, [persistPosition, persistSize, position, size]);
|
||||
|
||||
// FNXC:RightDock 2026-06-22-17:40: Run any active drag/resize teardown on unmount so document pointer listeners + a pending rAF never outlive the modal.
|
||||
|
||||
@@ -2026,18 +2026,23 @@ The previous bespoke rules here hid the tab labels (icon-only) and used a crampe
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:GitManager 2026-06-22-17:30:
|
||||
The dock tab strip WRAPS so every section is visible at once (no single-tab horizontal swipe). Tabs take intrinsic width — width:auto overrides the base .gm-nav-item width:100% that otherwise made each tab fill the row (one per swipe) — and are compact icon+label so all ~7 sections fit across 2-3 wrapped rows.
|
||||
FNXC:GitManager 2026-06-22-19:20:
|
||||
The dock tab strip is ONE ROW that scrolls left-right when needed, showing as many section icons as fit. Tabs are compact ICON-ONLY (labels are sr-only; the button title gives a tooltip) so the maximum number of sections is visible before horizontal scroll kicks in. width:auto overrides the base .gm-nav-item width:100% that otherwise made each tab fill the row (one per swipe).
|
||||
*/
|
||||
.gm-modal--embedded .gm-sidebar {
|
||||
flex: 0 0 auto;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
overflow: visible;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior-x: contain;
|
||||
scrollbar-width: thin;
|
||||
touch-action: pan-x;
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
@@ -2045,15 +2050,26 @@ The previous bespoke rules here hid the tab labels (icon-only) and used a crampe
|
||||
.gm-modal--embedded .gm-nav-item {
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--space-xs) / 2);
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
padding: var(--space-xs);
|
||||
border-left: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
font-size: var(--font-size-xs);
|
||||
min-width: calc(var(--space-2xl) + var(--space-sm));
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
min-width: calc(var(--space-xl) + var(--space-xs));
|
||||
}
|
||||
|
||||
/* Icon-only: hide the section label (kept for screen readers); the button title is the tooltip. */
|
||||
.gm-modal--embedded .gm-nav-label {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.gm-modal--embedded .gm-nav-item.active {
|
||||
@@ -2061,6 +2077,21 @@ The previous bespoke rules here hid the tab labels (icon-only) and used a crampe
|
||||
border-bottom-color: var(--todo);
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:GitManager 2026-06-22-19:20:
|
||||
In the one-row scrolling strip the refresh is a compact icon button matching .gm-nav-item, pinned as the last item. Drop the desktop margin-top:auto so it stays inline.
|
||||
*/
|
||||
.gm-modal--embedded .gm-nav-refresh {
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
margin-top: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-xs);
|
||||
border-left: none;
|
||||
min-width: calc(var(--space-xl) + var(--space-xs));
|
||||
}
|
||||
|
||||
.gm-modal--embedded .gm-content {
|
||||
min-height: 200px;
|
||||
padding: var(--space-md);
|
||||
@@ -2231,6 +2262,37 @@ The previous bespoke rules here hid the tab labels (icon-only) and used a crampe
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:GitManager 2026-06-22-19:00:
|
||||
Refresh button pinned at the end of the section nav strip (replaces the removed duplicate internal gray .modal-header refresh). In the desktop vertical sidebar margin-top:auto pins it to the bottom; in the dock wrapping/mobile horizontal strip it sits as the last tab. Shares the .gm-nav-item visual language; theme tokens only.
|
||||
*/
|
||||
.gm-nav-refresh {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-sm) var(--space-lg);
|
||||
margin-top: auto;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.gm-nav-refresh:hover:not(:disabled) {
|
||||
color: var(--text);
|
||||
background: color-mix(in srgb, var(--text) 5%, transparent);
|
||||
}
|
||||
|
||||
.gm-nav-refresh:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* ── Header Actions ── */
|
||||
|
||||
.gm-header-actions {
|
||||
@@ -4200,6 +4262,25 @@ The previous bespoke rules here hid the tab labels (icon-only) and used a crampe
|
||||
border-bottom-color: var(--todo);
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:GitManager 2026-06-22-19:00:
|
||||
Mobile standalone modal horizontal nav strip: refresh mirrors the mobile .gm-nav-item (column, intrinsic width) as the last tab; drop desktop margin-top:auto.
|
||||
*/
|
||||
.gm-nav-refresh {
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
margin-top: 0;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--space-xs) / 2);
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
border-left: none;
|
||||
font-size: var(--font-size-xs);
|
||||
min-width: calc(var(--space-2xl) + var(--space-xl));
|
||||
min-height: calc(var(--space-xl) + var(--space-sm));
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gm-content {
|
||||
min-height: 200px;
|
||||
padding: var(--space-md);
|
||||
@@ -4320,6 +4401,11 @@ The previous bespoke rules here hid the tab labels (icon-only) and used a crampe
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
/* FNXC:GitManager 2026-06-22-19:00: relocated refresh hover matches nav-item in light theme. */
|
||||
[data-theme="light"] .gm-nav-refresh:hover:not(:disabled) {
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
[data-theme="light"] .gm-nav-item.active {
|
||||
background: color-mix(in srgb, var(--todo) 6%, transparent);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,34 @@ The standalone Secrets page is mounted as a flex item inside the .project-conten
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:RightDockEmbedded 2026-06-22-19:05:
|
||||
SecretsView 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
|
||||
.secrets-header title row (the "Secrets" heading plus Refresh/Add actions) is redundant title chrome there. Hide it in
|
||||
those two host contexts only; the header stays in the DOM (just display:none). The standalone full-page render and the
|
||||
Settings-modal SecretsSection render are NOT inside these ancestors, so their header stays visible.
|
||||
*/
|
||||
.right-dock__body .secrets-view > .secrets-header,
|
||||
.right-dock-expand-modal__body .secrets-view > .secrets-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 .secrets-view > .secrets-header,
|
||||
.right-dock-expand-modal__body .secrets-view > .secrets-header {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.secrets-header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -1057,8 +1057,62 @@ FN-6500 fixes a tablet regression from FN-5599: the task-detail overlay offset a
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:TaskDetail 2026-06-22-18:40:
|
||||
Embedded in the full-width board-card panel the detail content must be width-bounded so it cannot overflow and get clipped on the right. Constrain the embedded root and its header/body to the host width (width:100%; min-width:0; max-width:100%); min-width:0 lets inner flex rows (header title row, tabs, action bars) shrink and wrap instead of forcing horizontal overflow. Vertical scroll stays on .detail-body.
|
||||
*/
|
||||
.task-detail-content--embedded {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.task-detail-content--embedded .modal-header,
|
||||
.task-detail-content--embedded .detail-body,
|
||||
.task-detail-content--embedded .detail-tabs,
|
||||
.task-detail-content--embedded .modal-actions {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* The gray header row must wrap (task id left, Back-to-board right) instead of overflowing on narrow panels. */
|
||||
.task-detail-content--embedded .modal-header {
|
||||
flex-wrap: wrap;
|
||||
row-gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.task-detail-content--embedded .detail-title-row {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-detail-content--embedded .modal-header-actions {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:TaskDetail 2026-06-22-18:40:
|
||||
"Back to board" affordance inside the gray header. margin-left:auto pushes it to the far right (across from the task id on the left); it shares the header-actions row but stays pinned right and wraps when space is tight. Theme tokens only.
|
||||
*/
|
||||
.task-detail-header-back-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
margin-left: auto;
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-detail-header-back-btn:hover {
|
||||
color: var(--text);
|
||||
background: var(--card-hover);
|
||||
}
|
||||
|
||||
.task-detail-content--embedded .detail-actions-menu,
|
||||
|
||||
@@ -409,6 +409,11 @@ export interface TaskDetailModalProps {
|
||||
export type TaskDetailContentProps = Omit<TaskDetailModalProps, "onClose"> & {
|
||||
embedded?: boolean;
|
||||
onRequestClose?: () => void;
|
||||
/*
|
||||
FNXC:TaskDetail 2026-06-22-18:40:
|
||||
onBackToBoard powers the board-card full-panel "Back to board" affordance rendered in the gray header (far right). It is only honored when embedded is also true, so ListView split-pane and modal usages never show it.
|
||||
*/
|
||||
onBackToBoard?: () => void;
|
||||
};
|
||||
|
||||
function truncate(s: string, max: number): string {
|
||||
@@ -583,6 +588,7 @@ export function TaskDetailContent({
|
||||
mobileHeaderMode = "close",
|
||||
embedded = false,
|
||||
onRequestClose,
|
||||
onBackToBoard,
|
||||
workflowFieldDefs: workflowFieldDefsProp,
|
||||
}: TaskDetailContentProps) {
|
||||
const { t } = useTranslation("app");
|
||||
@@ -2734,6 +2740,20 @@ export function TaskDetailContent({
|
||||
</span>
|
||||
</div>
|
||||
<div className="modal-header-actions">
|
||||
{/*
|
||||
FNXC:TaskDetail 2026-06-22-18:40:
|
||||
Board-card full-panel "Back to board" affordance lives here on the far right of the gray header (across from the task id on the left), pushed by margin-left:auto so it never overlaps the id and wraps on narrow widths. Only rendered when embedded AND onBackToBoard are supplied (board-card detail), never in ListView split-pane or modal usages.
|
||||
*/}
|
||||
{embedded && onBackToBoard && (
|
||||
<button
|
||||
type="button"
|
||||
className="task-detail-header-back-btn"
|
||||
onClick={onBackToBoard}
|
||||
>
|
||||
<ArrowLeft size={14} aria-hidden="true" />
|
||||
<span>{t("app.taskDetail.backToBoard", "Back to board")}</span>
|
||||
</button>
|
||||
)}
|
||||
{!isEditing && canEdit && (
|
||||
<button
|
||||
className="modal-edit-btn"
|
||||
|
||||
@@ -810,7 +810,8 @@ describe("GitHubImportModal", () => {
|
||||
expect(previewCard.textContent).not.toContain(`${"P".repeat(200)}…`);
|
||||
});
|
||||
|
||||
it("truncates long selected issue body on desktop", async () => {
|
||||
// FNXC:GitHubImport 2026-06-22-18:30: Desktop preview must show the FULL issue/PR body (no 200-char clamp). The list response already carries the complete body, so no detail fetch is needed.
|
||||
it("renders long selected issue body in full on desktop without a truncation ellipsis", async () => {
|
||||
Object.defineProperty(window, "innerWidth", {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
@@ -834,12 +835,14 @@ describe("GitHubImportModal", () => {
|
||||
fireEvent.click(screen.getByRole("radio", { name: /Select issue #1/i }));
|
||||
|
||||
const previewCard = await screen.findByTestId("github-import-preview-card");
|
||||
expect(previewCard.textContent).toContain(`${"I".repeat(200)}…`);
|
||||
expect(previewCard.textContent).not.toContain(beyondDesktopCutoff);
|
||||
expect(previewCard.textContent).not.toContain(longBody);
|
||||
expect(previewCard.textContent).toContain(longBody);
|
||||
expect(previewCard.textContent).toContain(beyondDesktopCutoff);
|
||||
expect(previewCard.textContent).not.toContain(`${"I".repeat(200)}…`);
|
||||
// Body renders as markdown via the shared MailboxMessageContent surface.
|
||||
expect(screen.getByTestId("github-import-preview-body")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("truncates long selected pull request body on desktop", async () => {
|
||||
it("renders long selected pull request body in full on desktop without a truncation ellipsis", async () => {
|
||||
Object.defineProperty(window, "innerWidth", {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
@@ -865,9 +868,52 @@ describe("GitHubImportModal", () => {
|
||||
fireEvent.click(screen.getByRole("radio", { name: /Select pull request #1/i }));
|
||||
|
||||
const previewCard = await screen.findByTestId("github-import-preview-card");
|
||||
expect(previewCard.textContent).toContain(`${"R".repeat(200)}…`);
|
||||
expect(previewCard.textContent).not.toContain(beyondDesktopCutoff);
|
||||
expect(previewCard.textContent).not.toContain(longBody);
|
||||
expect(previewCard.textContent).toContain(longBody);
|
||||
expect(previewCard.textContent).toContain(beyondDesktopCutoff);
|
||||
expect(previewCard.textContent).not.toContain(`${"R".repeat(200)}…`);
|
||||
expect(screen.getByTestId("github-import-preview-body")).toBeTruthy();
|
||||
});
|
||||
|
||||
// FNXC:GitHubImport 2026-06-22-18:30: Full-issue preview must surface key metadata (state, author, GitHub URL) alongside the full markdown body.
|
||||
it("renders full issue metadata (state, author, GitHub link) in the desktop preview", async () => {
|
||||
Object.defineProperty(window, "innerWidth", {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
value: 1200,
|
||||
});
|
||||
|
||||
const issues = [
|
||||
{
|
||||
number: 7,
|
||||
title: "Metadata Issue",
|
||||
body: "**bold** issue body with `code`",
|
||||
html_url: "https://github.com/owner/repo/issues/7",
|
||||
labels: [{ name: "bug" }],
|
||||
state: "open" as const,
|
||||
author: "octocat",
|
||||
},
|
||||
];
|
||||
vi.mocked(fetchGitRemotes).mockResolvedValueOnce(singleRemote);
|
||||
vi.mocked(apiFetchGitHubIssues).mockResolvedValueOnce(issues);
|
||||
|
||||
render(<GitHubImportModal isOpen={true} onClose={onClose} onImport={onImport} tasks={[]} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Metadata Issue")).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("radio", { name: /Select issue #7/i }));
|
||||
|
||||
const previewCard = await screen.findByTestId("github-import-preview-card");
|
||||
expect(within(previewCard).getByText("open")).toBeTruthy();
|
||||
expect(within(previewCard).getByText(/octocat/)).toBeTruthy();
|
||||
expect(within(previewCard).getByText("bug")).toBeTruthy();
|
||||
const link = within(previewCard).getByRole("link", { name: /View on GitHub/i }) as HTMLAnchorElement;
|
||||
expect(link.getAttribute("href")).toBe("https://github.com/owner/repo/issues/7");
|
||||
// Markdown is rendered (bold/code become elements, not literal asterisks/backticks).
|
||||
const body = screen.getByTestId("github-import-preview-body");
|
||||
expect(body.querySelector("strong")).toBeTruthy();
|
||||
expect(body.querySelector("code")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("returns to list view on mobile after successful import", async () => {
|
||||
|
||||
@@ -408,6 +408,32 @@ describe("QuickEntryBox", () => {
|
||||
expect((textarea as HTMLTextAreaElement).rows).toBe(2);
|
||||
});
|
||||
|
||||
// FNXC:QuickEntry 2026-06-22-19:25: List view passes singleLine so quick-add is a compact one-line input (not the tall 80px auto-grow variant).
|
||||
describe("singleLine (List view compact mode)", () => {
|
||||
it("renders a one-line textarea that is not expanded and does not grow on focus/typing", () => {
|
||||
renderQuickEntryBox({ singleLine: true });
|
||||
const box = screen.getByTestId("quick-entry-box");
|
||||
const textarea = screen.getByTestId("quick-entry-input") as HTMLTextAreaElement;
|
||||
|
||||
expect(box.className).toContain("quick-entry--single-line");
|
||||
expect(textarea.rows).toBe(1);
|
||||
// Never the tall expanded variant — even after focus (which auto-expands when not singleLine).
|
||||
expect(textarea.className).not.toContain("quick-entry-input--expanded");
|
||||
fireEvent.focus(textarea);
|
||||
expect(textarea.className).not.toContain("quick-entry-input--expanded");
|
||||
fireEvent.change(textarea, { target: { value: "line one\nline two\nline three" } });
|
||||
expect(textarea.className).not.toContain("quick-entry-input--expanded");
|
||||
});
|
||||
|
||||
it("keeps the default tall/expandable behavior when singleLine is not passed (Board/columns)", () => {
|
||||
renderQuickEntryBox({ singleLine: false });
|
||||
const box = screen.getByTestId("quick-entry-box");
|
||||
const textarea = screen.getByTestId("quick-entry-input") as HTMLTextAreaElement;
|
||||
expect(box.className).not.toContain("quick-entry--single-line");
|
||||
expect(textarea.rows).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("post-submission focus restoration (FN-6217)", () => {
|
||||
it("does not auto-focus the quick-entry textarea on empty desktop mount", async () => {
|
||||
mockDesktopViewport();
|
||||
|
||||
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { RightDock, RIGHT_DOCK_VIEW_STORAGE_KEY, RIGHT_DOCK_WIDTH_STORAGE_KEY } from "../RightDock";
|
||||
import { RightDockExpandModal } from "../RightDockExpandModal";
|
||||
import { useRightDockController, type RightDockControllerInput } from "../useRightDockController";
|
||||
|
||||
vi.mock("../../api", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../api")>();
|
||||
@@ -266,7 +267,10 @@ describe("RightDock", () => {
|
||||
it("drags the floating pop-out by its header and clamps + persists the new position", () => {
|
||||
/*
|
||||
FNXC:RightDock 2026-06-22-17:40:
|
||||
Pointerdown on the header drag handle then pointermove on the document moves the panel via state-driven fixed left/top, and pointerup persists the clamped position. Assert the panel moved and that a position was persisted (clamped on-screen).
|
||||
Pointerdown on the header drag handle then pointermove moves the panel via state-driven fixed left/top, and pointerup persists the clamped position. Assert the panel moved and that a position was persisted (clamped on-screen).
|
||||
|
||||
FNXC:RightDock 2026-06-22-18:50:
|
||||
Move/up are now dispatched on the captured handle element (not document) because the handler attaches its pointermove/up/cancel listeners to the captured target — setPointerCapture redirects the touch stream there, which is what makes touch dragging smooth.
|
||||
*/
|
||||
render(
|
||||
<RightDockExpandModal
|
||||
@@ -278,8 +282,8 @@ describe("RightDock", () => {
|
||||
|
||||
const handle = screen.getByTestId("right-dock-expand-drag-handle");
|
||||
fireEvent.pointerDown(handle, { pointerId: 1, clientX: 100, clientY: 100 });
|
||||
fireEvent.pointerMove(document, { pointerId: 1, clientX: 60, clientY: 140 });
|
||||
fireEvent.pointerUp(document, { pointerId: 1, clientX: 60, clientY: 140 });
|
||||
fireEvent.pointerMove(handle, { pointerId: 1, clientX: 60, clientY: 140 });
|
||||
fireEvent.pointerUp(handle, { pointerId: 1, clientX: 60, clientY: 140 });
|
||||
|
||||
const persisted = window.localStorage.getItem("fusion:right-dock-expand-modal-position");
|
||||
expect(persisted).not.toBeNull();
|
||||
@@ -299,4 +303,62 @@ describe("RightDock", () => {
|
||||
fireEvent.click(screen.getByTestId("right-dock-expand"));
|
||||
expect(onExpand).toHaveBeenCalledWith("git-manager");
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:RightDock 2026-06-22-18:50:
|
||||
The popped-out expand modal is independent of the dock's open state. This drives the real controller, pops out a view, then toggles the dock closed and asserts the floating modal is STILL mounted and interactive — only its own close button dismisses it. Guards against the regression where toggling the dock cleared expandedView (and where the modal was a child of the dock that early-returns null when closed).
|
||||
*/
|
||||
it("keeps the popped-out expand modal mounted when the dock is toggled closed", () => {
|
||||
const controllerInput = {
|
||||
active: true,
|
||||
projectId: "project-1",
|
||||
addToast: vi.fn(),
|
||||
settingsLoaded: true,
|
||||
researchReadinessVersion: 0,
|
||||
tasks: [],
|
||||
workflowSteps: [],
|
||||
subscribePluginEvents: () => () => {},
|
||||
openDetailTask: vi.fn(),
|
||||
openFileInBrowser: vi.fn(),
|
||||
openSettings: vi.fn(),
|
||||
onSendSelectionToTask: vi.fn(),
|
||||
onCreateTaskFromInsight: vi.fn(),
|
||||
onNavigateToMission: vi.fn(),
|
||||
onTaskCreated: vi.fn(),
|
||||
workflowStepNameLookup: new Map<string, string>(),
|
||||
prAuthAvailable: false,
|
||||
autoMerge: false,
|
||||
visibilityOptions: {},
|
||||
footerVisible: false,
|
||||
} as unknown as RightDockControllerInput;
|
||||
|
||||
function Harness() {
|
||||
const controller = useRightDockController(controllerInput);
|
||||
return (
|
||||
<>
|
||||
<button type="button" data-testid="harness-toggle-dock" onClick={controller.toggle}>
|
||||
toggle dock
|
||||
</button>
|
||||
{controller.dock}
|
||||
{controller.modal}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render(<Harness />);
|
||||
|
||||
// Pop out the currently selected (Files) view from the open dock.
|
||||
fireEvent.click(screen.getByTestId("right-dock-expand"));
|
||||
expect(screen.getByTestId("right-dock-expand-modal")).toBeInTheDocument();
|
||||
|
||||
// Toggle the dock closed: the dock itself unmounts, the floating modal MUST survive.
|
||||
fireEvent.click(screen.getByTestId("harness-toggle-dock"));
|
||||
expect(screen.queryByTestId("right-dock")).toBeNull();
|
||||
expect(screen.getByTestId("right-dock-expand-modal")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("right-dock-expand-body")).toBeInTheDocument();
|
||||
|
||||
// Its own close button still dismisses it.
|
||||
fireEvent.click(screen.getByTestId("right-dock-expand-close"));
|
||||
expect(screen.queryByTestId("right-dock-expand-modal")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -167,8 +167,20 @@ vi.mock("../../components/TaskDetailModal", () => ({
|
||||
</div>
|
||||
),
|
||||
// FNXC:Navigation 2026-06-22-00:00: Board card clicks now open task detail in the full main panel via TaskDetailContent (not the modal). The mock exposes a stable testid so the embedded-panel popstate tests can assert on the new surface.
|
||||
TaskDetailContent: ({ task }: { task: { id: string; title?: string } }) => (
|
||||
// FNXC:TaskDetail 2026-06-22-18:40: "Back to board" moved into TaskDetailContent's gray header (rendered when embedded && onBackToBoard). The mock surfaces that button via onBackToBoard so the panel-dismiss popstate tests still drive the same affordance.
|
||||
TaskDetailContent: ({
|
||||
task,
|
||||
onBackToBoard,
|
||||
}: {
|
||||
task: { id: string; title?: string };
|
||||
onBackToBoard?: () => void;
|
||||
}) => (
|
||||
<div data-testid="task-detail-main-panel-content">
|
||||
{onBackToBoard && (
|
||||
<button type="button" onClick={onBackToBoard}>
|
||||
Back to board
|
||||
</button>
|
||||
)}
|
||||
<h2>{task.title ?? task.id}</h2>
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -48,6 +48,9 @@ export interface RightDockController {
|
||||
/*
|
||||
FNXC:Navigation 2026-06-21-23:40:
|
||||
The right dock is visible by default and collapses from inside the dock. Keep the persisted open/collapsed state in this controller so App and Header do not need duplicate right-dock toggle wiring.
|
||||
|
||||
FNXC:RightDock 2026-06-22-18:50:
|
||||
The popped-out expand modal is INDEPENDENT of the dock's open state. `expandedView` and the modal it drives live at the controller level (a sibling of `dock`, NOT a child of RightDock — which early-returns null when closed). Toggling the dock closed must therefore NOT clear `expandedView`: once a view is popped out it stays open and interactive even with the dock hidden, and only its own close button (`onClose -> setExpandedView(null)`) dismisses it. We still clear `expandedView` when the surface becomes inactive (project change/teardown) because that unmounts the whole controller surface, not a user dock-hide.
|
||||
*/
|
||||
export function useRightDockController(input: RightDockControllerInput): RightDockController {
|
||||
const [open, setOpen] = useState(readStoredRightDockOpen);
|
||||
@@ -57,11 +60,23 @@ export function useRightDockController(input: RightDockControllerInput): RightDo
|
||||
setOpen((current) => {
|
||||
const next = !current;
|
||||
persistRightDockOpen(next);
|
||||
if (!next) setExpandedView(null);
|
||||
// FNXC:RightDock 2026-06-22-18:50: Do NOT clear expandedView on dock-hide; the floating pop-out is independent and survives the dock closing.
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
/*
|
||||
FNXC:RightDock 2026-06-22-19:25:
|
||||
Popping a view out CLOSES the right dock but KEEPS the floating modal open. The modal is independent of dock open state (see expandedView note above), so collapsing the dock on pop-out gives the user the full-width app behind the movable, non-blocking modal. Clearing the pop-out (viewKey null) leaves the dock as-is.
|
||||
*/
|
||||
const handleExpand = useCallback((viewKey: OverflowViewKey | null) => {
|
||||
setExpandedView(viewKey);
|
||||
if (viewKey) {
|
||||
setOpen(false);
|
||||
persistRightDockOpen(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!input.active) setExpandedView(null);
|
||||
}, [input.active]);
|
||||
@@ -125,7 +140,7 @@ export function useRightDockController(input: RightDockControllerInput): RightDo
|
||||
return {
|
||||
open,
|
||||
toggle,
|
||||
dock: input.active ? <RightDock open={open} renderProps={renderProps} visibilityOptions={input.visibilityOptions} footerVisible={input.footerVisible} onExpand={setExpandedView} /> : null,
|
||||
dock: input.active ? <RightDock open={open} renderProps={renderProps} visibilityOptions={input.visibilityOptions} footerVisible={input.footerVisible} onExpand={handleExpand} /> : null,
|
||||
modal: input.active ? <RightDockExpandModal viewKey={expandedView} renderProps={renderProps} visibilityOptions={input.visibilityOptions} onClose={() => setExpandedView(null)} /> : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3844,42 +3844,27 @@ Toast text must contrast its status background across every dashboard theme and
|
||||
|
||||
/*
|
||||
FNXC:Navigation 2026-06-22-00:00:
|
||||
Board card clicks open task detail as a full main-content view that replaces the board ("Full main panel" design). This layout fills the main content area: a fixed Back-to-board row over a scrollable embedded TaskDetailContent body. Theme tokens only; mobile shell renders it unchanged because the panel just fills its host.
|
||||
Board card clicks open task detail as a full main-content view that replaces the board ("Full main panel" design). This layout fills the main content area with a scrollable embedded TaskDetailContent body. Theme tokens only; mobile shell renders it unchanged because the panel just fills its host.
|
||||
|
||||
FNXC:TaskDetail 2026-06-22-18:40:
|
||||
The panel and its body must be width-bounded (width:100%; min-width:0; max-width:100%) so the embedded detail content cannot exceed the full-width host and get clipped on the right; the body scrolls vertically only (overflow-x:hidden). The separate back-row was removed; "Back to board" now lives inside TaskDetailContent's gray header (see .task-detail-header-back-btn).
|
||||
*/
|
||||
.task-detail-main-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.task-detail-main-panel-back-row {
|
||||
flex: 0 0 auto;
|
||||
padding: var(--space-lg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.task-detail-main-panel-back-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-detail-main-panel-back-btn:hover {
|
||||
color: var(--text);
|
||||
background: var(--card-hover);
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.task-detail-main-panel-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -3055,6 +3055,7 @@ export class GitHubClient {
|
||||
labels: Array<{ name: string }>;
|
||||
state?: "open" | "closed";
|
||||
updatedAt?: string;
|
||||
author?: string | null;
|
||||
}>> {
|
||||
if (this.hasGhAuth()) {
|
||||
try {
|
||||
@@ -3085,6 +3086,7 @@ export class GitHubClient {
|
||||
labels: Array<{ name: string }>;
|
||||
state?: "open" | "closed";
|
||||
updatedAt?: string;
|
||||
author?: string | null;
|
||||
}>> {
|
||||
const limit = options?.limit ?? 30;
|
||||
const state = options?.state ?? "open";
|
||||
@@ -3098,12 +3100,14 @@ export class GitHubClient {
|
||||
labels: Array<{ name: string }>;
|
||||
state: "OPEN" | "CLOSED";
|
||||
updatedAt: string;
|
||||
author?: { login?: string } | null;
|
||||
}>>([
|
||||
"issue", "list",
|
||||
"--repo", `${owner}/${repo}`,
|
||||
"--state", state,
|
||||
"--limit", String(Math.min(limit, 100)),
|
||||
"--json", "number,title,body,url,labels,state,updatedAt",
|
||||
// FNXC:GitHubImport 2026-06-22-18:30: Request `author` so the import preview pane can show full issue metadata (author/state alongside the already-present full body) without a per-item detail fetch.
|
||||
"--json", "number,title,body,url,labels,state,updatedAt,author",
|
||||
]);
|
||||
|
||||
let result = issues.map((issue) => ({
|
||||
@@ -3114,6 +3118,7 @@ export class GitHubClient {
|
||||
labels: issue.labels,
|
||||
state: this.mapGhIssueState(issue.state),
|
||||
updatedAt: issue.updatedAt,
|
||||
author: issue.author?.login ?? null,
|
||||
}));
|
||||
|
||||
// Filter by labels if specified (client-side filtering)
|
||||
@@ -3140,6 +3145,7 @@ export class GitHubClient {
|
||||
labels: Array<{ name: string }>;
|
||||
state?: "open" | "closed";
|
||||
updatedAt?: string;
|
||||
author?: string | null;
|
||||
}>> {
|
||||
const limit = options?.limit ?? 30;
|
||||
const state = options?.state ?? "open";
|
||||
@@ -3171,6 +3177,7 @@ export class GitHubClient {
|
||||
labels: Array<{ name: string }>;
|
||||
state: string;
|
||||
updated_at: string;
|
||||
user?: { login?: string } | null;
|
||||
pull_request?: unknown;
|
||||
}>;
|
||||
|
||||
@@ -3185,6 +3192,7 @@ export class GitHubClient {
|
||||
labels: issue.labels,
|
||||
state: this.mapIssueState(issue.state),
|
||||
updatedAt: issue.updated_at,
|
||||
author: issue.user?.login ?? null,
|
||||
}))
|
||||
.slice(0, limit);
|
||||
}
|
||||
@@ -3443,6 +3451,8 @@ export class GitHubClient {
|
||||
html_url: string;
|
||||
headBranch: string;
|
||||
baseBranch: string;
|
||||
state?: "open" | "closed" | "merged";
|
||||
author?: string | null;
|
||||
}>> {
|
||||
if (this.hasGhAuth()) {
|
||||
try {
|
||||
@@ -3472,6 +3482,8 @@ export class GitHubClient {
|
||||
html_url: string;
|
||||
headBranch: string;
|
||||
baseBranch: string;
|
||||
state?: "open" | "closed" | "merged";
|
||||
author?: string | null;
|
||||
}>> {
|
||||
const limit = options?.limit ?? 30;
|
||||
|
||||
@@ -3482,12 +3494,15 @@ export class GitHubClient {
|
||||
url: string;
|
||||
headRefName: string;
|
||||
baseRefName: string;
|
||||
state?: "OPEN" | "CLOSED" | "MERGED";
|
||||
author?: { login?: string } | null;
|
||||
}>>([
|
||||
"pr", "list",
|
||||
"--repo", `${owner}/${repo}`,
|
||||
"--state", "open",
|
||||
"--limit", String(Math.min(limit, 100)),
|
||||
"--json", "number,title,body,url,headRefName,baseRefName",
|
||||
// FNXC:GitHubImport 2026-06-22-18:30: Request `state,author` so the import preview pane shows full PR metadata (author/state with the already-present full body) without a per-item detail fetch.
|
||||
"--json", "number,title,body,url,headRefName,baseRefName,state,author",
|
||||
]);
|
||||
|
||||
return pulls.map((pr) => ({
|
||||
@@ -3497,6 +3512,8 @@ export class GitHubClient {
|
||||
html_url: pr.url,
|
||||
headBranch: pr.headRefName,
|
||||
baseBranch: pr.baseRefName,
|
||||
state: pr.state ? (pr.state.toLowerCase() as "open" | "closed" | "merged") : undefined,
|
||||
author: pr.author?.login ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -3511,6 +3528,8 @@ export class GitHubClient {
|
||||
html_url: string;
|
||||
headBranch: string;
|
||||
baseBranch: string;
|
||||
state?: "open" | "closed" | "merged";
|
||||
author?: string | null;
|
||||
}>> {
|
||||
const limit = options?.limit ?? 30;
|
||||
|
||||
@@ -3537,6 +3556,8 @@ export class GitHubClient {
|
||||
html_url: string;
|
||||
head: { ref: string };
|
||||
base: { ref: string };
|
||||
state?: string;
|
||||
user?: { login?: string } | null;
|
||||
}>;
|
||||
|
||||
return data.slice(0, limit).map((pr) => ({
|
||||
@@ -3546,6 +3567,8 @@ export class GitHubClient {
|
||||
html_url: pr.html_url,
|
||||
headBranch: pr.head.ref,
|
||||
baseBranch: pr.base.ref,
|
||||
state: pr.state === "open" || pr.state === "closed" ? pr.state : undefined,
|
||||
author: pr.user?.login ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user