FN-8016: scope task popups to their opening view
Scope task-detail popups to their origin dashboard view by default. - Default per-view popup scoping while retaining a legacy global-popup opt-out. - Key popup lifecycle, navigation, and Escape dismissal by task and origin view. - Update settings copy, documentation, localization, and regression coverage. Files changed: .changeset/fn-8016-task-popup-view-scoping.md | 7 ++ docs/dashboard-guide.md | 4 +- .../core/src/__tests__/settings-defaults.test.ts | 4 +- packages/core/src/settings-schema.ts | 6 +- packages/core/src/types.ts | 6 +- packages/dashboard/app/App.tsx | 67 ++++++----- .../app/__tests__/App.keyboard-shortcuts.test.tsx | 14 ++- .../app/__tests__/App.taskPopupViewGating.test.tsx | 125 +++++++-------------- .../dashboard/app/components/SettingsModal.tsx | 2 +- .../settings/sections/AppearanceSection.tsx | 6 +- .../sections/__tests__/AppearanceSection.test.tsx | 18 ++- .../app/hooks/__tests__/useAppSettings.test.ts | 15 +++ .../app/hooks/__tests__/usePoppedOutTasks.test.ts | 28 ++--- packages/dashboard/app/hooks/useAppSettings.ts | 8 +- packages/dashboard/app/hooks/usePoppedOutTasks.ts | 14 +-- packages/i18n/locales/en/app.json | 4 +- packages/i18n/src/resources.d.ts | 4 +- 17 files changed, 158 insertions(+), 174 deletions(-) Fusion-Task-Id: FN-8016 Fusion-Task-Lineage: e33beeae-0ce3-4202-95dc-6fb2d26f9770 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8016-task-popup-view-scoping.md
Normal file
7
.changeset/fn-8016-task-popup-view-scoping.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Task-detail popups now open in — and stay scoped to — the view where you opened them.
|
||||
category: fix
|
||||
dev: isTaskPopupVisibleForView now scopes by origin view for all views (not just Board/List); taskPopupsBoardListOnly defaults on and popups dedupe per (task id, origin view) so the same task can open independently in multiple views. Escape/keyboard close carries (taskId, originView) identity. FN-8016.
|
||||
@@ -113,10 +113,10 @@ If **Settings → Appearance → Open tasks in the right sidebar** is enabled, b
|
||||
FNXC:TaskPopupGeometry 2026-07-03-00:00: Desktop/tablet task popups share a persisted geometry key across task IDs so operators can size and place the popup once, then open other tasks without repeating that setup. Mobile remains a full-screen sheet regardless of saved desktop geometry.
|
||||
FNXC:RightDockTaskPopup 2026-07-03-00:00: The same task-popup preference also applies to ordinary clicks in the right-dock Tasks list so that opted-in operators never lose the list to embedded detail by clicking a dock task row.
|
||||
FNXC:TaskPopupLayer 2026-07-04-18:36: Ordinary task popups sit on the board/task-detail layer instead of the global floating-utility stack so the board/list context remains visually primary while Terminal, Quick Chat, file browser, workflow editor, and other utility windows keep their top utility stacking.
|
||||
FNXC:TaskPopupViewGating 2026-07-13-00:00: The default-off popup view attachment setting must be documented as render-only hiding, not closing, because popup snapshots and shared geometry survive view switches. -->
|
||||
FNXC:TaskPopupViewGating 2026-07-15-15:20: FN-8016 makes popup view scoping default-on for every dashboard view. Hidden popups are render-only hidden, not closed, so snapshots and shared geometry survive navigation; same-task popups remain independently addressable by origin view. -->
|
||||
**Settings → Appearance → Open tasks as popups** changes ordinary board task-card clicks, List row/card opens, and right-dock Tasks-list clicks across desktop, tablet, and mobile viewports. When enabled, those clicks use the existing task popup/FloatingWindow surface on the board/task-detail layer instead of the full-panel task detail, List split-detail/docked detail, or right-dock task detail, keeping the board, List view, or dock list visible in the background while utility windows keep their higher global stacking. On desktop and tablet, task popups restore the last saved popup size and position between tasks; on mobile, task popups stay full-screen sheets. Deep `changes`/`retries`/`workflow` opens, List context-menu/refine actions, task-detail links, plugin/graph opens, and explicit pop-out actions keep their existing paths.
|
||||
|
||||
**Settings → Appearance → Keep task popups on their Board/List view** is default off. When enabled, task-detail popups opened from Board or List stay attached to that originating view: navigating to Command Center, Agents, Settings, or a different task view hides the popup without closing it, and returning to the originating Board/List view re-shows the same popup in the same saved position. Explicit task opens from Mailbox, Artifacts/Documents, and other non-Board/List surfaces remain visible so their **View task** actions always open a usable window.
|
||||
**Settings → Appearance → Keep task popups on the view where they were opened** is enabled by default. Every task-detail popup is attached to its exact originating view, including Planning, Agents, Command Center, Documents, Missions, and plugin views: navigating elsewhere hides it without closing it, and returning re-shows it in the same saved position. You can open the same task independently in more than one view; closing or pressing Escape on one popup does not affect the other. Disable this setting only to restore legacy globally shared popups. Legacy saved popups without an origin remain visible everywhere for compatibility.
|
||||
|
||||
<!-- FNXC:DashboardNavigationDocs 2026-06-27-00:00: The right dock now hosts Chat as an inline tool panel; keep this user-facing roster aligned with STATIC_OVERFLOW_VIEW_ENTRIES so users know Chat can also pop out from the dock. -->
|
||||
<!-- FNXC:RightDockTasks 2026-06-28-19:55: The dock task-detail overlay is now anchored to the first-class Tasks tool tab. Document that Tasks is a dock-only auxiliary surface with a last-viewed detail/list fallback, not a new primary navigation destination. -->
|
||||
|
||||
@@ -178,8 +178,8 @@ describe("settings defaults invariants", () => {
|
||||
});
|
||||
|
||||
describe("taskPopupsBoardListOnly default", () => {
|
||||
it("keeps taskPopupsBoardListOnly explicitly false in project defaults", () => {
|
||||
expect(DEFAULT_PROJECT_SETTINGS.taskPopupsBoardListOnly).toBe(false);
|
||||
it("keeps taskPopupsBoardListOnly explicitly true in project defaults", () => {
|
||||
expect(DEFAULT_PROJECT_SETTINGS.taskPopupsBoardListOnly).toBe(true);
|
||||
expect("taskPopupsBoardListOnly" in DEFAULT_PROJECT_SETTINGS).toBe(true);
|
||||
expect(PROJECT_SETTINGS_KEYS).toContain("taskPopupsBoardListOnly");
|
||||
});
|
||||
|
||||
@@ -417,10 +417,10 @@ export const DEFAULT_PROJECT_SETTINGS = {
|
||||
*/
|
||||
openMobileTasksInPopup: false,
|
||||
/*
|
||||
FNXC:TaskPopupViewGating 2026-07-13-00:00:
|
||||
Default off preserves current always-visible task-detail popup behavior. When true, the dashboard render gate shows each open task-detail FloatingWindow only on the Board/List view where it was opened without clearing popup snapshots or their shared persisted geometry.
|
||||
FNXC:TaskPopupViewGating 2026-07-15-15:20:
|
||||
FN-8016 defaults task-detail popups to their opening view on every dashboard surface. Explicit false retains globally shared popup behavior for operators who need it; hidden popups preserve snapshots and shared persisted geometry.
|
||||
*/
|
||||
taskPopupsBoardListOnly: false,
|
||||
taskPopupsBoardListOnly: true,
|
||||
/*
|
||||
FNXC:TaskCardCostBadge 2026-07-11-12:15:
|
||||
Default off preserves existing board-card density. When true, the dashboard may render a read-time derived cost badge only for tasks with positive token usage; unavailable pricing remains the guess-free “—” sentinel.
|
||||
|
||||
@@ -3359,10 +3359,10 @@ export interface ProjectSettings {
|
||||
*/
|
||||
openMobileTasksInPopup?: boolean;
|
||||
/**
|
||||
* When true, open task-detail popups render only on the Board/List view where they were opened. Default: false.
|
||||
* When true, open task-detail popups render only on the view where they were opened. Default: true.
|
||||
*
|
||||
* FNXC:TaskPopupViewGating 2026-07-13-00:00:
|
||||
* This project-scoped setting is default-off so currently opened task-detail FloatingWindows remain visible across all main-content views unless operators opt in. When true, open task-detail popups attach to the Board/List view where they were opened; popup state is preserved across view switches and never cleared, so returning to that view restores the same popups and persisted position.
|
||||
* FNXC:TaskPopupViewGating 2026-07-15-15:20:
|
||||
* FN-8016 removed the Board/List restriction so every dashboard view can own task-detail FloatingWindows. This project-scoped setting defaults on; explicit false retains legacy globally shared popups. Scoped popup state is preserved across view switches and returning restores the same persisted position.
|
||||
*/
|
||||
taskPopupsBoardListOnly?: boolean;
|
||||
/**
|
||||
|
||||
@@ -79,7 +79,7 @@ import { useScopedDismissFlag } from "./hooks/useScopedDismissFlag";
|
||||
import { useCapacityRiskBanner } from "./hooks/useCapacityRiskBanner";
|
||||
import { useMainPanelTaskDetail } from "./hooks/useMainPanelTaskDetail";
|
||||
import { useBoardScrollRestore } from "./hooks/useBoardScrollRestore";
|
||||
import { usePoppedOutTasks } from "./hooks/usePoppedOutTasks";
|
||||
import { usePoppedOutTasks, type PoppedOutTaskEntry } from "./hooks/usePoppedOutTasks";
|
||||
import { NativeShellOnboardingModal } from "./components/NativeShellOnboardingModal";
|
||||
import { NativeShellConnectionManager } from "./components/NativeShellConnectionManager";
|
||||
import { ShellConnectionStatus } from "./components/ShellConnectionStatus";
|
||||
@@ -212,32 +212,35 @@ export function getBoardTaskOpenRoute(options: {
|
||||
}
|
||||
|
||||
export interface DashboardShortcutPopupState {
|
||||
poppedOutTaskIds: string[];
|
||||
poppedOutTaskEntries: Array<Pick<PoppedOutTaskEntry, "task" | "originTaskView">>;
|
||||
quickChatOpen: boolean;
|
||||
terminalOpen: boolean;
|
||||
modalClosers: Array<[boolean, () => void]>;
|
||||
}
|
||||
|
||||
export interface DashboardShortcutPopupHandlers {
|
||||
closePoppedOutTask: (taskId: string) => void;
|
||||
closePoppedOutTask: (taskId: string, originTaskView?: TaskView) => void;
|
||||
closeQuickChat: () => void;
|
||||
closeTerminal: () => void;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:TaskPopupViewGating 2026-07-15-14:55:
|
||||
The board/list-only preference attaches only popups opened from Board or List. Mailbox and Documents have no board/list origin, so their explicit View task actions must remain visible on every surface rather than creating an invisible FloatingWindow.
|
||||
FNXC:TaskPopupViewGating 2026-07-15-15:20:
|
||||
FN-8016 scopes every newly opened task popup to its exact dashboard origin, not only Board/List. Legacy entries without an origin predate this contract and deliberately remain visible on every view for backwards compatibility.
|
||||
*/
|
||||
export function isTaskPopupVisibleForView(options: {
|
||||
taskPopupsBoardListOnly: boolean;
|
||||
taskView: TaskView;
|
||||
originTaskView?: TaskView;
|
||||
}): boolean {
|
||||
if (!options.taskPopupsBoardListOnly) return true;
|
||||
if (options.originTaskView !== "board" && options.originTaskView !== "list") return true;
|
||||
if (!options.taskPopupsBoardListOnly || options.originTaskView === undefined) return true;
|
||||
return options.originTaskView === options.taskView;
|
||||
}
|
||||
|
||||
export function taskPopupIdentityKey(taskId: string, originTaskView?: TaskView): string {
|
||||
return `${taskId}:${originTaskView ?? "global"}`;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:DashboardShortcuts 2026-07-04-12:02:
|
||||
The App-level Escape close order is factored into a pure helper so regression tests can prove the real dashboard shell ordering without rendering every lazy dashboard surface. The helper must close exactly one surface and return false when no popup is open so component-local Escape handlers remain authoritative.
|
||||
@@ -246,9 +249,9 @@ export function closeTopmostDashboardPopupForShortcut(
|
||||
state: DashboardShortcutPopupState,
|
||||
handlers: DashboardShortcutPopupHandlers,
|
||||
): boolean {
|
||||
const lastPoppedOutTaskId = state.poppedOutTaskIds[state.poppedOutTaskIds.length - 1];
|
||||
if (lastPoppedOutTaskId) {
|
||||
handlers.closePoppedOutTask(lastPoppedOutTaskId);
|
||||
const lastPoppedOutTask = state.poppedOutTaskEntries[state.poppedOutTaskEntries.length - 1];
|
||||
if (lastPoppedOutTask) {
|
||||
handlers.closePoppedOutTask(lastPoppedOutTask.task.id, lastPoppedOutTask.originTaskView);
|
||||
return true;
|
||||
}
|
||||
if (state.quickChatOpen) {
|
||||
@@ -475,8 +478,8 @@ function AppInner() {
|
||||
const { capture: captureCurrentBoardScrollSnapshot, requestRestore } = useBoardScrollRestore(taskView);
|
||||
const mainPanelDetailNavRevertRef = useRef<(() => void) | null>(null);
|
||||
/*
|
||||
FNXC:FloatingWindow 2026-06-22-20:45:
|
||||
Open popped-out task-detail windows. Each entry is a task snapshot rendered inside its own movable, resizable, non-blocking FloatingWindow. Several can be open at once and coexist with the right-dock pop-out and terminal (all click-through overlays). Snapshots survive a tasks revalidation; rendering prefers the live row by id and falls back to the snapshot. Pop-out dedupes by task id — re-popping an already-open task is a no-op (its window stays; focus-to-front in FloatingWindow handles re-raising on click).
|
||||
FNXC:FloatingWindow 2026-07-15-15:20:
|
||||
FN-8016 identifies a popped-out task detail by task id plus origin view. The same task can therefore coexist in separate view-scoped FloatingWindows while re-opening it on one view refreshes only that entry.
|
||||
*/
|
||||
const { entries: poppedOutTaskEntries, popOut: popOutTaskDetail, close: closePoppedOutTask } = usePoppedOutTasks();
|
||||
const popupNavCloseRef = useRef(new Map<string, () => void>());
|
||||
@@ -488,23 +491,25 @@ function AppInner() {
|
||||
iOS swipe, and Android Back dismiss only that popup rather than leaving the
|
||||
Fusion stack empty and allowing Back to skip past the originating task view.
|
||||
*/
|
||||
const closePoppedOutTaskWithNav = useCallback((taskId: string) => {
|
||||
const closeFromHistory = popupNavCloseRef.current.get(taskId);
|
||||
const closePoppedOutTaskWithNav = useCallback((taskId: string, originTaskView?: TaskView) => {
|
||||
const identityKey = taskPopupIdentityKey(taskId, originTaskView);
|
||||
const closeFromHistory = popupNavCloseRef.current.get(identityKey);
|
||||
if (closeFromHistory) {
|
||||
popupNavCloseRef.current.delete(taskId);
|
||||
popupNavCloseRef.current.delete(identityKey);
|
||||
removeNav(closeFromHistory);
|
||||
}
|
||||
closePoppedOutTask(taskId);
|
||||
closePoppedOutTask(taskId, originTaskView);
|
||||
}, [closePoppedOutTask, removeNav]);
|
||||
|
||||
const popOutTaskDetailForCurrentView = useCallback((task: Task | TaskDetail) => {
|
||||
const alreadyOpen = poppedOutTaskEntries.some((entry) => entry.task.id === task.id);
|
||||
const identityKey = taskPopupIdentityKey(task.id, taskView);
|
||||
const alreadyOpen = poppedOutTaskEntries.some((entry) => entry.task.id === task.id && entry.originTaskView === taskView);
|
||||
if (isMobile && !alreadyOpen) {
|
||||
const closeFromHistory = () => {
|
||||
popupNavCloseRef.current.delete(task.id);
|
||||
closePoppedOutTask(task.id);
|
||||
popupNavCloseRef.current.delete(identityKey);
|
||||
closePoppedOutTask(task.id, taskView);
|
||||
};
|
||||
popupNavCloseRef.current.set(task.id, closeFromHistory);
|
||||
popupNavCloseRef.current.set(identityKey, closeFromHistory);
|
||||
pushNav({ type: "modal", close: closeFromHistory });
|
||||
}
|
||||
popOutTaskDetail(task, taskView);
|
||||
@@ -693,14 +698,13 @@ function AppInner() {
|
||||
originTaskView,
|
||||
}), [taskPopupsBoardListOnly, taskView]);
|
||||
/*
|
||||
FNXC:TaskPopupViewGating 2026-07-15-14:55:
|
||||
Default-off preserves globally visible task popups. When enabled, only Board/List-origin popups are render-attached to their originating view; Mailbox, Documents, and other non-task-view opens remain visible so their View task actions always open a usable window.
|
||||
FNXC:TaskPopupViewGating 2026-07-15-15:20:
|
||||
FN-8016 defaults popup rendering to the originating view for every dashboard surface. Entries without an origin are legacy snapshots and intentionally remain globally visible; navigating away unmounts scoped FloatingWindow shells without deleting their state.
|
||||
*/
|
||||
const visiblePoppedOutTaskEntries = useMemo(
|
||||
() => poppedOutTaskEntries.filter((entry) => taskPopupsVisibleOnCurrentView(entry.originTaskView)),
|
||||
[poppedOutTaskEntries, taskPopupsVisibleOnCurrentView],
|
||||
);
|
||||
const visiblePoppedOutTasks = useMemo(() => visiblePoppedOutTaskEntries.map((entry) => entry.task), [visiblePoppedOutTaskEntries]);
|
||||
|
||||
const pluginDashboardViews = useMemo<PluginDashboardViewEntry[]>(() => {
|
||||
/*
|
||||
@@ -1087,7 +1091,7 @@ function AppInner() {
|
||||
*/
|
||||
return closeTopmostDashboardPopupForShortcut(
|
||||
{
|
||||
poppedOutTaskIds: visiblePoppedOutTasks.map((task) => task.id),
|
||||
poppedOutTaskEntries: visiblePoppedOutTaskEntries,
|
||||
quickChatOpen,
|
||||
terminalOpen: modalManager.terminalOpen,
|
||||
modalClosers: [
|
||||
@@ -1116,7 +1120,7 @@ function AppInner() {
|
||||
closeTerminal: closeTerminalWithNav,
|
||||
},
|
||||
);
|
||||
}, [closePoppedOutTaskWithNav, closeTerminalWithNav, modalManager, quickChatOpen, visiblePoppedOutTasks]);
|
||||
}, [closePoppedOutTaskWithNav, closeTerminalWithNav, modalManager, quickChatOpen, visiblePoppedOutTaskEntries]);
|
||||
|
||||
const openFilesWithNav = useCallback((workspace?: string, initialFile?: string | null) => {
|
||||
modalManager.openFiles(workspace, initialFile);
|
||||
@@ -1845,16 +1849,17 @@ function AppInner() {
|
||||
FNXC:TaskPopupLayer 2026-07-04-18:36:
|
||||
Ordinary task-detail popups belong to the board/task-detail layer, not the global floating-utility stack. Pass the task-detail layer so board/right-dock task opens preserve the visible board context while utility windows keep the higher app-wide raise/focus contract.
|
||||
|
||||
FNXC:TaskPopupViewGating 2026-07-15-14:55:
|
||||
Rendering uses visible entries only; the source hook keeps Board/List-hidden snapshots in React state rather than clearing them on view change. Non-board/list opens stay visible even with the opt-in setting, preserving Mailbox and Documents View task behavior.
|
||||
FNXC:TaskPopupViewGating 2026-07-15-15:20:
|
||||
Rendering uses only the active view's scoped entries; state keeps hidden snapshots so returning remounts them with shared geometry. Each FloatingWindow key includes its origin so identical task ids never collide across views.
|
||||
*/}
|
||||
{visiblePoppedOutTaskEntries.map(({ task: snapshot }) => {
|
||||
{visiblePoppedOutTaskEntries.map(({ task: snapshot, originTaskView }) => {
|
||||
const liveTask = tasks.find((candidate) => candidate.id === snapshot.id) ?? snapshot;
|
||||
const close = () => closePoppedOutTaskWithNav(snapshot.id);
|
||||
const popupKey = taskPopupIdentityKey(snapshot.id, originTaskView);
|
||||
const close = () => closePoppedOutTaskWithNav(snapshot.id, originTaskView);
|
||||
return (
|
||||
<FloatingWindow
|
||||
key={snapshot.id}
|
||||
windowKey={`task-detail-${snapshot.id}`}
|
||||
key={popupKey}
|
||||
windowKey={`task-detail-${snapshot.id}-${originTaskView ?? "global"}`}
|
||||
title={liveTask.id}
|
||||
onClose={close}
|
||||
hideHeader
|
||||
|
||||
@@ -111,41 +111,43 @@ describe("App dashboard keyboard shortcuts", () => {
|
||||
|
||||
expect(closeTopmostDashboardPopupForShortcut(
|
||||
{
|
||||
poppedOutTaskIds: ["FN-1", "FN-2"],
|
||||
// FN-8016: explicit globally-visible opt-out can expose both same-id entries;
|
||||
// Escape must preserve origin identity and close only the topmost one.
|
||||
poppedOutTaskEntries: [{ task: { id: "FN-1" }, originTaskView: "board" }, { task: { id: "FN-1" }, originTaskView: "planning" }],
|
||||
quickChatOpen: true,
|
||||
terminalOpen: true,
|
||||
modalClosers: [[true, closeSettings], [true, closeTaskDetail]],
|
||||
},
|
||||
{ closePoppedOutTask, closeQuickChat, closeTerminal },
|
||||
)).toBe(true);
|
||||
expect(closePoppedOutTask).toHaveBeenCalledWith("FN-2");
|
||||
expect(closePoppedOutTask).toHaveBeenCalledWith("FN-1", "planning");
|
||||
expect(closeQuickChat).not.toHaveBeenCalled();
|
||||
expect(closeTerminal).not.toHaveBeenCalled();
|
||||
expect(closeSettings).not.toHaveBeenCalled();
|
||||
|
||||
expect(closeTopmostDashboardPopupForShortcut(
|
||||
{ poppedOutTaskIds: [], quickChatOpen: true, terminalOpen: true, modalClosers: [[true, closeSettings]] },
|
||||
{ poppedOutTaskEntries: [], quickChatOpen: true, terminalOpen: true, modalClosers: [[true, closeSettings]] },
|
||||
{ closePoppedOutTask, closeQuickChat, closeTerminal },
|
||||
)).toBe(true);
|
||||
expect(closeQuickChat).toHaveBeenCalledTimes(1);
|
||||
expect(closeTerminal).not.toHaveBeenCalled();
|
||||
|
||||
expect(closeTopmostDashboardPopupForShortcut(
|
||||
{ poppedOutTaskIds: [], quickChatOpen: false, terminalOpen: true, modalClosers: [[true, closeSettings]] },
|
||||
{ poppedOutTaskEntries: [], quickChatOpen: false, terminalOpen: true, modalClosers: [[true, closeSettings]] },
|
||||
{ closePoppedOutTask, closeQuickChat, closeTerminal },
|
||||
)).toBe(true);
|
||||
expect(closeTerminal).toHaveBeenCalledTimes(1);
|
||||
expect(closeSettings).not.toHaveBeenCalled();
|
||||
|
||||
expect(closeTopmostDashboardPopupForShortcut(
|
||||
{ poppedOutTaskIds: [], quickChatOpen: false, terminalOpen: false, modalClosers: [[false, closeSettings], [true, closeTaskDetail]] },
|
||||
{ poppedOutTaskEntries: [], quickChatOpen: false, terminalOpen: false, modalClosers: [[false, closeSettings], [true, closeTaskDetail]] },
|
||||
{ closePoppedOutTask, closeQuickChat, closeTerminal },
|
||||
)).toBe(true);
|
||||
expect(closeTaskDetail).toHaveBeenCalledTimes(1);
|
||||
expect(closeSettings).not.toHaveBeenCalled();
|
||||
|
||||
expect(closeTopmostDashboardPopupForShortcut(
|
||||
{ poppedOutTaskIds: [], quickChatOpen: false, terminalOpen: false, modalClosers: [[false, closeSettings]] },
|
||||
{ poppedOutTaskEntries: [], quickChatOpen: false, terminalOpen: false, modalClosers: [[false, closeSettings]] },
|
||||
{ closePoppedOutTask, closeQuickChat, closeTerminal },
|
||||
)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -10,108 +10,59 @@ function task(id: string): Task {
|
||||
return { id, title: id, status: "todo" } as Task;
|
||||
}
|
||||
|
||||
function PopupGateHarness({
|
||||
entries,
|
||||
taskView,
|
||||
taskPopupsBoardListOnly,
|
||||
}: {
|
||||
function popupTestId(taskId: string, originTaskView?: TaskView) {
|
||||
return `floating-window-task-detail-${taskId}-${originTaskView ?? "global"}`;
|
||||
}
|
||||
|
||||
function PopupGateHarness({ entries, taskView, taskPopupsBoardListOnly }: {
|
||||
entries: PoppedOutTaskEntry[];
|
||||
taskView: TaskView;
|
||||
taskPopupsBoardListOnly: boolean;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{entries
|
||||
.filter((entry) => isTaskPopupVisibleForView({ taskPopupsBoardListOnly, taskView, originTaskView: entry.originTaskView }))
|
||||
.map(({ task: snapshot }) => (
|
||||
<FloatingWindow
|
||||
key={snapshot.id}
|
||||
windowKey={`task-detail-${snapshot.id}`}
|
||||
title={snapshot.id}
|
||||
onClose={() => {}}
|
||||
hideHeader
|
||||
dragHandleSelector=".task-detail-content--embedded > .modal-header"
|
||||
className="floating-window--task-detail"
|
||||
persistGeometryKey={TASK_DETAIL_FLOATING_GEOMETRY_KEY}
|
||||
layer="task-detail"
|
||||
>
|
||||
<div className="task-detail-content--embedded">
|
||||
<div className="modal-header">{snapshot.id}</div>
|
||||
<div>{snapshot.title}</div>
|
||||
</div>
|
||||
</FloatingWindow>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
return <>{entries.filter((entry) => isTaskPopupVisibleForView({ taskPopupsBoardListOnly, taskView, originTaskView: entry.originTaskView })).map(({ task: snapshot, originTaskView }) => {
|
||||
const windowKey = `task-detail-${snapshot.id}-${originTaskView ?? "global"}`;
|
||||
return <FloatingWindow key={windowKey} windowKey={windowKey} title={snapshot.id} onClose={() => {}} hideHeader dragHandleSelector=".task-detail-content--embedded > .modal-header" className="floating-window--task-detail" persistGeometryKey={TASK_DETAIL_FLOATING_GEOMETRY_KEY} layer="task-detail">
|
||||
<div className="task-detail-content--embedded"><div className="modal-header">{snapshot.id}</div></div>
|
||||
</FloatingWindow>;
|
||||
})}</>;
|
||||
}
|
||||
|
||||
function expectNoTaskPopupShell(taskId: string) {
|
||||
expect(screen.queryByTestId(`floating-window-task-detail-${taskId}`)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(`floating-window-overlay-task-detail-${taskId}`)).not.toBeInTheDocument();
|
||||
function expectNoTaskPopupShell(taskId: string, originTaskView?: TaskView) {
|
||||
const id = popupTestId(taskId, originTaskView);
|
||||
expect(screen.queryByTestId(id)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(id.replace("floating-window-", "floating-window-overlay-"))).not.toBeInTheDocument();
|
||||
}
|
||||
|
||||
const origins: TaskView[] = ["board", "list", "planning", "agents", "command-center", "missions", "documents", "plugin:sample"];
|
||||
|
||||
describe("App task popup view gating", () => {
|
||||
it("keeps default/off popups visible regardless of the active view", () => {
|
||||
render(
|
||||
<PopupGateHarness
|
||||
taskView="command-center"
|
||||
taskPopupsBoardListOnly={false}
|
||||
entries={[{ task: task("FN-7944-A"), originTaskView: "board" }]}
|
||||
/>,
|
||||
);
|
||||
it.each(origins)("renders a %s-origin popup only on its origin when scoping is enabled", (originTaskView) => {
|
||||
const entry = { task: task(`FN-8016-${originTaskView}`), originTaskView };
|
||||
const { rerender } = render(<PopupGateHarness taskView={originTaskView} taskPopupsBoardListOnly entries={[entry]} />);
|
||||
expect(screen.getByTestId(popupTestId(entry.task.id, originTaskView))).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId("floating-window-task-detail-FN-7944-A")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("floating-window-overlay-task-detail-FN-7944-A")).toBeInTheDocument();
|
||||
rerender(<PopupGateHarness taskView="settings" taskPopupsBoardListOnly entries={[entry]} />);
|
||||
expectNoTaskPopupShell(entry.task.id, originTaskView);
|
||||
});
|
||||
|
||||
it("attaches enabled popups to the Board/List view where they were opened", () => {
|
||||
const entries: PoppedOutTaskEntry[] = [
|
||||
{ task: task("FN-7944-board"), originTaskView: "board" },
|
||||
{ task: task("FN-7944-list"), originTaskView: "list" },
|
||||
];
|
||||
|
||||
const { rerender } = render(<PopupGateHarness taskView="board" taskPopupsBoardListOnly entries={entries} />);
|
||||
|
||||
expect(screen.getByTestId("floating-window-task-detail-FN-7944-board")).toBeInTheDocument();
|
||||
expectNoTaskPopupShell("FN-7944-list");
|
||||
|
||||
rerender(<PopupGateHarness taskView="list" taskPopupsBoardListOnly entries={entries} />);
|
||||
|
||||
expectNoTaskPopupShell("FN-7944-board");
|
||||
expect(screen.getByTestId("floating-window-task-detail-FN-7944-list")).toBeInTheDocument();
|
||||
it("reproduces the planning-origin symptom and keeps another non-board/list view scoped", () => {
|
||||
expect(isTaskPopupVisibleForView({ taskPopupsBoardListOnly: true, taskView: "planning", originTaskView: "planning" })).toBe(true);
|
||||
expect(isTaskPopupVisibleForView({ taskPopupsBoardListOnly: true, taskView: "agents", originTaskView: "agents" })).toBe(true);
|
||||
expect(isTaskPopupVisibleForView({ taskPopupsBoardListOnly: true, taskView: "agents", originTaskView: "planning" })).toBe(false);
|
||||
});
|
||||
|
||||
it("hides all attached popups on non-task views without leaving shells or overlays, then re-shows the same entry", () => {
|
||||
const entries: PoppedOutTaskEntry[] = [
|
||||
{ task: task("FN-7944-board"), originTaskView: "board" },
|
||||
{ task: task("FN-7944-list"), originTaskView: "list" },
|
||||
];
|
||||
|
||||
const { rerender } = render(<PopupGateHarness taskView="board" taskPopupsBoardListOnly entries={entries} />);
|
||||
expect(screen.getByTestId("floating-window-task-detail-FN-7944-board")).toBeInTheDocument();
|
||||
|
||||
rerender(<PopupGateHarness taskView="agents" taskPopupsBoardListOnly entries={entries} />);
|
||||
expectNoTaskPopupShell("FN-7944-board");
|
||||
expectNoTaskPopupShell("FN-7944-list");
|
||||
|
||||
rerender(<PopupGateHarness taskView="board" taskPopupsBoardListOnly entries={entries} />);
|
||||
expect(screen.getByTestId("floating-window-task-detail-FN-7944-board")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("floating-window-body-task-detail-FN-7944-board")).toHaveTextContent("FN-7944-board");
|
||||
it("treats legacy undefined-origin snapshots as globally visible", () => {
|
||||
render(<PopupGateHarness taskView="planning" taskPopupsBoardListOnly entries={[{ task: task("FN-8016-legacy") }]} />);
|
||||
expect(screen.getByTestId(popupTestId("FN-8016-legacy"))).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps Mailbox and other non-board/list popups visible when attachment is enabled", () => {
|
||||
render(
|
||||
<PopupGateHarness
|
||||
taskView="command-center"
|
||||
taskPopupsBoardListOnly
|
||||
entries={[
|
||||
{ task: task("FN-7976-mailbox") },
|
||||
{ task: task("FN-7976-command"), originTaskView: "command-center" },
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("floating-window-task-detail-FN-7976-mailbox")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("floating-window-task-detail-FN-7976-command")).toBeInTheDocument();
|
||||
it("unmounts on navigation and remounts the original scoped entry", () => {
|
||||
const entry = { task: task("FN-8016-remount"), originTaskView: "planning" as const };
|
||||
const { rerender } = render(<PopupGateHarness taskView="planning" taskPopupsBoardListOnly entries={[entry]} />);
|
||||
expect(screen.getByTestId(popupTestId(entry.task.id, entry.originTaskView))).toBeInTheDocument();
|
||||
rerender(<PopupGateHarness taskView="agents" taskPopupsBoardListOnly entries={[entry]} />);
|
||||
expectNoTaskPopupShell(entry.task.id, entry.originTaskView);
|
||||
rerender(<PopupGateHarness taskView="planning" taskPopupsBoardListOnly entries={[entry]} />);
|
||||
expect(screen.getByTestId(popupTestId(entry.task.id, entry.originTaskView))).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1057,7 +1057,7 @@ export function SettingsModal({
|
||||
showWorktreeGrouping: false,
|
||||
openTasksInRightSidebar: false,
|
||||
openMobileTasksInPopup: false,
|
||||
taskPopupsBoardListOnly: false,
|
||||
taskPopupsBoardListOnly: true,
|
||||
showCostBadgeOnCards: false,
|
||||
taskDetailChatFirst: false,
|
||||
executorAllowSiblingBranchRename: false,
|
||||
|
||||
@@ -53,12 +53,12 @@ export function AppearanceSection({ scopeBanner, form, setForm, themeMode, color
|
||||
<small className="form-text text-muted">{t("settings.appearance.openMobileTasksInPopupHelp", "When enabled, ordinary board task-card, List row/card, and right-dock Tasks-list clicks open the existing movable task popup so the board or list remains visible. Deep-tab and other task opens keep their current behavior. Default: disabled.")}</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
{/* FNXC:TaskPopupViewGating 2026-07-13-00:00: This project-scoped setting is opt-in because existing task popups float globally by default. When enabled, each popup stays attached to the Board/List view where it was opened, hiding on other views without closing or clearing geometry. */}
|
||||
{/* FNXC:TaskPopupViewGating 2026-07-15-15:20: FN-8016 scopes task popups to their opening dashboard view by default. Operators may explicitly disable it for legacy globally shared popups; hidden scoped entries retain geometry and reopen on return. */}
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" checked={form.taskPopupsBoardListOnly === true} onChange={(e) => setForm((f) => ({ ...f, taskPopupsBoardListOnly: e.target.checked }))}/>
|
||||
<span>{t("settings.appearance.taskPopupsBoardListOnly", "Keep task popups on their Board/List view")}</span>
|
||||
<span>{t("settings.appearance.taskPopupsBoardListOnly", "Keep task popups on the view where they were opened")}</span>
|
||||
</label>
|
||||
<small className="form-text text-muted">{t("settings.appearance.taskPopupsBoardListOnlyHelp", "When enabled, each open task-detail popup appears only on the Board or List view where it was opened. Switching to another view hides it without closing; returning to that view restores it in the same position. Default: disabled.")}</small>
|
||||
<small className="form-text text-muted">{t("settings.appearance.taskPopupsBoardListOnlyHelp", "When enabled, each open task-detail popup appears only on the view where it was opened. Switching views hides it without closing; returning restores it in the same position. Default: enabled.")}</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
{/* FNXC:TaskCardCostBadge 2026-07-11-12:15: This project setting is opt-in because board cards are already dense; when enabled, only tasks with recorded positive token usage render a read-time derived spend badge. */}
|
||||
|
||||
@@ -21,7 +21,7 @@ function renderAppearanceSection(formOverrides: Partial<Settings> = {}) {
|
||||
autoMerge: true,
|
||||
openTasksInRightSidebar: false,
|
||||
openMobileTasksInPopup: false,
|
||||
taskPopupsBoardListOnly: false,
|
||||
taskPopupsBoardListOnly: true,
|
||||
showCostBadgeOnCards: false,
|
||||
taskDetailChatFirst: false,
|
||||
...formOverrides,
|
||||
@@ -70,8 +70,6 @@ describe("AppearanceSection", () => {
|
||||
|
||||
const checkbox = screen.getByLabelText("Open tasks as popups");
|
||||
expect(checkbox).not.toBeChecked();
|
||||
expect(screen.getByText(/ordinary board task-card and right-dock Tasks-list clicks open the existing task popup/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Deep-tab and other task opens keep their current behavior/)).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
@@ -88,21 +86,21 @@ describe("AppearanceSection", () => {
|
||||
it("renders and updates the task popup view attachment checkbox", () => {
|
||||
const { setForm, getForm } = renderAppearanceSection();
|
||||
|
||||
const checkbox = screen.getByLabelText("Keep task popups on their Board/List view");
|
||||
expect(checkbox).not.toBeChecked();
|
||||
expect(screen.getByText(/appears only on the Board or List view where it was opened/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/returning to that view restores it in the same position\. Default: disabled/)).toBeInTheDocument();
|
||||
const checkbox = screen.getByLabelText("Keep task popups on the view where they were opened");
|
||||
expect(checkbox).toBeChecked();
|
||||
expect(screen.getByText(/appears only on the view where it was opened/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/returning restores it in the same position\. Default: enabled/)).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
expect(setForm).toHaveBeenCalledTimes(1);
|
||||
expect(getForm().taskPopupsBoardListOnly).toBe(true);
|
||||
expect(getForm().taskPopupsBoardListOnly).toBe(false);
|
||||
});
|
||||
|
||||
it("reflects a persisted enabled task popup view attachment value", () => {
|
||||
it("reflects the default enabled task popup view scoping value", () => {
|
||||
renderAppearanceSection({ taskPopupsBoardListOnly: true });
|
||||
|
||||
expect(screen.getByLabelText("Keep task popups on their Board/List view")).toBeChecked();
|
||||
expect(screen.getByLabelText("Keep task popups on the view where they were opened")).toBeChecked();
|
||||
});
|
||||
|
||||
it("renders and updates the cost badge checkbox", () => {
|
||||
|
||||
@@ -46,6 +46,21 @@ describe("useAppSettings", () => {
|
||||
mockUpdateSettings.mockResolvedValue({} as never);
|
||||
});
|
||||
|
||||
it("defaults omitted task popup scoping to enabled during hydration", async () => {
|
||||
const { result } = renderHook(() => useAppSettings("proj_123"));
|
||||
|
||||
await waitFor(() => expect(result.current.settingsLoaded).toBe(true));
|
||||
expect(result.current.taskPopupsBoardListOnly).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves an explicit false task popup scoping opt-out during hydration", async () => {
|
||||
mockFetchSettings.mockResolvedValueOnce({ taskPopupsBoardListOnly: false } as never);
|
||||
const { result } = renderHook(() => useAppSettings("proj_123"));
|
||||
|
||||
await waitFor(() => expect(result.current.settingsLoaded).toBe(true));
|
||||
expect(result.current.taskPopupsBoardListOnly).toBe(false);
|
||||
});
|
||||
|
||||
it("loads settings state from API", async () => {
|
||||
const { result } = renderHook(() => useAppSettings("proj_123"));
|
||||
|
||||
|
||||
@@ -5,44 +5,46 @@ import { usePoppedOutTasks } from "../usePoppedOutTasks";
|
||||
const task = (id: string) => ({ id, title: id, status: "todo" } as never);
|
||||
|
||||
describe("usePoppedOutTasks", () => {
|
||||
it("popOut adds a task and upgrades duplicate-id snapshots and origins", () => {
|
||||
it("refreshes duplicate snapshots only for the same task and origin view", () => {
|
||||
const { result } = renderHook(() => usePoppedOutTasks());
|
||||
const stale = { ...task("1"), title: "stale" };
|
||||
const fresh = { ...task("1"), title: "fresh" };
|
||||
|
||||
act(() => {
|
||||
result.current.popOut(stale, "board");
|
||||
result.current.popOut(fresh);
|
||||
result.current.popOut(task("2"));
|
||||
result.current.popOut(fresh, "board");
|
||||
result.current.popOut(task("1"), "planning");
|
||||
});
|
||||
|
||||
expect(result.current.tasks.map((t) => t.id)).toEqual(["1", "2"]);
|
||||
expect(result.current.entries[0]).toEqual({ task: fresh, originTaskView: undefined });
|
||||
expect(result.current.entries).toEqual([
|
||||
{ task: fresh, originTaskView: "board" },
|
||||
{ task: task("1"), originTaskView: "planning" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("records the originating task view for view-attached popups", () => {
|
||||
it("keeps the same task independently open on different origin views", () => {
|
||||
const { result } = renderHook(() => usePoppedOutTasks());
|
||||
|
||||
act(() => {
|
||||
result.current.popOut(task("1"), "board");
|
||||
result.current.popOut(task("2"), "list");
|
||||
result.current.popOut(task("1"), "planning");
|
||||
});
|
||||
|
||||
expect(result.current.entries.map((entry) => [entry.task.id, entry.originTaskView])).toEqual([
|
||||
["1", "board"],
|
||||
["2", "list"],
|
||||
["1", "planning"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("close removes only the matching id", () => {
|
||||
it("closes only the matching task and origin view", () => {
|
||||
const { result } = renderHook(() => usePoppedOutTasks());
|
||||
|
||||
act(() => {
|
||||
result.current.popOut(task("1"));
|
||||
result.current.popOut(task("2"));
|
||||
result.current.close("1");
|
||||
result.current.popOut(task("1"), "board");
|
||||
result.current.popOut(task("1"), "planning");
|
||||
result.current.close("1", "planning");
|
||||
});
|
||||
|
||||
expect(result.current.tasks.map((t) => t.id)).toEqual(["2"]);
|
||||
expect(result.current.entries).toEqual([{ task: task("1"), originTaskView: "board" }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,7 +83,11 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
||||
const [capacityRiskTodoThreshold, setCapacityRiskTodoThreshold] = useState(20);
|
||||
const [openTasksInRightSidebar, setOpenTasksInRightSidebar] = useState(false);
|
||||
const [openMobileTasksInPopup, setOpenMobileTasksInPopup] = useState(false);
|
||||
const [taskPopupsBoardListOnly, setTaskPopupsBoardListOnly] = useState(false);
|
||||
/*
|
||||
FNXC:TaskPopupViewGating 2026-07-15-15:20:
|
||||
FN-8016 makes per-view popup scoping the default. Explicit persisted false remains the compatibility opt-out for globally shared popups; only an absent field falls back to true.
|
||||
*/
|
||||
const [taskPopupsBoardListOnly, setTaskPopupsBoardListOnly] = useState(true);
|
||||
const [showCostBadgeOnCards, setShowCostBadgeOnCards] = useState(false);
|
||||
const [modelPricingOverrides, setModelPricingOverrides] = useState<ModelPricingOverrides | undefined>(undefined);
|
||||
const [taskDetailChatFirst, setTaskDetailChatFirst] = useState(false);
|
||||
@@ -164,7 +168,7 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
||||
setCapacityRiskTodoThreshold(settings.capacityRiskTodoThreshold ?? 20);
|
||||
setOpenTasksInRightSidebar(settings.openTasksInRightSidebar === true);
|
||||
setOpenMobileTasksInPopup(settings.openMobileTasksInPopup === true);
|
||||
setTaskPopupsBoardListOnly(settings.taskPopupsBoardListOnly === true);
|
||||
setTaskPopupsBoardListOnly(settings.taskPopupsBoardListOnly !== false);
|
||||
/*
|
||||
FNXC:TaskCardCostBadge 2026-07-11-12:15:
|
||||
The app shell exposes the default-off card cost badge setting to the board context only after settings hydration, preserving the no-badge default for upgraded projects.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
FNXC:FloatingWindow 2026-07-15-14:55:
|
||||
Popped-out task-detail windows are movable, resizable, non-blocking FloatingWindows. Each entry is a task snapshot; several can be open at once. Reopening an id replaces its snapshot and origin so a stale or previously view-gated entry becomes current and visible. Extracted from AppInner.
|
||||
Popped-out task-detail windows are movable, resizable, non-blocking FloatingWindows. Each entry is a task snapshot; several can be open at once. Reopening the same task from the same origin refreshes its snapshot without collapsing an independently opened popup in another view. Extracted from AppInner.
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
@@ -16,7 +16,7 @@ export interface UsePoppedOutTasksResult {
|
||||
entries: PoppedOutTaskEntry[];
|
||||
tasks: Array<Task | TaskDetail>;
|
||||
popOut: (task: Task | TaskDetail, originTaskView?: TaskView) => void;
|
||||
close: (taskId: string) => void;
|
||||
close: (taskId: string, originTaskView?: TaskView) => void;
|
||||
}
|
||||
|
||||
export function usePoppedOutTasks(): UsePoppedOutTasksResult {
|
||||
@@ -24,7 +24,7 @@ export function usePoppedOutTasks(): UsePoppedOutTasksResult {
|
||||
|
||||
const popOut = useCallback((task: Task | TaskDetail, originTaskView?: TaskView) => {
|
||||
setEntries((current) => {
|
||||
const existingIndex = current.findIndex((entry) => entry.task.id === task.id);
|
||||
const existingIndex = current.findIndex((entry) => entry.task.id === task.id && entry.originTaskView === originTaskView);
|
||||
if (existingIndex === -1) return [...current, { task, originTaskView }];
|
||||
|
||||
const upgraded = [...current];
|
||||
@@ -33,13 +33,13 @@ export function usePoppedOutTasks(): UsePoppedOutTasksResult {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const close = useCallback((taskId: string) => {
|
||||
setEntries((current) => current.filter((entry) => entry.task.id !== taskId));
|
||||
const close = useCallback((taskId: string, originTaskView?: TaskView) => {
|
||||
setEntries((current) => current.filter((entry) => entry.task.id !== taskId || entry.originTaskView !== originTaskView));
|
||||
}, []);
|
||||
|
||||
/*
|
||||
FNXC:TaskPopupViewGating 2026-07-15-14:55:
|
||||
Popups store their opening view so the opt-in gate can attach Board/List popups to that surface. Reopening a duplicate id updates this origin and its snapshot; callers that only need task snapshots can keep reading `tasks`.
|
||||
FNXC:TaskPopupViewGating 2026-07-15-15:20:
|
||||
FN-8016 scopes popup identity to task id plus opening view. Every new pop-out has an origin; undefined origins are retained only for legacy snapshots and remain globally visible for compatibility. Closing receives the same identity so a task open on two views stays independent.
|
||||
*/
|
||||
const tasks = useMemo(() => entries.map((entry) => entry.task), [entries]);
|
||||
|
||||
|
||||
@@ -5695,8 +5695,8 @@
|
||||
"title": "Appearance",
|
||||
"openTasksInRightSidebarHelp": "When enabled, board task cards open detail in the right sidebar when it is available; mobile and hidden-sidebar states keep the full task panel. Default: disabled.",
|
||||
"openMobileTasksInPopupHelp": "When enabled, ordinary board task-card and right-dock Tasks-list clicks open the existing task popup so the board or list remains visible. Deep-tab and other task opens keep their current behavior. Default: disabled.",
|
||||
"taskPopupsBoardListOnly": "Keep task popups on their Board/List view",
|
||||
"taskPopupsBoardListOnlyHelp": "When enabled, each open task-detail popup appears only on the Board or List view where it was opened. Switching to another view hides it without closing; returning to that view restores it in the same position. Default: disabled.",
|
||||
"taskPopupsBoardListOnly": "Keep task popups on the view where they were opened",
|
||||
"taskPopupsBoardListOnlyHelp": "When enabled, each open task-detail popup appears only on the view where it was opened. Switching views hides it without closing; returning restores it in the same position. Default: enabled.",
|
||||
"taskDetailChatFirstHelp": "Off by default: task details list Activity first and omitted non-done opens land on Activity. Turn on to restore Chat-first order/default; explicit Chat links still work either way.",
|
||||
"showCostBadgeOnCards": "Show cost badges on task cards",
|
||||
"showCostBadgeOnCardsHelp": "Default: disabled. When enabled, board cards show derived model cost next to execution time; unavailable pricing displays — and tasks without token usage show no badge."
|
||||
|
||||
4
packages/i18n/src/resources.d.ts
vendored
4
packages/i18n/src/resources.d.ts
vendored
@@ -5699,8 +5699,8 @@ export default interface Resources {
|
||||
"showCostBadgeOnCardsHelp": "Default: disabled. When enabled, board cards show derived model cost next to execution time; unavailable pricing displays — and tasks without token usage show no badge.",
|
||||
"suppressTheLdquoNeedsYourInputRdquoBanner": " Suppress the “needs your input” banner that appears when AI sessions are awaiting input or have failed. ",
|
||||
"taskDetailChatFirstHelp": "Off by default: task details list Activity first and omitted non-done opens land on Activity. Turn on to restore Chat-first order/default; explicit Chat links still work either way.",
|
||||
"taskPopupsBoardListOnly": "Keep task popups on their Board/List view",
|
||||
"taskPopupsBoardListOnlyHelp": "When enabled, each open task-detail popup appears only on the Board or List view where it was opened. Switching to another view hides it without closing; returning to that view restores it in the same position. Default: disabled.",
|
||||
"taskPopupsBoardListOnly": "Keep task popups on the view where they were opened",
|
||||
"taskPopupsBoardListOnlyHelp": "When enabled, each open task-detail popup appears only on the view where it was opened. Switching views hides it without closing; returning restores it in the same position. Default: enabled.",
|
||||
"title": "Appearance"
|
||||
},
|
||||
"auth": {
|
||||
|
||||
Reference in New Issue
Block a user