refactor: dedupe the jscpd clones; drop the baseline loading gate

- Extract the shared New Session action into WorkspacesService.startSession
  (sidebar button and workspace browser both delegate; recent-Workspace
  targeting and the no-workspace clear live in one place).
- Fold the chip-insertion transaction shared by insert-ref and paste-upgrade
  into one InputMachine helper.
- Share the fixture's session-not-found guard across the sessionId-addressed
  catalog routes.
- Drop the AppFrame baselines-ready loading gate (user ruling: the bare
  status line reads worse than the shell's own pending rendering); both
  column occupants mount from first paint.
This commit is contained in:
imccyu
2026-07-27 08:51:05 +08:00
parent 084e64744a
commit dd2d9ca50a
9 changed files with 75 additions and 103 deletions

View File

@@ -85,12 +85,7 @@ export function AppFrame({
useStore,
actions,
renderSlot,
useWorkspaces,
}: AppFrameProps) {
// Baseline gate: before both object-layer baselines land, empty snapshots
// are indistinguishable from a genuine no-session state — rendering the
// conversation shell then would flash the New Workspace hero on boot.
const baselinesReady = useWorkspaces(s => s.baselinesReady)
const panels = useStore((s) => s)
const frameRef = useRef<HTMLDivElement | null>(null)
const [viewport, setViewport] = useState(() => window.innerWidth)
@@ -156,24 +151,15 @@ export function AppFrame({
width: cols.sidebar,
})}
</div>
{baselinesReady
? (
<>
{/* Both column occupants stay at fixed tree positions. The
conversation is session-maybe; the strict details entry
naturally renders empty while no session is current. */}
<CenterColumn>{renderSlot('conversation', {})}</CenterColumn>
<DetailsColumn>{renderSlot('details', {})}</DetailsColumn>
</>
)
: (
<>
<CenterColumn>
<div role="status">Loading workspaces and sessions</div>
</CenterColumn>
<DetailsColumn />
</>
)}
<>
{/* Both column occupants stay at fixed tree positions from first
paint — no loading gate (user ruling: the bare status line looked
worse than the shell's own pending rendering). The conversation
is session-maybe; the strict details entry naturally renders
empty while no session is current. */}
<CenterColumn>{renderSlot('conversation', {})}</CenterColumn>
<DetailsColumn>{renderSlot('details', {})}</DetailsColumn>
</>
{/* The collapsed rail is fixed-width: no resize handle while closed. */}
{panels.sidebar > 0 && <DragHandle side="sidebar" left={cols.sidebar} onStart={onSidebarStart} onDrag={onSidebarDrag} onEnd={onDragEnd} />}
{cols.details > 0 && <DragHandle side="details" left={viewport - cols.details} onStart={onDetailsStart} onDrag={onDetailsDrag} onEnd={onDragEnd} />}

View File

@@ -160,11 +160,13 @@ describe('AppFrame', () => {
expect(slotCalls.map((c) => c.key)).toContain('conversation')
})
it('keeps the loading branch until both object-layer baselines are ready', () => {
it('renders both column occupants before baselines settle (no loading gate)', () => {
// User ruling: the bare loading status looked worse than the shell's own
// pending rendering — both occupants mount from first paint.
baselinesReady.current = false
const { slotCalls, getByRole } = mountFrame()
expect(getByRole('status').textContent).toContain('Loading workspaces and sessions')
expect(slotCalls.map((c) => c.key)).not.toContain('conversation')
const { slotCalls } = mountFrame()
expect(slotCalls.map((c) => c.key)).toContain('conversation')
expect(slotCalls.map((c) => c.key)).toContain('details')
})
it('sidebar slot receives live concession output as owner props', () => {