mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
fix(desktop): isolate draft context and recovery layout
Problem: Draft Git controls could target the preceding formal session, while an empty backing tab could hide draft recovery actions. The runtime file also exceeded the repository's existing size budget. Root cause: Draft presentation inherited formal-tab identity and combined the hidden tab's hero state with the active draft's layout. Shell presentation logic had been added to the already large runtime lifecycle owner. Fix: Project draft workspace labels in the composer builder and clear inherited tab IDs and Git branches. Give active drafts sole ownership of hero visibility and extract presentation predicates into the shell layer. Verification: - focused workspace-context and draft-presentation regressions - pnpm test:typecheck - pnpm build - go run ./tools/repolint - browser check for the clean project draft and project selector
This commit is contained in:
1 parent
6c62b430ef
commit
da9adbd054
7 files changed
+81
-32
No files matched your search
@@ -113,6 +113,27 @@ const draftSurfaceInput = {
|
||||
const draftContext = buildComposerSurface(draftSurfaceInput).props.workspaceContext;
|
||||
assert.equal(draftContext?.workspaceRoot, "/repo", "drafts keep workspace selection when the backing tab is not in its hero state");
|
||||
assert.equal(draftContext?.scopeKey, "draft:workspace-a", "draft workspace actions use the draft owner identity");
|
||||
assert.equal(draftContext?.tabId, undefined, "drafts cannot issue Git RPCs against the previous formal session");
|
||||
assert.equal(draftContext?.gitBranch, undefined, "drafts do not display the previous formal session's branch");
|
||||
for (const [scope, workspaceRoot, workspaceName] of [
|
||||
["project", "/other/project-b", "project-b"],
|
||||
["project", "D:\\Work\\TEST\\", "TEST"],
|
||||
["global", "", undefined],
|
||||
] as const) {
|
||||
const owner = draftSurfaceInput.draft!;
|
||||
const projected = buildComposerSurface({
|
||||
...draftSurfaceInput,
|
||||
workspaceContext: { ...baseContext, remote: true },
|
||||
draft: { ...owner, surface: { ...owner.surface!, draft: { ...owner.surface!.draft, scope, workspaceRoot } } },
|
||||
}).props.workspaceContext;
|
||||
assert.equal(projected?.scope, scope);
|
||||
assert.equal(projected?.workspaceRoot, workspaceRoot);
|
||||
assert.equal(projected?.workspaceName, workspaceName, "project labels belong to the draft, including Windows paths and global drafts");
|
||||
assert.equal(projected?.remote, false);
|
||||
assert.equal(projected?.tabId, undefined);
|
||||
assert.equal(projected?.gitBranch, undefined);
|
||||
assert.equal(projected?.onSwitchWorkspace, baseContext.onSwitchWorkspace, "draft project switching remains available");
|
||||
}
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
assert(rootElement);
|
||||
@@ -191,6 +212,15 @@ await render({ scope: "global", workspaceRoot: "/stale-cwd", workspaceName: "Sta
|
||||
assert(document.querySelector('button[aria-label="Project: No project"]'), "global sessions never inherit a stale cwd label");
|
||||
assert.equal(document.querySelector('button[aria-label="Work without a project"]'), null, "global sessions do not render a redundant clear action");
|
||||
|
||||
await render(draftContext);
|
||||
assert(document.querySelector('button[aria-label="Project: repo"]'), "drafts show their own project");
|
||||
assert.equal(document.querySelector('.composer-workspace-branch'), null);
|
||||
assert.equal(document.querySelector('button[aria-label^="Current Git branch:"]'), null, "drafts cannot open the previous session's Git menu");
|
||||
await click(document.querySelector('button[aria-label="Project: repo"]'));
|
||||
await click([...document.querySelectorAll('.composer-workspace-menu--projects [role="menuitem"]')].find((item) => item.textContent?.includes("Other")) ?? null);
|
||||
assert.deepEqual(calls.switches, ["/other", "/other"], "drafts can still switch projects");
|
||||
assert.equal(calls.history, 1, "draft project actions never query the old session's Git history");
|
||||
|
||||
await act(async () => { root.unmount(); await flush(); });
|
||||
dom.window.close();
|
||||
console.log("PASS composer workspace context project, branch, create and git graph flows");
|
||||
@@ -7,6 +7,7 @@ import { JSDOM } from "jsdom";
|
||||
import type { SessionDraftSurface } from "../app-runtime/useSessionDraftSurface";
|
||||
import { initialState } from "../lib/useController";
|
||||
import { LocaleProvider, type Translator } from "../lib/i18n";
|
||||
import { creationHeroVisible } from "../app-shell/draftPresentation";
|
||||
|
||||
register(new URL("../../scripts/svg-loader.mjs", import.meta.url));
|
||||
const { ChatPaneRegion } = await import("../app-shell/ChatPaneRegion");
|
||||
@@ -45,6 +46,18 @@ const surface: SessionDraftSurface = {
|
||||
};
|
||||
|
||||
const noop = () => {};
|
||||
for (const emptyHero of [true, false]) {
|
||||
assert.equal(creationHeroVisible(null, emptyHero), emptyHero, "formal sessions keep their own layout");
|
||||
assert.equal(creationHeroVisible(surface, emptyHero), true, "healthy drafts own the welcome layout");
|
||||
for (const failure of [
|
||||
{ saveState: "conflict" as const },
|
||||
{ saveState: "error" as const },
|
||||
{ taskError: "Attachment failed" },
|
||||
]) {
|
||||
assert.equal(creationHeroVisible({ ...surface, ...failure }, emptyHero), false,
|
||||
"draft recovery is never collapsed by a hidden empty formal session");
|
||||
}
|
||||
}
|
||||
const commands = { onPrompt: noop, onFork: noop, onLoadOlderHistory: async () => false, onLoadNewerHistory: async () => false, onSurfacePaintReady: noop };
|
||||
const transcript = {
|
||||
state: { ...initialState, meta: { ready: true, eventChannel: "fixture" } },
|
||||
|
||||
@@ -148,22 +148,6 @@ export function draftSubmissionLocksEditing(operation?: SessionDraftSubmissionVi
|
||||
return Boolean(operation && !["cancelled", "terminal_failed"].includes(operation.phase));
|
||||
}
|
||||
|
||||
const DRAFT_ATTENTION_PHASES = new Set([
|
||||
"accepted",
|
||||
"dispatch_unknown",
|
||||
"dispatching_shell",
|
||||
"resume_required",
|
||||
"runtime_failed",
|
||||
"terminal_failed",
|
||||
]);
|
||||
|
||||
/** Healthy drafts use the ordinary new-session landing. Only states that need
|
||||
* an explicit user decision interrupt that surface with recovery controls. */
|
||||
export function draftSurfaceNeedsAttention(draft: SessionDraftSurface): boolean {
|
||||
return draft.saveState === "conflict" || draft.saveState === "error" || Boolean(draft.taskError)
|
||||
|| Boolean(draft.operation && DRAFT_ATTENTION_PHASES.has(draft.operation.phase));
|
||||
}
|
||||
|
||||
function projectEntry(entry: DraftEntry): SessionDraftSurface {
|
||||
return {
|
||||
kind: "draft",
|
||||
|
||||
@@ -15,7 +15,8 @@ import type { useNavigationSurface } from "../lib/useNavigationSurface";
|
||||
import type { useAppShellStores } from "../app-runtime/useAppShellStores";
|
||||
import type { useAppSessionComposition } from "../app-runtime/useAppSessionComposition";
|
||||
import type { useAppNavigationComposition } from "../app-runtime/useAppNavigationComposition";
|
||||
import { draftSurfaceNeedsAttention, type useSessionDraftSurface } from "../app-runtime/useSessionDraftSurface";
|
||||
import type { useSessionDraftSurface } from "../app-runtime/useSessionDraftSurface";
|
||||
import { creationHeroVisible } from "./draftPresentation";
|
||||
import type { HistoryViewState } from "../app-runtime/historyViewProjection";
|
||||
import { ShellHotkeys, TextSizeHotkeys } from "./HotkeyRegistrations";
|
||||
import { WindowChromeLifecycle } from "../app-runtime/WindowChromeLifecycle";
|
||||
@@ -114,7 +115,6 @@ export function AppRuntimeView(props: AppRuntimeViewProps) {
|
||||
const { state, activeTab, activeTabId, t, locale } = core;
|
||||
const { windowsFramelessChrome, mainWindowMaximised } = shell;
|
||||
const draftActive = Boolean(draft.surface);
|
||||
const draftNeedsAttention = draft.surface ? draftSurfaceNeedsAttention(draft.surface) : false;
|
||||
const {
|
||||
conversationView, visibleRuntimeState, sidebarImDetailConnection,
|
||||
surfaceWorkspacePanelRenderable, surfaceWorkspacePanelGridOpen, surfaceWorkspacePanelOverlay, terminalSurfaceOpen,
|
||||
@@ -125,17 +125,11 @@ export function AppRuntimeView(props: AppRuntimeViewProps) {
|
||||
const runtimeTransitioning = core.surface.transitioning;
|
||||
const presentationTransitioning = runtimeTransitioning && core.remoteSurfaceActive;
|
||||
const browserPreviewChrome = navigation.browserPreviewChrome;
|
||||
// A draft owns its workspace independently from whichever formal tab was
|
||||
// visible before it opened. Build the composer context from that owner so a
|
||||
// restored draft never shows the previous project's name or loses the bar.
|
||||
const draftWorkspace = draft.surface?.draft.scope === "project"
|
||||
? (draft.surface.draft.workspaceRoot ?? "").trim()
|
||||
: "";
|
||||
const workspaceContextProject = Boolean(
|
||||
activeTab?.remote || (activeTab?.scope === "project" && activeTab.workspaceRoot) || draftWorkspace,
|
||||
activeTab?.remote || (activeTab?.scope === "project" && activeTab.workspaceRoot),
|
||||
);
|
||||
const workspaceContextRoot = workspaceContextProject
|
||||
? draftWorkspace || (activeTab?.workspaceRoot ?? state.meta?.workspaceRoot ?? state.meta?.cwd ?? "")
|
||||
? (activeTab?.workspaceRoot ?? state.meta?.workspaceRoot ?? state.meta?.cwd ?? "")
|
||||
: "";
|
||||
|
||||
const workbenchChromeHidden = true;
|
||||
@@ -322,7 +316,7 @@ export function AppRuntimeView(props: AppRuntimeViewProps) {
|
||||
/>}
|
||||
</TopicbarRegion>
|
||||
|
||||
<section className={`chat-pane${session.transcript.emptyHero || (draftActive && !draftNeedsAttention) ? " chat-pane--creation-empty" : ""}`}>
|
||||
<section className={`chat-pane${creationHeroVisible(draft.surface, session.transcript.emptyHero) ? " chat-pane--creation-empty" : ""}`}>
|
||||
{!draft.surface && <SessionStatusBanners {...buildSessionStatusBannerProps({
|
||||
t,
|
||||
activeTab,
|
||||
@@ -458,9 +452,7 @@ export function AppRuntimeView(props: AppRuntimeViewProps) {
|
||||
scope: workspaceContextProject ? "project" : "global",
|
||||
workspaceRoot: workspaceContextRoot,
|
||||
workspaceName: workspaceContextProject
|
||||
? draftWorkspace
|
||||
? workspaceContextRoot.replace(/[\\/]+$/, "").split(/[\\/]/).filter(Boolean).pop()
|
||||
: activeTab?.workspaceName ?? state.meta?.workspaceName
|
||||
? activeTab?.workspaceName ?? state.meta?.workspaceName
|
||||
: undefined,
|
||||
gitBranch: workspaceContextProject && !activeTab?.remote ? state.meta?.gitBranch : undefined,
|
||||
tabId: activeTabId,
|
||||
|
||||
@@ -11,7 +11,8 @@ import type { ForkBlockReason } from "../lib/forkTargets";
|
||||
import type { SessionAvailability } from "../lib/sessionAvailability";
|
||||
import { orderedLocalSubmissions } from "../lib/localSubmissionState";
|
||||
import { RotateCcw } from "lucide-react";
|
||||
import { draftSurfaceNeedsAttention, type SessionDraftSurface } from "../app-runtime/useSessionDraftSurface";
|
||||
import type { SessionDraftSurface } from "../app-runtime/useSessionDraftSurface";
|
||||
import { draftSurfaceNeedsAttention } from "./draftPresentation";
|
||||
|
||||
const RemoteSessionSurface = lazy(() => import("../components/RemoteSessionSurface").then((module) => ({ default: module.RemoteSessionSurface })));
|
||||
const SidebarImConnectionDetail = lazy(() => import("./SidebarImConnectionDetail").then((module) => ({ default: module.SidebarImConnectionDetail })));
|
||||
|
||||
@@ -15,7 +15,8 @@ import type { useComposerModeActions } from "../lib/useComposerModeActions";
|
||||
import type { useComposerGoalCommands } from "../app-runtime/useComposerGoalCommands";
|
||||
import type { useRemoteComposerRuntimeActions } from "../lib/useRemoteComposerIntegration";
|
||||
import type { useControllerProfileCommands } from "../lib/useControllerProfileCommands";
|
||||
import { draftSubmissionLocksEditing, draftSurfaceNeedsAttention, type useSessionDraftSurface } from "../app-runtime/useSessionDraftSurface";
|
||||
import { draftSubmissionLocksEditing, type useSessionDraftSurface } from "../app-runtime/useSessionDraftSurface";
|
||||
import { draftSurfaceNeedsAttention } from "./draftPresentation";
|
||||
import type {
|
||||
ApprovalProps,
|
||||
AskProps,
|
||||
@@ -414,6 +415,13 @@ export function buildComposerSurface(input: ComposerSurfaceInput): DecisionFoote
|
||||
...surface.props.workspaceContext,
|
||||
scope: draft.draft.scope === "project" ? "project" : "global",
|
||||
workspaceRoot: draft.draft.workspaceRoot,
|
||||
workspaceName: draft.draft.scope === "project"
|
||||
? draft.draft.workspaceRoot?.replace(/[\\/]+$/, "").split(/[\\/]/).filter(Boolean).pop()
|
||||
: undefined,
|
||||
// Drafts have no formal tab. Git RPCs require that tab's identity and
|
||||
// must never target the session that happened to be open beforehand.
|
||||
tabId: undefined,
|
||||
gitBranch: undefined,
|
||||
scopeKey: `draft:${draft.draft.workspaceId}`,
|
||||
remote: false,
|
||||
} : undefined,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { SessionDraftSurface } from "../app-runtime/useSessionDraftSurface";
|
||||
|
||||
const DRAFT_ATTENTION_PHASES = new Set([
|
||||
"accepted",
|
||||
"dispatch_unknown",
|
||||
"dispatching_shell",
|
||||
"resume_required",
|
||||
"runtime_failed",
|
||||
"terminal_failed",
|
||||
]);
|
||||
|
||||
/** Healthy drafts use the new-session landing; recovery controls need room. */
|
||||
export function draftSurfaceNeedsAttention(draft: SessionDraftSurface): boolean {
|
||||
return draft.saveState === "conflict" || draft.saveState === "error" || Boolean(draft.taskError)
|
||||
|| Boolean(draft.operation && DRAFT_ATTENTION_PHASES.has(draft.operation.phase));
|
||||
}
|
||||
|
||||
export function creationHeroVisible(draft: SessionDraftSurface | null | undefined, emptyHero: boolean): boolean {
|
||||
// A hidden formal tab must not collapse the active draft's recovery surface.
|
||||
return draft ? !draftSurfaceNeedsAttention(draft) : emptyHero;
|
||||
}
|
||||
Reference in new issue
Block a user