mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
bc5b6f0588
A design-generation run that ended its turn with TodoWrite tasks still pending / in_progress / stopped — or was truncated by max_tokens — was stamped `succeeded` and read "Completed" on the Pet task center and the project pill, while the chat footer simultaneously read "Stopped with unfinished work". Users trusted "Completed", assumed the work was done, and re-requested, creating duplicate generations and eroding trust in the run-state UI (#1247 / #1060). Root cause: the daemon equates "the turn/process ended" with "succeeded" and never consults the agent's own declared work; multiple web surfaces then map `succeeded` -> "Completed" without any completeness signal, even though the chat footer already distinguishes unfinished work. Fix — one canonical signal, consumed everywhere so the surfaces cannot drift: - packages/contracts/api/run-completeness.ts defines the canonical "unfinished declared work" predicate (status !== 'completed', so pending / in_progress / stopped all count), matching the web footer's unfinishedTodosFromEvents exactly. - Daemon folds the last TodoWrite snapshot + a max_tokens truncation off the agent event stream (every runtime, via the single emitAgentEvent choke point) and derives run.endedWithUnfinishedWork once at finish(). - The project-status projection derives `incomplete` from the already persisted events_json — no schema migration — so it survives reload/reattach instead of flashing "Completed" once the in-memory run ages out. - Contracts carry endedWithUnfinishedWork on ChatRunStatusResponse and the SSE end payload (backward compatible: absent = finished), plus a new `incomplete` ProjectDisplayStatus. - Web surfaces (Pet task center, project pill, DesignsTab) render `incomplete` as amber needs-attention, never the success color; the footer now sources the same shared predicate. Truncation uses the display-only option (spec Option A): the run stays `succeeded` but is flagged incomplete, avoiding the code===0 / stdin-close hazards of reclassifying a truncated turn to failed. Regression coverage, red on main / green here: an HTTP-boundary e2e (e2e/tests/dialog/task-completeness.test.ts) drives the interactive chat path for unfinished / stopped / truncated / finished runs; daemon + contracts + web unit tests pin the projection and the canonical predicate. Ref: https://plane.powerformer.net/open-design/projects/49832a02-3158-4faf-bf2f-d0e39c40c7e6/issues/011f6f51-7a25-4043-ae94-182e0b8b0931 Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
26 lines
912 B
TypeScript
26 lines
912 B
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { STATUS_LABEL_KEYS, STATUS_ORDER } from '../../src/components/DesignsTab';
|
|
|
|
describe('DesignsTab status metadata', () => {
|
|
it('places awaiting_input and incomplete before succeeded', () => {
|
|
// `incomplete` is a terminal-but-needs-attention state (a succeeded run that
|
|
// ended with unfinished declared work, #1247 / #1060). It sorts alongside
|
|
// awaiting_input, ahead of the plain `succeeded` "Completed" pill.
|
|
expect(STATUS_ORDER).toEqual([
|
|
'not_started',
|
|
'running',
|
|
'awaiting_input',
|
|
'incomplete',
|
|
'succeeded',
|
|
'failed',
|
|
'canceled',
|
|
]);
|
|
});
|
|
|
|
it('maps awaiting_input and incomplete to their i18n label keys', () => {
|
|
expect(STATUS_LABEL_KEYS.awaiting_input).toBe('designs.status.awaitingInput');
|
|
expect(STATUS_LABEL_KEYS.incomplete).toBe('designs.status.incomplete');
|
|
});
|
|
});
|