fix: review-bot findings on the provider-hosted shell
- Keep ConversationSession mounted for blank sessions (chrome-less) so the draft-persistence mirror stays bound in the hero; hero typing reaches the chat store again. - Restore the baselines-ready gate in AppFrame: empty boot snapshots no longer flash the New Workspace hero before either baseline lands. - Commit ordinary sends through the machine (send-committed event + Shell.commitSend): undo can no longer resurrect already-sent content on the default-sink path. - Give the production InputMachine a real wall clock so the typing-run merge window actually expires. - Coalesce concurrent connectWorkspace creates per workspace: the summary has no cwd until the host frame lands, so a second New Session inside that window minted a duplicate hidden blank session.
This commit is contained in:
@@ -85,7 +85,12 @@ 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)
|
||||
@@ -151,13 +156,24 @@ export function AppFrame({
|
||||
width: cols.sidebar,
|
||||
})}
|
||||
</div>
|
||||
<>
|
||||
{/* 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>
|
||||
</>
|
||||
{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 />
|
||||
</>
|
||||
)}
|
||||
{/* 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} />}
|
||||
|
||||
@@ -160,13 +160,11 @@ describe('AppFrame', () => {
|
||||
expect(slotCalls.map((c) => c.key)).toContain('conversation')
|
||||
})
|
||||
|
||||
it('renders both column occupants before baselines settle (no loading gate)', () => {
|
||||
// The loading branch is gone: fixed tree positions from first paint, the
|
||||
// occupants render their own pending states.
|
||||
it('keeps the loading branch until both object-layer baselines are ready', () => {
|
||||
baselinesReady.current = false
|
||||
const { slotCalls } = mountFrame()
|
||||
expect(slotCalls.map((c) => c.key)).toContain('conversation')
|
||||
expect(slotCalls.map((c) => c.key)).toContain('details')
|
||||
const { slotCalls, getByRole } = mountFrame()
|
||||
expect(getByRole('status').textContent).toContain('Loading workspaces and sessions')
|
||||
expect(slotCalls.map((c) => c.key)).not.toContain('conversation')
|
||||
})
|
||||
|
||||
it('sidebar slot receives live concession output as owner props', () => {
|
||||
|
||||
Reference in New Issue
Block a user