合并master并保留双侧决策记录

合并 origin/master 的 UI 编辑器、GDD 审批与前端修复。

保留本分支 LLM Router、Direct 过程卡与私有路径相关实现和决策记录。

解决 decision-log 文档冲突,完整保留双方新增决策条目。
This commit is contained in:
2026-09-02 19:31:12 +08:00
39 changed files with 1824 additions and 97 deletions
@@ -410,7 +410,11 @@ export function GddApprovalCard({
const canDecide = Boolean(
pending && state.state === 'ready_for_approval' && !state.recoveryPending,
);
const decisionDisabled = !canDecide || decisionBusy || hydrateBusy;
// `hydrateBusy` only describes an in-flight background read. Once a card is rendered, the
// loaded projection remains the actionable snapshot until it explicitly reports
// `recoveryPending`; otherwise a refresh racing the first render can transiently disable an
// already-open dialog before the user has had a chance to submit it.
const decisionDisabled = !canDecide || decisionBusy;
const submitComment = () => {
// 方案 §18.2`recoveryPending` 期间只允许重试同一 ID,不允许提交决定。触发按钮
@@ -14,6 +14,13 @@ export type UiDesignStateSaveResult =
}
| { status: 'conflict'; current: UiDesignStateSnapshot };
export type UiDesignCodeGenerationResult = {
relativePath: string;
treeExports: string[];
treeCount: number;
nodeCount: number;
};
export type IUiDesignStateStore = {
load(assetId: string): Promise<UiDesignStateSnapshot>;
save(
@@ -21,6 +28,7 @@ export type IUiDesignStateStore = {
expectedRevision: number,
state: State,
): Promise<UiDesignStateSaveResult>;
generateCode(assetId: string): Promise<UiDesignCodeGenerationResult>;
};
export function createTauriUiDesignStateStore(
@@ -44,6 +52,11 @@ export function createTauriUiDesignStateStore(
},
});
},
generateCode(assetId) {
return invoke<UiDesignCodeGenerationResult>('generate_ui_design_code', {
input: { projectPath, expectedProjectId, assetId },
});
},
};
}
@@ -76,6 +89,9 @@ export const uiDesignStateStore: IUiDesignStateStore = {
committedProjectRevision: 0,
};
},
async generateCode() {
throw new Error('当前环境不支持生成代码');
},
};
export const EMPTY_UI_DESIGN_STATE = EMPTY_UI_EDITOR_STATE;
@@ -80,14 +80,22 @@ function fillClipPath(method: FillMethod, amount: number): string | undefined {
const sweep = amount * maxSweep;
const origin = config.origin;
const originAngle =
origin === 'Top' || origin === 'TopLeft' || origin === 'TopRight'
? 0
: origin === 'Right' || origin === 'BottomRight'
? 90
: origin === 'Bottom' || origin === 'BottomLeft'
? 180
: 270;
const start = config.clockwise ? originAngle : originAngle - sweep;
kind === 'Radial90'
? origin === 'TopLeft'
? 0
: origin === 'TopRight'
? 90
: origin === 'BottomRight'
? 180
: 270
: origin === 'Top'
? 0
: origin === 'Right'
? 90
: origin === 'Bottom'
? 180
: 270;
const start = originAngle - (config.clockwise ? 90 : sweep);
// CSS conic gradients provide a deterministic browser preview for radial
// fills. Exact engine parity is intentionally deferred.
return `conic-gradient(from ${start}deg, #000 0deg ${sweep}deg, transparent ${sweep}deg 360deg)`;
@@ -141,6 +149,7 @@ function imageTypeModel(
const size = logicalSize(sprite, multiplier);
if ('Tiled' in imageType) {
// TODO: Tiled.fill_center=false 当前暂不支持,保持浏览器 repeat fallback。
return {
kind: 'background',
src,