mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
ab0a1a4e61
Reworks the entry experience end to end — the Home hero and its composer, the
nav rail, the workspace tab bar, the recent-projects strip, the community
gallery and the message center — and adds the per-project run status the rail
and the strip now render.
Home / entry
- Hero headline cycles a noun out of `homeHero.titleWords` (RotatingTitleWord);
locales whose `homeHero.title` carries no `{word}` stay a plain sentence.
- The composer is now one component in two shapes: `page` (Home's full column)
and `dock` (the bar alone, used by the community view), sharing @mentions,
staged files, the working-directory row and submit.
- ComposerModePicker is gone; ComposerPlusMenu absorbs what it offered.
- Entry layout, nav rail (with RailRecentRow), workspace tabs and the recent
projects strip are restyled together against one set of measurements.
Run status
- New `projectRunStatus` state, `useProjectRunStatuses`, `run-progress` and
`latest-user-prompt` runtime helpers, and `ProjectRunStatusIcon`.
- `ChatRunsResponse` gains `awaitingInputProjectIds`: a run that asked a
question reads `succeeded` while the project is still blocked, so a caller
rendering per-project status needs the set to avoid showing such a project
as finished. Optional — absent means unknown, not none.
Community / message center / design files
- Community gallery gets its own empty state, and the message center gets
archive plus a reworked card.
- Design files panel gets a dedicated empty state component.
Daemon / desktop / sidecar
- Project create bounds its read-only preparation work (15s) so a stalled
local catalogue scan cannot strand the web on an optimistic route or leave a
half-initialized project behind.
- The startup splash runs the same pixel-scan wordmark as the Home hero,
ported to raw WebGL because the splash renders before the sidecar boots
(replaces the splash video).
- Sidecar proxy tracks the in-flight request/response so a keepalive replay
tears down the previous attempt instead of leaking it.
New visuals: SiriOrb, MarqueeLabel, SpaceBackground, the composer beam and the
chat-cards-next surface.
No placeholder data ships with this: the community card byline (author name and
view/remix counts) and the message center's temporary cover image are removed,
so the card footer shows the title only and the media slot renders only when a
message payload actually carries `imageUrl`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016cbrF36bYyw9qpnksvZTjd
108 lines
3.8 KiB
TypeScript
108 lines
3.8 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import type { ChatRunStatusResponse } from '@open-design/contracts';
|
|
|
|
import { foldRunsToProjectStatuses } from '../../src/state/projectRunStatus';
|
|
|
|
function run(over: Partial<ChatRunStatusResponse> & { status: ChatRunStatusResponse['status'] }) {
|
|
return {
|
|
id: `run-${Math.random()}`,
|
|
projectId: 'p1',
|
|
conversationId: null,
|
|
assistantMessageId: null,
|
|
agentId: 'claude',
|
|
createdAt: 0,
|
|
updatedAt: 1,
|
|
...over,
|
|
} as ChatRunStatusResponse;
|
|
}
|
|
|
|
describe('foldRunsToProjectStatuses', () => {
|
|
it('reports each terminal outcome under its own name', () => {
|
|
const statuses = foldRunsToProjectStatuses([
|
|
run({ projectId: 'ok', status: 'succeeded' }),
|
|
run({ projectId: 'bad', status: 'failed' }),
|
|
run({ projectId: 'stopped', status: 'canceled' }),
|
|
]);
|
|
|
|
expect(statuses.get('ok')).toBe('succeeded');
|
|
expect(statuses.get('bad')).toBe('failed');
|
|
expect(statuses.get('stopped')).toBe('canceled');
|
|
});
|
|
|
|
it('keeps queued distinct from running', () => {
|
|
// The daemon folds queued into running for its own consumers; this one
|
|
// paces its indicator differently, so the distinction has to survive.
|
|
const statuses = foldRunsToProjectStatuses([
|
|
run({ projectId: 'waiting', status: 'queued' }),
|
|
run({ projectId: 'working', status: 'running' }),
|
|
]);
|
|
|
|
expect(statuses.get('waiting')).toBe('queued');
|
|
expect(statuses.get('working')).toBe('running');
|
|
});
|
|
|
|
it('calls a succeeded run with unfinished declared work incomplete (#1247 / #1060)', () => {
|
|
const statuses = foldRunsToProjectStatuses([
|
|
run({ status: 'succeeded', endedWithUnfinishedWork: true }),
|
|
]);
|
|
|
|
expect(statuses.get('p1')).toBe('incomplete');
|
|
});
|
|
|
|
it('lets a pending question outrank a succeeded run', () => {
|
|
// The run that asked reports `succeeded` and exits, so without the
|
|
// awaiting-input set a blocked project reads as finished.
|
|
const runs = [run({ status: 'succeeded' })];
|
|
|
|
expect(foldRunsToProjectStatuses(runs).get('p1')).toBe('succeeded');
|
|
expect(foldRunsToProjectStatuses(runs, ['p1']).get('p1')).toBe('awaiting_input');
|
|
});
|
|
|
|
it('does not let a pending question mask a failure', () => {
|
|
// Only `succeeded` is superseded — a failed or canceled run leaves nothing
|
|
// to answer, and hiding the failure behind "needs input" would misdirect.
|
|
expect(foldRunsToProjectStatuses([run({ status: 'failed' })], ['p1']).get('p1')).toBe('failed');
|
|
expect(foldRunsToProjectStatuses([run({ status: 'canceled' })], ['p1']).get('p1')).toBe(
|
|
'canceled',
|
|
);
|
|
});
|
|
|
|
it('lets an in-flight run outrank finished history', () => {
|
|
const statuses = foldRunsToProjectStatuses([
|
|
run({ status: 'succeeded', updatedAt: 99 }),
|
|
run({ status: 'running', updatedAt: 1 }),
|
|
]);
|
|
|
|
expect(statuses.get('p1')).toBe('running');
|
|
});
|
|
|
|
it('picks the newest run within each of active and terminal', () => {
|
|
expect(
|
|
foldRunsToProjectStatuses([
|
|
run({ status: 'succeeded', updatedAt: 1 }),
|
|
run({ status: 'failed', updatedAt: 2 }),
|
|
]).get('p1'),
|
|
).toBe('failed');
|
|
|
|
expect(
|
|
foldRunsToProjectStatuses([
|
|
run({ status: 'queued', updatedAt: 2 }),
|
|
run({ status: 'running', updatedAt: 1 }),
|
|
]).get('p1'),
|
|
).toBe('queued');
|
|
});
|
|
|
|
it('omits projects it was told nothing about', () => {
|
|
// A missing entry means "unknown", which the UI renders as an empty slot.
|
|
// Defaulting to not_started here would assert something unverified.
|
|
const statuses = foldRunsToProjectStatuses([run({ projectId: 'known', status: 'running' })]);
|
|
|
|
expect(statuses.has('unknown')).toBe(false);
|
|
expect(foldRunsToProjectStatuses([]).size).toBe(0);
|
|
});
|
|
|
|
it('ignores runs with no project', () => {
|
|
expect(foldRunsToProjectStatuses([run({ projectId: null, status: 'running' })]).size).toBe(0);
|
|
});
|
|
});
|