diff --git a/docs/design/2026-09-11-req160-empty-turn-row/design.md b/docs/design/2026-09-11-req160-empty-turn-row/design.md
new file mode 100644
index 000000000000..243d24ec8ad3
--- /dev/null
+++ b/docs/design/2026-09-11-req160-empty-turn-row/design.md
@@ -0,0 +1,92 @@
+---
+type: design
+slug: req160-empty-turn-row
+date: 2026-09-11
+status: accepted
+relates:
+ - jinjunnn/alpha-code#1318(本增量是其实现前的形态修正)
+ - jinjunnn/alpha-code#1325(REQ-160 AC3)
+ - jinjunnn/alpha-work#101 AC3
+---
+
+# 模型一个字都没回时,把这件事说在它发生的地方
+
+> **owner 2026-09-11 批准**(呈现形态 + 文案裁决)。帧见同目录 [`frame.html`](frame.html)(常态 / 无重试入口 / 重试失败 / 与既有中断行同屏,浅深两色)。
+> 批准后并入 [`current/conversation-timeline/design.html`](../current/conversation-timeline/design.html)
+> 的 `#struct`,铸锚 `#empty-turn`;台账见
+> [`current/conversation-timeline/components.md`](../current/conversation-timeline/components.md)。
+
+## 1. 与上一稿的关系
+
+**继承**:时间线现行的**中断行**形态(`design ②` 的 `.interrupted` 帧)—— 左对齐安静行、
+12px 描边图标、三级灰正文、点分隔、就地的强调色文字按钮、失败提示就地出现。
+
+**新增**:同族的第二个成员 —— 助手回合结束但一个字都没回时的那一行。
+
+**取代**:`#1318` 票面 §Scope 里的
+「Sticky **top** banner under session header」+ AC1(常驻顶部横幅)+ AC4(按 `messageId` 记住已关闭)。
+
+**为什么**(owner 2026-09-10 提出「这个前端元素的类型好像不对」,以下是查证后的四条):
+
+| # | 顶部横幅的问题 |
+|---|---|
+| 1 | **事件是一个回合的,横幅是整个会话的。** 你滚到别处它还在,它描述的那条消息却已经不在视野里。 |
+| 2 | **票面自己露了馅**:为了一个全局元素,它不得不发明「按 `messageId` 记住已关闭」这种状态。要给全局元素挂逐条记忆,说明它本来就该挂在那一条上。 |
+| 3 | **顶栏这个位置已经有人了。** REQ-159 刚把「沙箱开启」「这个项目只读」放进会话顶栏 —— 那些是**会话/应用级**的持续状态。把「这一条回复是空的」也塞进去,会让这个位置什么都说,于是什么都不说。 |
+| 4 | **这个仓早有这一族的既定形态**:中断行。它的源码注释明写「左对齐安静行,**不是居中告警 pill**」,并且是 fail-closed 的(动作接不上就只剩事实陈述,不给一个点不动的按钮)。同一族的新成员没有理由另起一种。 |
+
+另外:票面 §Scope 写「Likely touch: `packages/app/src/pages/session/timeline/`」—— 那是**上游**的叶子,
+alpha 早在 REQ-125 C5/C6 就自持了时间线,而北极星守卫会拦下对它的改动。这一句也一并作废。
+
+## 2. 动笔前的地面真相(本轮实读)
+
+| 事实 | 坐标 |
+| --- | --- |
+| 中断行的形态与 fail-closed 取舍 | `session-timeline-view.tsx:797-830` `InterruptedRow` |
+| 该族样式:左对齐、三级灰、12px 描边图标、强调色文字按钮、失败就地 | `session-timeline.css:176-230` |
+| 既有文案三条 | `i18n/zh.ts:1368-1371`(`interrupted` / `continueTurn` / `continueFailed`) |
+| 会话顶栏现有常驻元素 | `session-workspace-shell.tsx`:运行状态胶囊 · 沙箱 · 只读(REQ-159) |
+| alpha 自持时间线,上游那棵不再消费 | `renderer/alpha-ui/session-timeline/`;北极星守卫辖区 |
+| 空回合的判据(票面 §Scope,未改) | 完成 + `finish`/`reason` 为 `unknown` + 无非空 text + `tokens.output === 0` |
+
+## 3. 契约(批准后即 AC 字面量锚点)
+
+| 项 | 规则 |
+| --- | --- |
+| 位置 | 时间线内,**紧挨那个空回合**,与中断行同族同层 |
+| 形态 | 左对齐安静行:盾形 12px 描边图标(`--a-warning` 描边)+ 三级灰事实句 + 点分隔 + 强调色文字按钮 |
+| 数量 | **一个元素**,两层文字(事实 + 可能性)。票面原本是「顶部横幅 + 气泡内提示」两个说同一件事 |
+| 动作 | 「修改后再试」聚焦输入框。**fail-closed**:intent 缺席就只剩事实句,不给点不动的按钮(同中断行) |
+| 失败 | 动作失败就地出提示,再点即重试(同中断行) |
+| 关闭 | **没有关闭按钮**。它不是通知,是那一回合的事实;回合还在,这行就在 |
+| 多条 | 每个空回合各有自己那一行。不需要「只显示最新一条」—— 那是全局元素才有的问题 |
+| 判据钩子 | `data-alpha-timeline-row="empty-turn"` |
+
+## 4. 文案(owner 2026-09-11 已裁)
+
+**裁决:把内容安全审核放在明面上,但不断言它就是原因。** 四条判据同时成立时提示;原因以
+**可能性**给出,安全审核排第一位 —— 既满足备案叙事(产品确有内容安全这一环并对用户可见),
+又不在猜错时指责用户。
+
+为什么不能断言:票面自己的 §Context 写着这一形态**没有** error、**没有**任何内容安全枚举落在
+存储里 —— 它是从「跑完了 + 无正文 + 零 token」推断的。真实原因也可能是上游抖动、供应商 bug、
+网络中断。与 REQ-159 的「不许宣称比实际更大的保护面」同源:那次别把保护说大,这次别把原因说死。
+
+因此这一行是**两层**:上层是确知的事实,下层是可能性与出路。仍然是一个元素、一行的重量。
+
+| 槽位 | zh | en |
+| --- | --- | --- |
+| 事实句 | 模型没有返回内容 | The model returned no reply |
+| 次级说明 | 可能未通过内容安全审核,也可能是模型或网络异常 | It may not have passed content safety review, or the model or network may have failed |
+| 动作 | 修改后再试 | Edit and retry |
+| 动作失败 | (沿用既有 `alpha.timeline.continueFailed`) | (同) |
+
+被否决的票面原文:标题直接写「内容未通过安全审核」/「Content blocked by safety review」——
+它把一个推断当判决说给用户听。
+
+## 5. 批准后的落点
+
+1. 帧并入 `current/conversation-timeline/design.html` 的 `#struct`,铸锚 `#empty-turn`;
+2. 台账加一行(组件 / 锚 / 增量稿 / 实现票 `ac#1318` · `ac#1325`);
+3. `#1318` 的 AC1 / AC4 按本稿改写(**AC 改写归 owner**,实现票不自行改);
+4. 本目录就地冻结。
diff --git a/docs/design/2026-09-11-req160-empty-turn-row/frame.html b/docs/design/2026-09-11-req160-empty-turn-row/frame.html
new file mode 100644
index 000000000000..6e1725d6a37f
--- /dev/null
+++ b/docs/design/2026-09-11-req160-empty-turn-row/frame.html
@@ -0,0 +1,121 @@
+
+
+
+
+
+空回合行 · 时间线内同族增量
+
+
+
+
+会话时间线 · 空回合行组件增量 · 与中断行同族
+
+
+
+
+
+
模型一个字都没回时,把这件事说在它发生的地方
+
助手回合跑完了,却没有任何正文。页面上看起来像卡住,用户不知道发生了什么。这一行就挨着那个空回合,说清事实并给出下一步 —— 它不是通知,是那一回合的事实,所以没有关闭按钮。
+
+
+
常态
+
+
帮我把这段话改得更礼貌一些
+
+
+
模型没有返回内容可能未通过内容安全审核,也可能是模型或网络异常
+
+
修改后再试
+
+
+
+
同屏对照:它与既有的中断行是同一族
+
+
继续写下去
+
+
+ 已由你停止
+
+ 继续生成
+
+
换个说法再试一次
+
+
+
模型没有返回内容可能未通过内容安全审核,也可能是模型或网络异常
+
+
修改后再试
+
+
+
+
+
+
接不上输入框时(fail-closed)
+
+
+
模型没有返回内容可能未通过内容安全审核,也可能是模型或网络异常
+
+
+
+
动作失败,就地重试
+
+
+
模型没有返回内容可能未通过内容安全审核,也可能是模型或网络异常
+
+
修改后再试
+
发送失败,请重试
+
+
+
+
+
+ | 项 | 规则 |
+ | 位置 | 时间线内,紧挨那个空回合;与中断行同层同族。 |
+ | 形态 | 左对齐安静行:盾形 12px 描边图标(警示色描边)+ 三级灰事实句 + 点分隔 + 强调色文字按钮。不是横幅,不是居中 pill。 |
+ | 数量 | 一个元素。不再另加气泡内提示 —— 两个元素说同一件事只会分散。 |
+ | 动作 | 「修改后再试」聚焦输入框。接不上就只剩事实句,不给点不动的按钮。 |
+ | 失败 | 就地出提示,再点即重试。 |
+ | 关闭 | 没有关闭按钮。回合还在,这行就在。 |
+ | 多条 | 每个空回合各有自己那一行,不需要「只显示最新一条」。 |
+
+
+
为什么不用顶部横幅(帧外):事件是一个回合的,横幅是整个会话的 —— 你滚到别处它还在,它说的那条却已不在视野里。票面为此不得不发明「按消息记住已关闭」,那正是元素挂错了地方的症状。而会话顶栏现在已经放着沙箱与只读这类**会话级持续状态**,再塞进一条「这一次回复是空的」,会让那个位置什么都说,于是什么都不说。
+
文案:把安全审核放在明面上,但不断言(owner 2026-09-11 裁):四条判据同时成立时提示,原因以**可能性**给出、安全审核排第一位 —— 既满足备案叙事(产品确有内容安全这一环且对用户可见),又不在猜错时指责用户。这一形态没有任何错误码、也没有内容安全枚举落在存储里,是推断出来的;真实原因也可能是上游抖动或网络中断。
+
+
+
+
+
diff --git a/packages/ui-mac/src/renderer/alpha-ui/session-timeline/cards/timeline-intents.ts b/packages/ui-mac/src/renderer/alpha-ui/session-timeline/cards/timeline-intents.ts
index f7d9f7fb1717..629796053886 100644
--- a/packages/ui-mac/src/renderer/alpha-ui/session-timeline/cards/timeline-intents.ts
+++ b/packages/ui-mac/src/renderer/alpha-ui/session-timeline/cards/timeline-intents.ts
@@ -68,6 +68,12 @@ export interface TimelineIntents {
* 通道呈现不了)—— 由中断行就地给出失败提示,不得静默吞掉。
*/
continueTurn?: () => void | Promise
+ /**
+ * 空回合行「修改后再试」(REQ-160 AC3):把焦点交回输入框,让用户就地改提问重发。
+ * 缺席即那一行只剩事实陈述,不出按钮(fail-closed,同中断行 —— 不给一个点不动的按钮)。
+ * 返回 Promise 时,拒绝 = 失败,由该行就地提示,不得静默吞掉。
+ */
+ focusPrompt?: () => void | Promise
}
export const TimelineIntentsContext = createContext({})
diff --git a/packages/ui-mac/src/renderer/alpha-ui/session-timeline/empty-turn.test.ts b/packages/ui-mac/src/renderer/alpha-ui/session-timeline/empty-turn.test.ts
new file mode 100644
index 000000000000..4d8e012c2ee7
--- /dev/null
+++ b/packages/ui-mac/src/renderer/alpha-ui/session-timeline/empty-turn.test.ts
@@ -0,0 +1,54 @@
+// REQ-160 AC3(`#1318` / `#1325`)—— 空回合判据。
+//
+// 四条**同时**成立才算。这张票最容易做假的地方就是「少一条也触发」——那会把正常回复标成异常。
+// 所以每一条都有一个**只差它一条**的反例。
+import { describe, expect, test } from "bun:test"
+import { isEmptyUnknownTurn } from "./timeline-model"
+
+type A = Parameters[0]
+const turn = (over: Record = {}): A =>
+ ({ id: "msg_1", role: "assistant", finish: "unknown", tokens: { input: 0, output: 0, reasoning: 0 }, ...over }) as unknown as A
+
+describe("REQ-160 AC3 空回合判据:四条同时成立才算", () => {
+ test("正样本:回合结束 + finish=unknown + 零可见正文 + tokens.output=0", () => {
+ expect(isEmptyUnknownTurn(turn(), 0)).toBe(true)
+ })
+
+ test("反例①:有可见正文 ⇒ 不算(票面 Non-goals:finish=unknown 但有正文的不是这一类)", () => {
+ expect(isEmptyUnknownTurn(turn(), 1)).toBe(false)
+ })
+
+ test("反例②:finish 不是 unknown ⇒ 不算(stop + 明确拒答文本是另一回事)", () => {
+ for (const finish of ["stop", "length", "tool-calls", undefined]) {
+ expect(isEmptyUnknownTurn(turn({ finish }), 0)).toBe(false)
+ }
+ })
+
+ test("反例③:tokens.output 非 0 ⇒ 不算(模型确实产出了,只是没落成可见正文)", () => {
+ expect(isEmptyUnknownTurn(turn({ tokens: { input: 12, output: 7, reasoning: 0 } }), 0)).toBe(false)
+ })
+
+ test("反例④:被用户中止 ⇒ 不算,那是中断行的辖区(两行不得同时出现)", () => {
+ expect(isEmptyUnknownTurn(turn({ error: { name: "MessageAbortedError" } }), 0)).toBe(false)
+ })
+
+ test("反例⑤:tokens 缺席或形状不对 ⇒ 不算(拿不准就不标,不猜)", () => {
+ for (const tokens of [undefined, null, 0, "0", {}]) {
+ expect(isEmptyUnknownTurn(turn({ tokens }), 0)).toBe(false)
+ }
+ })
+
+ test("控制臂:恒答 true 的替身会把上面每一个反例都标成空回合", () => {
+ const alwaysTrue = () => true
+ const counterExamples: [A, number][] = [
+ [turn(), 1],
+ [turn({ finish: "stop" }), 0],
+ [turn({ tokens: { input: 0, output: 7, reasoning: 0 } }), 0],
+ [turn({ error: { name: "MessageAbortedError" } }), 0],
+ ]
+ for (const [a, emitted] of counterExamples) {
+ expect(isEmptyUnknownTurn(a, emitted)).toBe(false)
+ expect(alwaysTrue()).toBe(true) // 替身放行 —— 这正是上面那些断言要抓的形态
+ }
+ })
+})
diff --git a/packages/ui-mac/src/renderer/alpha-ui/session-timeline/session-timeline-view.tsx b/packages/ui-mac/src/renderer/alpha-ui/session-timeline/session-timeline-view.tsx
index 163df572f426..3f2af34e8c0c 100644
--- a/packages/ui-mac/src/renderer/alpha-ui/session-timeline/session-timeline-view.tsx
+++ b/packages/ui-mac/src/renderer/alpha-ui/session-timeline/session-timeline-view.tsx
@@ -749,6 +749,7 @@ function DividerRow(props: { row: Extract }) {
// label 随行身份定格;compaction 的 rev 在完成态 summary 到达时变化,两种形态互不重建。
const row = props.row
if (row.label === "interrupted") return
+ if (row.label === "emptyTurn") return
const [open, setOpen] = createSignal(false)
const expandable = () => row.summaryParts.length > 0
return (
@@ -794,6 +795,57 @@ function DividerRow(props: { row: Extract }) {
)
}
+// REQ-160 AC3(`#1318` / `#1325`,设计稿 2026-09-11-req160-empty-turn-row)——
+// 助手回合跑完了却一个字都没回。**与中断行同族**:左对齐安静行,不是顶部横幅、不是居中 pill。
+//
+// 票面原本要的是「会话头下的常驻顶部横幅 + 气泡内提示」两个元素,owner 2026-09-11 裁掉:
+// 事件是**一个回合**的,横幅是整个会话的 —— 票面为此不得不发明「按 messageId 记住已关闭」,
+// 那正是元素挂错地方的症状;而会话顶栏已被 REQ-159 的沙箱 / 只读这类**会话级持续状态**占着。
+//
+// 两层文字:上层是确知的事实,下层把内容安全审核放在**明面上但不断言**(owner 同日裁决)——
+// 这一形态没有 error、也没有任何内容安全枚举落在存储里,断言「你没通过安全审核」在猜错时
+// 是在指责用户。与 REQ-159「不许宣称比实际更大的保护面」同源。
+//
+// 没有关闭按钮:它不是通知,是那一回合的事实,回合还在这行就在。
+// 动作 fail-closed:intent 缺席就只剩事实,不给一个点不动的按钮(同中断行)。
+function EmptyTurnRow() {
+ const intents = useTimelineIntents()
+ const [sendFailed, setSendFailed] = createSignal(false)
+ return (
+
+
+
+ {t("alpha.timeline.emptyTurn")}
+ {t("alpha.timeline.emptyTurnWhy")}
+
+
+ {(handler) => (
+ <>
+
+
+
+
+ {t("alpha.timeline.continueFailed")}
+
+
+ >
+ )}
+
+
+ )
+}
+
// 中断态(design ② .interrupted 帧):左对齐安静行,不是居中告警 pill。
// 「继续生成」经 continueTurn intent 接绑定层的现有会话发送入口;intent 缺席即只剩事实陈述
// (fail-closed,不给一个点不动的按钮)。发送失败(admission 前被拒/网络断开)不产生任何
diff --git a/packages/ui-mac/src/renderer/alpha-ui/session-timeline/session-timeline.css b/packages/ui-mac/src/renderer/alpha-ui/session-timeline/session-timeline.css
index c00ed2e80cb6..70b3b170f617 100644
--- a/packages/ui-mac/src/renderer/alpha-ui/session-timeline/session-timeline.css
+++ b/packages/ui-mac/src/renderer/alpha-ui/session-timeline/session-timeline.css
@@ -193,6 +193,48 @@
stroke-linejoin: round;
}
+/* REQ-160 AC3:空回合行 —— 复用中断行骨架(.a-tl-interrupted),只多两层文字与盾形图标。
+ 顶对齐:次级说明换行时图标与按钮仍跟首行基线走。 */
+.a-tl-empty {
+ align-items: flex-start;
+}
+
+.a-tl-empty-shield {
+ flex: none;
+ width: 12px;
+ height: 12px;
+ margin-top: 1px;
+ fill: none;
+ stroke: var(--a-warning);
+ stroke-width: 1.7;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+
+.a-tl-empty-text {
+ display: flex;
+ min-width: 0;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.a-tl-empty-fact {
+ color: var(--a-text-secondary);
+}
+
+/* 原因是**可能性**不是判决,所以比事实句轻一档 —— 视觉上也不许它读起来像结论。 */
+.a-tl-empty-why {
+ color: var(--a-text-tertiary);
+ font-size: var(--a-text-2xs);
+ line-height: 1.5;
+}
+
+.a-tl-empty .a-tl-int-dot,
+.a-tl-empty .a-tl-int-continue,
+.a-tl-empty .a-tl-int-failed {
+ margin-top: 1px;
+}
+
.a-tl-int-dot {
width: 3px;
height: 3px;
diff --git a/packages/ui-mac/src/renderer/alpha-ui/session-timeline/timeline-model.ts b/packages/ui-mac/src/renderer/alpha-ui/session-timeline/timeline-model.ts
index dc9ed1cdde6d..c5e9c6638e53 100644
--- a/packages/ui-mac/src/renderer/alpha-ui/session-timeline/timeline-model.ts
+++ b/packages/ui-mac/src/renderer/alpha-ui/session-timeline/timeline-model.ts
@@ -201,6 +201,7 @@ export type TimelineRow =
summaryParts: TextPart[]
}
| { kind: "divider"; key: string; rev: string; userMessageID: string; label: "interrupted" }
+ | { kind: "divider"; key: string; rev: string; userMessageID: string; label: "emptyTurn" }
| { kind: "thinking"; key: string; rev: string; userMessageID: string }
| {
kind: "footnote"
@@ -488,6 +489,30 @@ function cappedField(value: unknown, max = FOOTNOTE_FIELD_MAX_CHARS): string | u
* 无错误,否则无脚注;不回溯早先的完成助手(成功→流式、成功→失败序列一律零脚注,
* 旧指标不得与未终结/失败内容混用;复制动作随行同门)。字段独立诚实缺席(I2)。
*/
+/**
+ * REQ-160 AC3(`#1318` / `#1325`)—— 「助手回合跑完了,却一个字都没回」。
+ *
+ * 四条**同时**成立才算(票面 §Scope,owner 2026-09-11 复述确认):
+ * ① 回合已结束(有 `finish`,且不是被用户中止 —— 那是中断行的辖区);
+ * ② `finish` 为 `unknown`;
+ * ③ 这一回合没有渲染出任何可见正文(投影层的 `emitted === 0`);
+ * ④ `tokens.output === 0`。
+ *
+ * **少一条都不算。** 尤其 ③:`finish=unknown` 但有正文的回合**不是**这一类(票面 Non-goals
+ * 明写),把它算进来会把正常回复标成异常。
+ *
+ * 这是**启发式**:存储里没有 error、也没有任何内容安全枚举(票面 §Context)。所以呈现层只
+ * 陈述确知的事实,原因以可能性给出 —— 见设计稿 2026-09-11-req160-empty-turn-row §4。
+ */
+export function isEmptyUnknownTurn(assistant: AssistantMessage, emitted: number): boolean {
+ if (emitted > 0) return false
+ if (assistant.error) return false // 被中止 / 真错误各有自己的行
+ const finish = (assistant as { finish?: unknown }).finish
+ if (finish !== "unknown") return false
+ const tokens = (assistant as { tokens?: { output?: unknown } }).tokens
+ return typeof tokens === "object" && tokens !== null && tokens.output === 0
+}
+
export function footnoteOf(assistants: readonly AssistantMessage[]): TimelineFootnote | undefined {
const source = assistants.at(-1)
if (!source || typeof source.time.completed !== "number" || source.error) return undefined
@@ -870,6 +895,14 @@ export function projectTimelineRows(input: TimelineProjectionInput): TimelineRow
userMessageID: userMessage.id,
label: "interrupted",
})
+ else if (isEmptyUnknownTurn(assistant, emitted))
+ rows.push({
+ kind: "divider",
+ key: `emptyTurn:${assistant.id}`,
+ rev: "emptyTurn",
+ userMessageID: userMessage.id,
+ label: "emptyTurn",
+ })
}
// 回合末富脚注(A6):只在回合尾态成功完成且有可见内容时出行;当前活跃回合
diff --git a/packages/ui-mac/src/renderer/i18n/en.ts b/packages/ui-mac/src/renderer/i18n/en.ts
index ba2cfab88b27..0cd2a9c28e4d 100644
--- a/packages/ui-mac/src/renderer/i18n/en.ts
+++ b/packages/ui-mac/src/renderer/i18n/en.ts
@@ -1398,6 +1398,11 @@ export const dict = {
"alpha.timeline.interrupted": "Stopped by you",
"alpha.timeline.continueTurn": "Continue",
"alpha.timeline.continuePrompt": "Continue",
+ // REQ-160 AC3 (`#1318`): the turn finished with no reply. Content safety review is named as a
+ // possibility, never asserted — there is no error and no safety enum in storage.
+ "alpha.timeline.emptyTurn": "The model returned no reply",
+ "alpha.timeline.emptyTurnWhy": "It may not have passed content safety review, or the model or network may have failed",
+ "alpha.timeline.emptyTurnRetry": "Edit and retry",
"alpha.timeline.continueFailed": "Couldn't send. Try again.",
"alpha.timeline.loadOlder": "Load earlier messages",
"alpha.timeline.loadingOlder": "Loading earlier messages…",
diff --git a/packages/ui-mac/src/renderer/i18n/zh.ts b/packages/ui-mac/src/renderer/i18n/zh.ts
index a0b788990e82..00fe4b99bf69 100644
--- a/packages/ui-mac/src/renderer/i18n/zh.ts
+++ b/packages/ui-mac/src/renderer/i18n/zh.ts
@@ -1368,6 +1368,11 @@ export const dict = {
"alpha.timeline.interrupted": "已由你停止",
"alpha.timeline.continueTurn": "继续生成",
"alpha.timeline.continuePrompt": "继续",
+ // REQ-160 AC3(`#1318`):助手回合跑完却零正文。**把内容安全审核放在明面上但不断言**
+ //(owner 2026-09-11 裁决)—— 存储里没有 error、也没有内容安全枚举,原因只能给可能性。
+ "alpha.timeline.emptyTurn": "模型没有返回内容",
+ "alpha.timeline.emptyTurnWhy": "可能未通过内容安全审核,也可能是模型或网络异常",
+ "alpha.timeline.emptyTurnRetry": "修改后再试",
"alpha.timeline.continueFailed": "发送失败,请重试",
"alpha.timeline.loadOlder": "加载更早的消息",
"alpha.timeline.loadingOlder": "正在加载更早的消息…",
diff --git a/scripts/gate-files.tsv b/scripts/gate-files.tsv
index 9949d1eb47cf..e92918e3f4f3 100644
--- a/scripts/gate-files.tsv
+++ b/scripts/gate-files.tsv
@@ -279,3 +279,4 @@
4 packages/ui-mac src/main/network-egress-disclosure.test.ts - REQ-137/#1337 AC5 覆盖面如实声明:终端「沙箱开启」悬停卡 zh / en 与勘破文档锚点段落三处都要逐字含「只罩引擎那棵树 / 已登记地址 / 其它地址被拒 / 应用自身联网(模型目录、登录、更新)不在其内」的独立字面量,且不许出现「全部 / 所有出网都被限制」一类过度声明;反向 ①把声明改成全部出网受限 ⇒ 同一判据红并点名过度声明,反向 ②只删掉排除句 ⇒ 红并点名缺哪一句,锚点整段删除 ⇒ 红。全平台纯逻辑
14 packages/ui-mac src/main/workspace-write-probe.test.ts packages/ui-mac/src/main/workspace-write-probe-fence.test.ts REQ-159/#1322 AC2/AC3 工作区写探针的全平台一半:main↔sidecar 合同形状 fail-closed(命令/应答不认形就当没有)、errno 分类只认 EPERM 为围栏的拒绝(EACCES/ENOENT/ENOTDIR 都 unknown,不替围栏背书)、真文件系统上写-删不留残且 wx 不覆盖用户文件、main 侧请求簿(超时 / 子进程退出 / 无关消息 / 对不上号 / postMessage 抛 都答 unknown,不猜);AC3 判据(workspace-write-probe-judge.ts)先自证 —— 恒答「不可写」的替身、恒答「可写」的替身各被拒在一边。真围栏臂委派给 fence 那份。下界=实际条数 14,不留余量
2 packages/ui-mac src/main/workspace-write-probe-fence.test.ts - REQ-159/#1322 AC3 真探针在真围栏下:真 .node(生产构建脚本现编)+ 真 seatbelt + 真子进程(bun)。围栏臂(= sidecar 形态):集合内 writable、集合外 denied(EPERM)、不存在的目录 unknown、两边不留探针文件、pid ≠ 测试进程、buildId = 本次编译,判据过;bare 臂(= main 进程形态,不套围栏):集合外也答 writable ⇒ 判据把它当假探针拒掉 —— 「在 main 里 fs.writeFile 探测恒答可写」那一格的实测。[平台:darwin] 非 darwin 上文件自报 skip,由本登记簿按「自报 0 条」验证标注
+7 packages/ui-mac src/renderer/alpha-ui/session-timeline/empty-turn.test.ts - REQ-160 AC3(`#1318`/`#1325`)空回合判据:四条(回合结束 / finish=unknown / 零可见正文 / tokens.output=0)同时成立才算,每条各有一个**只差它一条**的反例(有正文 / stop / output≠0 / 被中止 / tokens 形状不对);控制臂证明恒答 true 的替身会把每个反例都标成空回合。全平台纯逻辑