feat(ui): rework streaming output — step timeline, deep-thinking disclosure, tool trows, word fade-in (#636 rebased) - #644

Closed
jackwener wants to merge 10 commits into
mainfrom
pr-636-rebased
Closed

feat(ui): rework streaming output — step timeline, deep-thinking disclosure, tool trows, word fade-in (#636 rebased)#644
jackwener wants to merge 10 commits into
mainfrom
pr-636-rebased

Conversation

@jackwener

Copy link
Copy Markdown
Member

收编 #636(作者 @Astro-Han,保留全部原始 commits)+ rebase 到最新 main。

Maintainer review 结论(第一性原理)

高质量 PR,三层核心全部通过:数据层 stepId 可选兼容+双向投影;时间线聚合三类工具排序均有 ledger 论证;stream-fade 只包裹尾部新词、负 delay 防闪、snap 全跳过。治理意识好(动画白名单登记、时长绑 token)。

非阻塞注记:流式期间每帧全文 grapheme 分词——与 react-markdown 每帧全量 parse 同级,未改变复杂度;未来流式增量化时一起收益。

Rebase 冲突

#639(CronJob 预览)在 tool-activity.tsx 相遇:取本 PR 的 ToolCardBody 抽取结构,CronJob 预览挂进共享 body 的结果分发(card/trow 同享)。

验证

build ✓;typecheck 0;ui 46/46;desktop 2248/2248(含本 PR 新增三组契约/单测)。

Astro-Han added 10 commits July 8, 2026 21:57
Add optional ToolCallMessage.stepId, stamped by ToolRuntime from
getCurrentStepId() (same source as ToolStartEvent.stepId), and map it into
refs.stepId on the backfill path so post-restart model replay re-pairs a
tool call with its assistant step. The field ships with its first consumer,
the UI turn timeline (materializeTurns, next commit).
Replace PR1's neutral multi-step concatenation with a TurnTimelineItem
sequence (thinking / text / tools) rebuilt in storage order: each step's
thinking and text precede the tools whose stepId matches that step; legacy
stepless tools keep the tools-then-summary reading before the text; leftover
and live-only tools flush as trailing tool groups; adjacent thinking and tool
groups merge. Aggregate assistant/assistantThinking stay for copy/export/
prompt-rail consumers, with timeline as the rendering source of truth.
ToolActivityItem gains stepId for the pairing. Tests cover interleaved
multi-step, legacy single, pure-tool tail, live-only append, and merges.
…ol trow
Render the turn body from turn.timeline (materialize.ts) instead of the
tools-section + single-answer layout: each step's reasoning, answer, and tools
appear in production order. Commits 2 and 3 of the plan land together because
both restructure TurnView and can't be judged apart.
- DeepThinking: one controlled Collapsible (collapsed by default; no
defaultOpen) replacing ReasoningPanel and the .maka-turn-thinking <details>.
Live: shimmering '深度思考' title (new TextShimmer primitive + governance
keyframe maka-text-shimmer) and smooth plain-text body that follows the tail;
settled: Markdown + '复制思考过程'. '已截断' pill preserved.
- ToolTrow: a contiguous tool run as one flat, borderless disclosure —
single-tool groups render the tool's own row (no double nesting); multi-tool
groups add a summary line (shimmering active-tool description while running,
bucketed Chinese counts once settled via the pure summarizeTrowTools) that
expands to flat-stacked tool rows. waiting_permission auto-expands the group.
ToolActivityCard body extracted as ToolCardBody, shared by card and trow.
- Delete reasoning-panel.css + import, the .maka-turn-thinking / .maka-turn-tools
token blocks; update chat-marker + 406-motion governance contracts.
Full desktop suite (2224) + root typecheck green; trow-summary unit-tested.
Retire the ▎ blink caret (.maka-bubble-streaming::after + @Keyframes
maka-cursor) and its trailing-<p> display:inline hack in favor of a
per-word fade-in over the freshly revealed streaming tail.
New pure primitive stream-fade.ts: an append-record ring tracks each
growth of the displayed grapheme prefix as a batch, prunes batches older
than the fade window, and caps retained batches. From it we read a stable
boundary offset (older text never re-animates) and the age of any offset
(drives a negative CSS animation-delay so the entrance resumes mid-flight
across ~60Hz re-renders instead of re-flashing). A shared word/char
tokenizer groups whitespace and Latin runs while splitting CJK/emoji
per-grapheme. streamFadeRehypePlugin wraps post-boundary tokens in
.maka-stream-fade spans after rehype-highlight, advancing the cursor
through code fences but never wrapping inside them.
useStreamFade feeds both the answer bubble (via markdown-body's rehype
pass) and DeepThinking's plain-text live body. Under snap (reduced-motion
/ visual-smoke) the hook returns undefined and callers skip wrapping, so
deterministic captures show final text at full opacity.
@Keyframes maka-stream-fade-in animates opacity only; the fade window is
--duration-large (280ms) and FADE_MS is pinned to the same value so the
JS and CSS windows agree. Contracts updated: 406 governance functional
motion set, markdown-prose retired-hack asserts. Unit tests cover the
ring (window slide, order-independent ages, cap), the tokenizer, and the
rehype offset bookkeeping (multi-block prose, code fences unwrapped).
Finish the streaming UI rework wiring so the in-flight (live) path and
the review fixtures exercise the new per-step turn timeline.
- app-shell-session-events: the tool_start handler now carries the
event's stepId onto the live ToolActivityItem, so an in-flight tool is
ordered after its own step's thinking/text in the turn timeline instead
of being lumped into one trailing group. Optional — legacy / degraded
history emits no stepId and keeps the old grouping.
- chat-surface stories: add a MultiStepReasoning story whose fixture is a
single turn with two think->say->call steps (per-step thinking + text +
stepId-tagged tools), so Storybook covers the 深度思考 → answer → tool
trow per-step order.
- visual-smoke turn-narrative seed: append a second, multi-step turn to
the committed narrative so the auto-capture baseline locks the new
per-step timeline alongside the existing legacy stepless turn.
Verified: @maka/ui build + test, desktop build:main + full node:test
suite (2237), @maka/runtime test, and root typecheck (incl. renderer +
storybook projects) all green.
projectFunctionCall dropped event.refs.stepId, so sessions rebuilt from
the runtime event log (the session view's source of truth) lost the
tool-to-step pairing after reload and the UI timeline fell back to legacy
tools-before-text ordering. Carry it through and lock with a projection
test covering both the stepId and legacy no-stepId shapes. (Codex review P1.)
The fade ring measures raw buffer graphemes but the rehype pass walks
rendered visible text, which link URLs and emphasis markers leave far
shorter; once the cumulative hidden amount outgrew the 280ms window the
streaming tail silently stopped fading. Count the visible graphemes,
shift the boundary and age lookups by the hidden total (exact at the
tail, eager near the boundary), and lock with a link-syntax test that
asserts raw-coordinate age lookups. (Codex + Pi review P2.)
kind+messageId is already unique across a turn's timeline (one thinking
and one text per step; tool groups keyed by first toolUseId). Folding the
index in meant a group inserted mid-timeline remounted every entry after
it, collapsing disclosures the user had opened. (Codex review P3.)
… answer
A step that only calls tools persists no assistant row, so its tools carry
a stepId no assistant message matches; buildTurnTimeline parked them in
pending until the end of the turn, rendering the common 'call tools, then
summarize next step' pattern as answer-then-tools. Ledger append order
guarantees no later step's tools can be pending at an assistant row, so
flush the orphans ahead of the step's content. (ChatGPT review P1.)
The group only auto-opened for waiting_permission, so an errored tool's
banner and output hid behind the collapsed summary line — a diagnosability
regression from the boxed cards, which kept errored tools expanded. Extract
the predicate as trowNeedsAttention and unit-test it. (ChatGPT review P3.)
@jackwener

Copy link
Copy Markdown
MemberAuthor

#636 已在本 review 进行期间被直接合入 main,且合并时与 #639 的 tool-activity 语义冲突已被正确整合(CronJob 预览挂接位置与本分支的手工合并一致,main 全量测试 2250/2250 绿)。本收编分支不再需要,关闭。Review 结论已留在 #636 评论。

@jackwener
jackwener deleted the pr-636-rebased branch July 8, 2026 16:59
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jackwener@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(ui): rework streaming output — step timeline, deep-thinking disclosure, tool trows, word fade-in (#636 rebased) - #644

Closed
jackwener wants to merge 10 commits into
mainfrom
pr-636-rebased
Closed

feat(ui): rework streaming output — step timeline, deep-thinking disclosure, tool trows, word fade-in (#636 rebased)#644
jackwener wants to merge 10 commits into
mainfrom
pr-636-rebased

Conversation

@jackwener

Copy link
Copy Markdown
Member

收编 #636(作者 @Astro-Han,保留全部原始 commits)+ rebase 到最新 main。

Maintainer review 结论(第一性原理)

高质量 PR,三层核心全部通过:数据层 stepId 可选兼容+双向投影;时间线聚合三类工具排序均有 ledger 论证;stream-fade 只包裹尾部新词、负 delay 防闪、snap 全跳过。治理意识好(动画白名单登记、时长绑 token)。

非阻塞注记:流式期间每帧全文 grapheme 分词——与 react-markdown 每帧全量 parse 同级,未改变复杂度;未来流式增量化时一起收益。

Rebase 冲突

#639(CronJob 预览)在 tool-activity.tsx 相遇:取本 PR 的 ToolCardBody 抽取结构,CronJob 预览挂进共享 body 的结果分发(card/trow 同享)。

验证

build ✓;typecheck 0;ui 46/46;desktop 2248/2248(含本 PR 新增三组契约/单测)。

Astro-Han added 10 commits July 8, 2026 21:57
Add optional ToolCallMessage.stepId, stamped by ToolRuntime from
getCurrentStepId() (same source as ToolStartEvent.stepId), and map it into
refs.stepId on the backfill path so post-restart model replay re-pairs a
tool call with its assistant step. The field ships with its first consumer,
the UI turn timeline (materializeTurns, next commit).
Replace PR1's neutral multi-step concatenation with a TurnTimelineItem
sequence (thinking / text / tools) rebuilt in storage order: each step's
thinking and text precede the tools whose stepId matches that step; legacy
stepless tools keep the tools-then-summary reading before the text; leftover
and live-only tools flush as trailing tool groups; adjacent thinking and tool
groups merge. Aggregate assistant/assistantThinking stay for copy/export/
prompt-rail consumers, with timeline as the rendering source of truth.
ToolActivityItem gains stepId for the pairing. Tests cover interleaved
multi-step, legacy single, pure-tool tail, live-only append, and merges.
…ol trow
Render the turn body from turn.timeline (materialize.ts) instead of the
tools-section + single-answer layout: each step's reasoning, answer, and tools
appear in production order. Commits 2 and 3 of the plan land together because
both restructure TurnView and can't be judged apart.
- DeepThinking: one controlled Collapsible (collapsed by default; no
defaultOpen) replacing ReasoningPanel and the .maka-turn-thinking <details>.
Live: shimmering '深度思考' title (new TextShimmer primitive + governance
keyframe maka-text-shimmer) and smooth plain-text body that follows the tail;
settled: Markdown + '复制思考过程'. '已截断' pill preserved.
- ToolTrow: a contiguous tool run as one flat, borderless disclosure —
single-tool groups render the tool's own row (no double nesting); multi-tool
groups add a summary line (shimmering active-tool description while running,
bucketed Chinese counts once settled via the pure summarizeTrowTools) that
expands to flat-stacked tool rows. waiting_permission auto-expands the group.
ToolActivityCard body extracted as ToolCardBody, shared by card and trow.
- Delete reasoning-panel.css + import, the .maka-turn-thinking / .maka-turn-tools
token blocks; update chat-marker + 406-motion governance contracts.
Full desktop suite (2224) + root typecheck green; trow-summary unit-tested.
Retire the ▎ blink caret (.maka-bubble-streaming::after + @Keyframes
maka-cursor) and its trailing-<p> display:inline hack in favor of a
per-word fade-in over the freshly revealed streaming tail.
New pure primitive stream-fade.ts: an append-record ring tracks each
growth of the displayed grapheme prefix as a batch, prunes batches older
than the fade window, and caps retained batches. From it we read a stable
boundary offset (older text never re-animates) and the age of any offset
(drives a negative CSS animation-delay so the entrance resumes mid-flight
across ~60Hz re-renders instead of re-flashing). A shared word/char
tokenizer groups whitespace and Latin runs while splitting CJK/emoji
per-grapheme. streamFadeRehypePlugin wraps post-boundary tokens in
.maka-stream-fade spans after rehype-highlight, advancing the cursor
through code fences but never wrapping inside them.
useStreamFade feeds both the answer bubble (via markdown-body's rehype
pass) and DeepThinking's plain-text live body. Under snap (reduced-motion
/ visual-smoke) the hook returns undefined and callers skip wrapping, so
deterministic captures show final text at full opacity.
@Keyframes maka-stream-fade-in animates opacity only; the fade window is
--duration-large (280ms) and FADE_MS is pinned to the same value so the
JS and CSS windows agree. Contracts updated: 406 governance functional
motion set, markdown-prose retired-hack asserts. Unit tests cover the
ring (window slide, order-independent ages, cap), the tokenizer, and the
rehype offset bookkeeping (multi-block prose, code fences unwrapped).
Finish the streaming UI rework wiring so the in-flight (live) path and
the review fixtures exercise the new per-step turn timeline.
- app-shell-session-events: the tool_start handler now carries the
event's stepId onto the live ToolActivityItem, so an in-flight tool is
ordered after its own step's thinking/text in the turn timeline instead
of being lumped into one trailing group. Optional — legacy / degraded
history emits no stepId and keeps the old grouping.
- chat-surface stories: add a MultiStepReasoning story whose fixture is a
single turn with two think->say->call steps (per-step thinking + text +
stepId-tagged tools), so Storybook covers the 深度思考 → answer → tool
trow per-step order.
- visual-smoke turn-narrative seed: append a second, multi-step turn to
the committed narrative so the auto-capture baseline locks the new
per-step timeline alongside the existing legacy stepless turn.
Verified: @maka/ui build + test, desktop build:main + full node:test
suite (2237), @maka/runtime test, and root typecheck (incl. renderer +
storybook projects) all green.
projectFunctionCall dropped event.refs.stepId, so sessions rebuilt from
the runtime event log (the session view's source of truth) lost the
tool-to-step pairing after reload and the UI timeline fell back to legacy
tools-before-text ordering. Carry it through and lock with a projection
test covering both the stepId and legacy no-stepId shapes. (Codex review P1.)
The fade ring measures raw buffer graphemes but the rehype pass walks
rendered visible text, which link URLs and emphasis markers leave far
shorter; once the cumulative hidden amount outgrew the 280ms window the
streaming tail silently stopped fading. Count the visible graphemes,
shift the boundary and age lookups by the hidden total (exact at the
tail, eager near the boundary), and lock with a link-syntax test that
asserts raw-coordinate age lookups. (Codex + Pi review P2.)
kind+messageId is already unique across a turn's timeline (one thinking
and one text per step; tool groups keyed by first toolUseId). Folding the
index in meant a group inserted mid-timeline remounted every entry after
it, collapsing disclosures the user had opened. (Codex review P3.)
… answer
A step that only calls tools persists no assistant row, so its tools carry
a stepId no assistant message matches; buildTurnTimeline parked them in
pending until the end of the turn, rendering the common 'call tools, then
summarize next step' pattern as answer-then-tools. Ledger append order
guarantees no later step's tools can be pending at an assistant row, so
flush the orphans ahead of the step's content. (ChatGPT review P1.)
The group only auto-opened for waiting_permission, so an errored tool's
banner and output hid behind the collapsed summary line — a diagnosability
regression from the boxed cards, which kept errored tools expanded. Extract
the predicate as trowNeedsAttention and unit-test it. (ChatGPT review P3.)
@jackwener

Copy link
Copy Markdown
MemberAuthor

#636 已在本 review 进行期间被直接合入 main,且合并时与 #639 的 tool-activity 语义冲突已被正确整合(CronJob 预览挂接位置与本分支的手工合并一致,main 全量测试 2250/2250 绿)。本收编分支不再需要,关闭。Review 结论已留在 #636 评论。

@jackwener
jackwener deleted the pr-636-rebased branch July 8, 2026 16:59
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jackwener@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(ui): rework streaming output — step timeline, deep-thinking disclosure, tool trows, word fade-in (#636 rebased) - #644

Closed
jackwener wants to merge 10 commits into
mainfrom
pr-636-rebased
Closed

feat(ui): rework streaming output — step timeline, deep-thinking disclosure, tool trows, word fade-in (#636 rebased)#644
jackwener wants to merge 10 commits into
mainfrom
pr-636-rebased

Conversation

@jackwener

Copy link
Copy Markdown
Member

收编 #636(作者 @Astro-Han,保留全部原始 commits)+ rebase 到最新 main。

Maintainer review 结论(第一性原理)

高质量 PR,三层核心全部通过:数据层 stepId 可选兼容+双向投影;时间线聚合三类工具排序均有 ledger 论证;stream-fade 只包裹尾部新词、负 delay 防闪、snap 全跳过。治理意识好(动画白名单登记、时长绑 token)。

非阻塞注记:流式期间每帧全文 grapheme 分词——与 react-markdown 每帧全量 parse 同级,未改变复杂度;未来流式增量化时一起收益。

Rebase 冲突

#639(CronJob 预览)在 tool-activity.tsx 相遇:取本 PR 的 ToolCardBody 抽取结构,CronJob 预览挂进共享 body 的结果分发(card/trow 同享)。

验证

build ✓;typecheck 0;ui 46/46;desktop 2248/2248(含本 PR 新增三组契约/单测)。

Astro-Han added 10 commits July 8, 2026 21:57
Add optional ToolCallMessage.stepId, stamped by ToolRuntime from
getCurrentStepId() (same source as ToolStartEvent.stepId), and map it into
refs.stepId on the backfill path so post-restart model replay re-pairs a
tool call with its assistant step. The field ships with its first consumer,
the UI turn timeline (materializeTurns, next commit).
Replace PR1's neutral multi-step concatenation with a TurnTimelineItem
sequence (thinking / text / tools) rebuilt in storage order: each step's
thinking and text precede the tools whose stepId matches that step; legacy
stepless tools keep the tools-then-summary reading before the text; leftover
and live-only tools flush as trailing tool groups; adjacent thinking and tool
groups merge. Aggregate assistant/assistantThinking stay for copy/export/
prompt-rail consumers, with timeline as the rendering source of truth.
ToolActivityItem gains stepId for the pairing. Tests cover interleaved
multi-step, legacy single, pure-tool tail, live-only append, and merges.
…ol trow
Render the turn body from turn.timeline (materialize.ts) instead of the
tools-section + single-answer layout: each step's reasoning, answer, and tools
appear in production order. Commits 2 and 3 of the plan land together because
both restructure TurnView and can't be judged apart.
- DeepThinking: one controlled Collapsible (collapsed by default; no
defaultOpen) replacing ReasoningPanel and the .maka-turn-thinking <details>.
Live: shimmering '深度思考' title (new TextShimmer primitive + governance
keyframe maka-text-shimmer) and smooth plain-text body that follows the tail;
settled: Markdown + '复制思考过程'. '已截断' pill preserved.
- ToolTrow: a contiguous tool run as one flat, borderless disclosure —
single-tool groups render the tool's own row (no double nesting); multi-tool
groups add a summary line (shimmering active-tool description while running,
bucketed Chinese counts once settled via the pure summarizeTrowTools) that
expands to flat-stacked tool rows. waiting_permission auto-expands the group.
ToolActivityCard body extracted as ToolCardBody, shared by card and trow.
- Delete reasoning-panel.css + import, the .maka-turn-thinking / .maka-turn-tools
token blocks; update chat-marker + 406-motion governance contracts.
Full desktop suite (2224) + root typecheck green; trow-summary unit-tested.
Retire the ▎ blink caret (.maka-bubble-streaming::after + @Keyframes
maka-cursor) and its trailing-<p> display:inline hack in favor of a
per-word fade-in over the freshly revealed streaming tail.
New pure primitive stream-fade.ts: an append-record ring tracks each
growth of the displayed grapheme prefix as a batch, prunes batches older
than the fade window, and caps retained batches. From it we read a stable
boundary offset (older text never re-animates) and the age of any offset
(drives a negative CSS animation-delay so the entrance resumes mid-flight
across ~60Hz re-renders instead of re-flashing). A shared word/char
tokenizer groups whitespace and Latin runs while splitting CJK/emoji
per-grapheme. streamFadeRehypePlugin wraps post-boundary tokens in
.maka-stream-fade spans after rehype-highlight, advancing the cursor
through code fences but never wrapping inside them.
useStreamFade feeds both the answer bubble (via markdown-body's rehype
pass) and DeepThinking's plain-text live body. Under snap (reduced-motion
/ visual-smoke) the hook returns undefined and callers skip wrapping, so
deterministic captures show final text at full opacity.
@Keyframes maka-stream-fade-in animates opacity only; the fade window is
--duration-large (280ms) and FADE_MS is pinned to the same value so the
JS and CSS windows agree. Contracts updated: 406 governance functional
motion set, markdown-prose retired-hack asserts. Unit tests cover the
ring (window slide, order-independent ages, cap), the tokenizer, and the
rehype offset bookkeeping (multi-block prose, code fences unwrapped).
Finish the streaming UI rework wiring so the in-flight (live) path and
the review fixtures exercise the new per-step turn timeline.
- app-shell-session-events: the tool_start handler now carries the
event's stepId onto the live ToolActivityItem, so an in-flight tool is
ordered after its own step's thinking/text in the turn timeline instead
of being lumped into one trailing group. Optional — legacy / degraded
history emits no stepId and keeps the old grouping.
- chat-surface stories: add a MultiStepReasoning story whose fixture is a
single turn with two think->say->call steps (per-step thinking + text +
stepId-tagged tools), so Storybook covers the 深度思考 → answer → tool
trow per-step order.
- visual-smoke turn-narrative seed: append a second, multi-step turn to
the committed narrative so the auto-capture baseline locks the new
per-step timeline alongside the existing legacy stepless turn.
Verified: @maka/ui build + test, desktop build:main + full node:test
suite (2237), @maka/runtime test, and root typecheck (incl. renderer +
storybook projects) all green.
projectFunctionCall dropped event.refs.stepId, so sessions rebuilt from
the runtime event log (the session view's source of truth) lost the
tool-to-step pairing after reload and the UI timeline fell back to legacy
tools-before-text ordering. Carry it through and lock with a projection
test covering both the stepId and legacy no-stepId shapes. (Codex review P1.)
The fade ring measures raw buffer graphemes but the rehype pass walks
rendered visible text, which link URLs and emphasis markers leave far
shorter; once the cumulative hidden amount outgrew the 280ms window the
streaming tail silently stopped fading. Count the visible graphemes,
shift the boundary and age lookups by the hidden total (exact at the
tail, eager near the boundary), and lock with a link-syntax test that
asserts raw-coordinate age lookups. (Codex + Pi review P2.)
kind+messageId is already unique across a turn's timeline (one thinking
and one text per step; tool groups keyed by first toolUseId). Folding the
index in meant a group inserted mid-timeline remounted every entry after
it, collapsing disclosures the user had opened. (Codex review P3.)
… answer
A step that only calls tools persists no assistant row, so its tools carry
a stepId no assistant message matches; buildTurnTimeline parked them in
pending until the end of the turn, rendering the common 'call tools, then
summarize next step' pattern as answer-then-tools. Ledger append order
guarantees no later step's tools can be pending at an assistant row, so
flush the orphans ahead of the step's content. (ChatGPT review P1.)
The group only auto-opened for waiting_permission, so an errored tool's
banner and output hid behind the collapsed summary line — a diagnosability
regression from the boxed cards, which kept errored tools expanded. Extract
the predicate as trowNeedsAttention and unit-test it. (ChatGPT review P3.)
@jackwener

Copy link
Copy Markdown
MemberAuthor

#636 已在本 review 进行期间被直接合入 main,且合并时与 #639 的 tool-activity 语义冲突已被正确整合(CronJob 预览挂接位置与本分支的手工合并一致,main 全量测试 2250/2250 绿)。本收编分支不再需要,关闭。Review 结论已留在 #636 评论。

@jackwener
jackwener deleted the pr-636-rebased branch July 8, 2026 16:59
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jackwener@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(ui): rework streaming output — step timeline, deep-thinking disclosure, tool trows, word fade-in (#636 rebased) - #644

Closed
jackwener wants to merge 10 commits into
mainfrom
pr-636-rebased
Closed

feat(ui): rework streaming output — step timeline, deep-thinking disclosure, tool trows, word fade-in (#636 rebased)#644
jackwener wants to merge 10 commits into
mainfrom
pr-636-rebased

Conversation

@jackwener

Copy link
Copy Markdown
Member

收编 #636(作者 @Astro-Han,保留全部原始 commits)+ rebase 到最新 main。

Maintainer review 结论(第一性原理)

高质量 PR,三层核心全部通过:数据层 stepId 可选兼容+双向投影;时间线聚合三类工具排序均有 ledger 论证;stream-fade 只包裹尾部新词、负 delay 防闪、snap 全跳过。治理意识好(动画白名单登记、时长绑 token)。

非阻塞注记:流式期间每帧全文 grapheme 分词——与 react-markdown 每帧全量 parse 同级,未改变复杂度;未来流式增量化时一起收益。

Rebase 冲突

#639(CronJob 预览)在 tool-activity.tsx 相遇:取本 PR 的 ToolCardBody 抽取结构,CronJob 预览挂进共享 body 的结果分发(card/trow 同享)。

验证

build ✓;typecheck 0;ui 46/46;desktop 2248/2248(含本 PR 新增三组契约/单测)。

Astro-Han added 10 commits July 8, 2026 21:57
Add optional ToolCallMessage.stepId, stamped by ToolRuntime from
getCurrentStepId() (same source as ToolStartEvent.stepId), and map it into
refs.stepId on the backfill path so post-restart model replay re-pairs a
tool call with its assistant step. The field ships with its first consumer,
the UI turn timeline (materializeTurns, next commit).
Replace PR1's neutral multi-step concatenation with a TurnTimelineItem
sequence (thinking / text / tools) rebuilt in storage order: each step's
thinking and text precede the tools whose stepId matches that step; legacy
stepless tools keep the tools-then-summary reading before the text; leftover
and live-only tools flush as trailing tool groups; adjacent thinking and tool
groups merge. Aggregate assistant/assistantThinking stay for copy/export/
prompt-rail consumers, with timeline as the rendering source of truth.
ToolActivityItem gains stepId for the pairing. Tests cover interleaved
multi-step, legacy single, pure-tool tail, live-only append, and merges.
…ol trow
Render the turn body from turn.timeline (materialize.ts) instead of the
tools-section + single-answer layout: each step's reasoning, answer, and tools
appear in production order. Commits 2 and 3 of the plan land together because
both restructure TurnView and can't be judged apart.
- DeepThinking: one controlled Collapsible (collapsed by default; no
defaultOpen) replacing ReasoningPanel and the .maka-turn-thinking <details>.
Live: shimmering '深度思考' title (new TextShimmer primitive + governance
keyframe maka-text-shimmer) and smooth plain-text body that follows the tail;
settled: Markdown + '复制思考过程'. '已截断' pill preserved.
- ToolTrow: a contiguous tool run as one flat, borderless disclosure —
single-tool groups render the tool's own row (no double nesting); multi-tool
groups add a summary line (shimmering active-tool description while running,
bucketed Chinese counts once settled via the pure summarizeTrowTools) that
expands to flat-stacked tool rows. waiting_permission auto-expands the group.
ToolActivityCard body extracted as ToolCardBody, shared by card and trow.
- Delete reasoning-panel.css + import, the .maka-turn-thinking / .maka-turn-tools
token blocks; update chat-marker + 406-motion governance contracts.
Full desktop suite (2224) + root typecheck green; trow-summary unit-tested.
Retire the ▎ blink caret (.maka-bubble-streaming::after + @Keyframes
maka-cursor) and its trailing-<p> display:inline hack in favor of a
per-word fade-in over the freshly revealed streaming tail.
New pure primitive stream-fade.ts: an append-record ring tracks each
growth of the displayed grapheme prefix as a batch, prunes batches older
than the fade window, and caps retained batches. From it we read a stable
boundary offset (older text never re-animates) and the age of any offset
(drives a negative CSS animation-delay so the entrance resumes mid-flight
across ~60Hz re-renders instead of re-flashing). A shared word/char
tokenizer groups whitespace and Latin runs while splitting CJK/emoji
per-grapheme. streamFadeRehypePlugin wraps post-boundary tokens in
.maka-stream-fade spans after rehype-highlight, advancing the cursor
through code fences but never wrapping inside them.
useStreamFade feeds both the answer bubble (via markdown-body's rehype
pass) and DeepThinking's plain-text live body. Under snap (reduced-motion
/ visual-smoke) the hook returns undefined and callers skip wrapping, so
deterministic captures show final text at full opacity.
@Keyframes maka-stream-fade-in animates opacity only; the fade window is
--duration-large (280ms) and FADE_MS is pinned to the same value so the
JS and CSS windows agree. Contracts updated: 406 governance functional
motion set, markdown-prose retired-hack asserts. Unit tests cover the
ring (window slide, order-independent ages, cap), the tokenizer, and the
rehype offset bookkeeping (multi-block prose, code fences unwrapped).
Finish the streaming UI rework wiring so the in-flight (live) path and
the review fixtures exercise the new per-step turn timeline.
- app-shell-session-events: the tool_start handler now carries the
event's stepId onto the live ToolActivityItem, so an in-flight tool is
ordered after its own step's thinking/text in the turn timeline instead
of being lumped into one trailing group. Optional — legacy / degraded
history emits no stepId and keeps the old grouping.
- chat-surface stories: add a MultiStepReasoning story whose fixture is a
single turn with two think->say->call steps (per-step thinking + text +
stepId-tagged tools), so Storybook covers the 深度思考 → answer → tool
trow per-step order.
- visual-smoke turn-narrative seed: append a second, multi-step turn to
the committed narrative so the auto-capture baseline locks the new
per-step timeline alongside the existing legacy stepless turn.
Verified: @maka/ui build + test, desktop build:main + full node:test
suite (2237), @maka/runtime test, and root typecheck (incl. renderer +
storybook projects) all green.
projectFunctionCall dropped event.refs.stepId, so sessions rebuilt from
the runtime event log (the session view's source of truth) lost the
tool-to-step pairing after reload and the UI timeline fell back to legacy
tools-before-text ordering. Carry it through and lock with a projection
test covering both the stepId and legacy no-stepId shapes. (Codex review P1.)
The fade ring measures raw buffer graphemes but the rehype pass walks
rendered visible text, which link URLs and emphasis markers leave far
shorter; once the cumulative hidden amount outgrew the 280ms window the
streaming tail silently stopped fading. Count the visible graphemes,
shift the boundary and age lookups by the hidden total (exact at the
tail, eager near the boundary), and lock with a link-syntax test that
asserts raw-coordinate age lookups. (Codex + Pi review P2.)
kind+messageId is already unique across a turn's timeline (one thinking
and one text per step; tool groups keyed by first toolUseId). Folding the
index in meant a group inserted mid-timeline remounted every entry after
it, collapsing disclosures the user had opened. (Codex review P3.)
… answer
A step that only calls tools persists no assistant row, so its tools carry
a stepId no assistant message matches; buildTurnTimeline parked them in
pending until the end of the turn, rendering the common 'call tools, then
summarize next step' pattern as answer-then-tools. Ledger append order
guarantees no later step's tools can be pending at an assistant row, so
flush the orphans ahead of the step's content. (ChatGPT review P1.)
The group only auto-opened for waiting_permission, so an errored tool's
banner and output hid behind the collapsed summary line — a diagnosability
regression from the boxed cards, which kept errored tools expanded. Extract
the predicate as trowNeedsAttention and unit-test it. (ChatGPT review P3.)
@jackwener

Copy link
Copy Markdown
MemberAuthor

#636 已在本 review 进行期间被直接合入 main,且合并时与 #639 的 tool-activity 语义冲突已被正确整合(CronJob 预览挂接位置与本分支的手工合并一致,main 全量测试 2250/2250 绿)。本收编分支不再需要,关闭。Review 结论已留在 #636 评论。

@jackwener
jackwener deleted the pr-636-rebased branch July 8, 2026 16:59
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jackwener@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(ui): rework streaming output — step timeline, deep-thinking disclosure, tool trows, word fade-in (#636 rebased) - #644

Closed
jackwener wants to merge 10 commits into
mainfrom
pr-636-rebased
Closed

feat(ui): rework streaming output — step timeline, deep-thinking disclosure, tool trows, word fade-in (#636 rebased)#644
jackwener wants to merge 10 commits into
mainfrom
pr-636-rebased

Conversation

@jackwener

Copy link
Copy Markdown
Member

收编 #636(作者 @Astro-Han,保留全部原始 commits)+ rebase 到最新 main。

Maintainer review 结论(第一性原理)

高质量 PR,三层核心全部通过:数据层 stepId 可选兼容+双向投影;时间线聚合三类工具排序均有 ledger 论证;stream-fade 只包裹尾部新词、负 delay 防闪、snap 全跳过。治理意识好(动画白名单登记、时长绑 token)。

非阻塞注记:流式期间每帧全文 grapheme 分词——与 react-markdown 每帧全量 parse 同级,未改变复杂度;未来流式增量化时一起收益。

Rebase 冲突

#639(CronJob 预览)在 tool-activity.tsx 相遇:取本 PR 的 ToolCardBody 抽取结构,CronJob 预览挂进共享 body 的结果分发(card/trow 同享)。

验证

build ✓;typecheck 0;ui 46/46;desktop 2248/2248(含本 PR 新增三组契约/单测)。

Astro-Han added 10 commits July 8, 2026 21:57
Add optional ToolCallMessage.stepId, stamped by ToolRuntime from
getCurrentStepId() (same source as ToolStartEvent.stepId), and map it into
refs.stepId on the backfill path so post-restart model replay re-pairs a
tool call with its assistant step. The field ships with its first consumer,
the UI turn timeline (materializeTurns, next commit).
Replace PR1's neutral multi-step concatenation with a TurnTimelineItem
sequence (thinking / text / tools) rebuilt in storage order: each step's
thinking and text precede the tools whose stepId matches that step; legacy
stepless tools keep the tools-then-summary reading before the text; leftover
and live-only tools flush as trailing tool groups; adjacent thinking and tool
groups merge. Aggregate assistant/assistantThinking stay for copy/export/
prompt-rail consumers, with timeline as the rendering source of truth.
ToolActivityItem gains stepId for the pairing. Tests cover interleaved
multi-step, legacy single, pure-tool tail, live-only append, and merges.
…ol trow
Render the turn body from turn.timeline (materialize.ts) instead of the
tools-section + single-answer layout: each step's reasoning, answer, and tools
appear in production order. Commits 2 and 3 of the plan land together because
both restructure TurnView and can't be judged apart.
- DeepThinking: one controlled Collapsible (collapsed by default; no
defaultOpen) replacing ReasoningPanel and the .maka-turn-thinking <details>.
Live: shimmering '深度思考' title (new TextShimmer primitive + governance
keyframe maka-text-shimmer) and smooth plain-text body that follows the tail;
settled: Markdown + '复制思考过程'. '已截断' pill preserved.
- ToolTrow: a contiguous tool run as one flat, borderless disclosure —
single-tool groups render the tool's own row (no double nesting); multi-tool
groups add a summary line (shimmering active-tool description while running,
bucketed Chinese counts once settled via the pure summarizeTrowTools) that
expands to flat-stacked tool rows. waiting_permission auto-expands the group.
ToolActivityCard body extracted as ToolCardBody, shared by card and trow.
- Delete reasoning-panel.css + import, the .maka-turn-thinking / .maka-turn-tools
token blocks; update chat-marker + 406-motion governance contracts.
Full desktop suite (2224) + root typecheck green; trow-summary unit-tested.
Retire the ▎ blink caret (.maka-bubble-streaming::after + @Keyframes
maka-cursor) and its trailing-<p> display:inline hack in favor of a
per-word fade-in over the freshly revealed streaming tail.
New pure primitive stream-fade.ts: an append-record ring tracks each
growth of the displayed grapheme prefix as a batch, prunes batches older
than the fade window, and caps retained batches. From it we read a stable
boundary offset (older text never re-animates) and the age of any offset
(drives a negative CSS animation-delay so the entrance resumes mid-flight
across ~60Hz re-renders instead of re-flashing). A shared word/char
tokenizer groups whitespace and Latin runs while splitting CJK/emoji
per-grapheme. streamFadeRehypePlugin wraps post-boundary tokens in
.maka-stream-fade spans after rehype-highlight, advancing the cursor
through code fences but never wrapping inside them.
useStreamFade feeds both the answer bubble (via markdown-body's rehype
pass) and DeepThinking's plain-text live body. Under snap (reduced-motion
/ visual-smoke) the hook returns undefined and callers skip wrapping, so
deterministic captures show final text at full opacity.
@Keyframes maka-stream-fade-in animates opacity only; the fade window is
--duration-large (280ms) and FADE_MS is pinned to the same value so the
JS and CSS windows agree. Contracts updated: 406 governance functional
motion set, markdown-prose retired-hack asserts. Unit tests cover the
ring (window slide, order-independent ages, cap), the tokenizer, and the
rehype offset bookkeeping (multi-block prose, code fences unwrapped).
Finish the streaming UI rework wiring so the in-flight (live) path and
the review fixtures exercise the new per-step turn timeline.
- app-shell-session-events: the tool_start handler now carries the
event's stepId onto the live ToolActivityItem, so an in-flight tool is
ordered after its own step's thinking/text in the turn timeline instead
of being lumped into one trailing group. Optional — legacy / degraded
history emits no stepId and keeps the old grouping.
- chat-surface stories: add a MultiStepReasoning story whose fixture is a
single turn with two think->say->call steps (per-step thinking + text +
stepId-tagged tools), so Storybook covers the 深度思考 → answer → tool
trow per-step order.
- visual-smoke turn-narrative seed: append a second, multi-step turn to
the committed narrative so the auto-capture baseline locks the new
per-step timeline alongside the existing legacy stepless turn.
Verified: @maka/ui build + test, desktop build:main + full node:test
suite (2237), @maka/runtime test, and root typecheck (incl. renderer +
storybook projects) all green.
projectFunctionCall dropped event.refs.stepId, so sessions rebuilt from
the runtime event log (the session view's source of truth) lost the
tool-to-step pairing after reload and the UI timeline fell back to legacy
tools-before-text ordering. Carry it through and lock with a projection
test covering both the stepId and legacy no-stepId shapes. (Codex review P1.)
The fade ring measures raw buffer graphemes but the rehype pass walks
rendered visible text, which link URLs and emphasis markers leave far
shorter; once the cumulative hidden amount outgrew the 280ms window the
streaming tail silently stopped fading. Count the visible graphemes,
shift the boundary and age lookups by the hidden total (exact at the
tail, eager near the boundary), and lock with a link-syntax test that
asserts raw-coordinate age lookups. (Codex + Pi review P2.)
kind+messageId is already unique across a turn's timeline (one thinking
and one text per step; tool groups keyed by first toolUseId). Folding the
index in meant a group inserted mid-timeline remounted every entry after
it, collapsing disclosures the user had opened. (Codex review P3.)
… answer
A step that only calls tools persists no assistant row, so its tools carry
a stepId no assistant message matches; buildTurnTimeline parked them in
pending until the end of the turn, rendering the common 'call tools, then
summarize next step' pattern as answer-then-tools. Ledger append order
guarantees no later step's tools can be pending at an assistant row, so
flush the orphans ahead of the step's content. (ChatGPT review P1.)
The group only auto-opened for waiting_permission, so an errored tool's
banner and output hid behind the collapsed summary line — a diagnosability
regression from the boxed cards, which kept errored tools expanded. Extract
the predicate as trowNeedsAttention and unit-test it. (ChatGPT review P3.)
@jackwener

Copy link
Copy Markdown
MemberAuthor

#636 已在本 review 进行期间被直接合入 main,且合并时与 #639 的 tool-activity 语义冲突已被正确整合(CronJob 预览挂接位置与本分支的手工合并一致,main 全量测试 2250/2250 绿)。本收编分支不再需要,关闭。Review 结论已留在 #636 评论。

@jackwener
jackwener deleted the pr-636-rebased branch July 8, 2026 16:59
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jackwener@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(ui): rework streaming output — step timeline, deep-thinking disclosure, tool trows, word fade-in (#636 rebased) - #644

Closed
jackwener wants to merge 10 commits into
mainfrom
pr-636-rebased
Closed

feat(ui): rework streaming output — step timeline, deep-thinking disclosure, tool trows, word fade-in (#636 rebased)#644
jackwener wants to merge 10 commits into
mainfrom
pr-636-rebased

Conversation

@jackwener

Copy link
Copy Markdown
Member

收编 #636(作者 @Astro-Han,保留全部原始 commits)+ rebase 到最新 main。

Maintainer review 结论(第一性原理)

高质量 PR,三层核心全部通过:数据层 stepId 可选兼容+双向投影;时间线聚合三类工具排序均有 ledger 论证;stream-fade 只包裹尾部新词、负 delay 防闪、snap 全跳过。治理意识好(动画白名单登记、时长绑 token)。

非阻塞注记:流式期间每帧全文 grapheme 分词——与 react-markdown 每帧全量 parse 同级,未改变复杂度;未来流式增量化时一起收益。

Rebase 冲突

#639(CronJob 预览)在 tool-activity.tsx 相遇:取本 PR 的 ToolCardBody 抽取结构,CronJob 预览挂进共享 body 的结果分发(card/trow 同享)。

验证

build ✓;typecheck 0;ui 46/46;desktop 2248/2248(含本 PR 新增三组契约/单测)。

Astro-Han added 10 commits July 8, 2026 21:57
Add optional ToolCallMessage.stepId, stamped by ToolRuntime from
getCurrentStepId() (same source as ToolStartEvent.stepId), and map it into
refs.stepId on the backfill path so post-restart model replay re-pairs a
tool call with its assistant step. The field ships with its first consumer,
the UI turn timeline (materializeTurns, next commit).
Replace PR1's neutral multi-step concatenation with a TurnTimelineItem
sequence (thinking / text / tools) rebuilt in storage order: each step's
thinking and text precede the tools whose stepId matches that step; legacy
stepless tools keep the tools-then-summary reading before the text; leftover
and live-only tools flush as trailing tool groups; adjacent thinking and tool
groups merge. Aggregate assistant/assistantThinking stay for copy/export/
prompt-rail consumers, with timeline as the rendering source of truth.
ToolActivityItem gains stepId for the pairing. Tests cover interleaved
multi-step, legacy single, pure-tool tail, live-only append, and merges.
…ol trow
Render the turn body from turn.timeline (materialize.ts) instead of the
tools-section + single-answer layout: each step's reasoning, answer, and tools
appear in production order. Commits 2 and 3 of the plan land together because
both restructure TurnView and can't be judged apart.
- DeepThinking: one controlled Collapsible (collapsed by default; no
defaultOpen) replacing ReasoningPanel and the .maka-turn-thinking <details>.
Live: shimmering '深度思考' title (new TextShimmer primitive + governance
keyframe maka-text-shimmer) and smooth plain-text body that follows the tail;
settled: Markdown + '复制思考过程'. '已截断' pill preserved.
- ToolTrow: a contiguous tool run as one flat, borderless disclosure —
single-tool groups render the tool's own row (no double nesting); multi-tool
groups add a summary line (shimmering active-tool description while running,
bucketed Chinese counts once settled via the pure summarizeTrowTools) that
expands to flat-stacked tool rows. waiting_permission auto-expands the group.
ToolActivityCard body extracted as ToolCardBody, shared by card and trow.
- Delete reasoning-panel.css + import, the .maka-turn-thinking / .maka-turn-tools
token blocks; update chat-marker + 406-motion governance contracts.
Full desktop suite (2224) + root typecheck green; trow-summary unit-tested.
Retire the ▎ blink caret (.maka-bubble-streaming::after + @Keyframes
maka-cursor) and its trailing-<p> display:inline hack in favor of a
per-word fade-in over the freshly revealed streaming tail.
New pure primitive stream-fade.ts: an append-record ring tracks each
growth of the displayed grapheme prefix as a batch, prunes batches older
than the fade window, and caps retained batches. From it we read a stable
boundary offset (older text never re-animates) and the age of any offset
(drives a negative CSS animation-delay so the entrance resumes mid-flight
across ~60Hz re-renders instead of re-flashing). A shared word/char
tokenizer groups whitespace and Latin runs while splitting CJK/emoji
per-grapheme. streamFadeRehypePlugin wraps post-boundary tokens in
.maka-stream-fade spans after rehype-highlight, advancing the cursor
through code fences but never wrapping inside them.
useStreamFade feeds both the answer bubble (via markdown-body's rehype
pass) and DeepThinking's plain-text live body. Under snap (reduced-motion
/ visual-smoke) the hook returns undefined and callers skip wrapping, so
deterministic captures show final text at full opacity.
@Keyframes maka-stream-fade-in animates opacity only; the fade window is
--duration-large (280ms) and FADE_MS is pinned to the same value so the
JS and CSS windows agree. Contracts updated: 406 governance functional
motion set, markdown-prose retired-hack asserts. Unit tests cover the
ring (window slide, order-independent ages, cap), the tokenizer, and the
rehype offset bookkeeping (multi-block prose, code fences unwrapped).
Finish the streaming UI rework wiring so the in-flight (live) path and
the review fixtures exercise the new per-step turn timeline.
- app-shell-session-events: the tool_start handler now carries the
event's stepId onto the live ToolActivityItem, so an in-flight tool is
ordered after its own step's thinking/text in the turn timeline instead
of being lumped into one trailing group. Optional — legacy / degraded
history emits no stepId and keeps the old grouping.
- chat-surface stories: add a MultiStepReasoning story whose fixture is a
single turn with two think->say->call steps (per-step thinking + text +
stepId-tagged tools), so Storybook covers the 深度思考 → answer → tool
trow per-step order.
- visual-smoke turn-narrative seed: append a second, multi-step turn to
the committed narrative so the auto-capture baseline locks the new
per-step timeline alongside the existing legacy stepless turn.
Verified: @maka/ui build + test, desktop build:main + full node:test
suite (2237), @maka/runtime test, and root typecheck (incl. renderer +
storybook projects) all green.
projectFunctionCall dropped event.refs.stepId, so sessions rebuilt from
the runtime event log (the session view's source of truth) lost the
tool-to-step pairing after reload and the UI timeline fell back to legacy
tools-before-text ordering. Carry it through and lock with a projection
test covering both the stepId and legacy no-stepId shapes. (Codex review P1.)
The fade ring measures raw buffer graphemes but the rehype pass walks
rendered visible text, which link URLs and emphasis markers leave far
shorter; once the cumulative hidden amount outgrew the 280ms window the
streaming tail silently stopped fading. Count the visible graphemes,
shift the boundary and age lookups by the hidden total (exact at the
tail, eager near the boundary), and lock with a link-syntax test that
asserts raw-coordinate age lookups. (Codex + Pi review P2.)
kind+messageId is already unique across a turn's timeline (one thinking
and one text per step; tool groups keyed by first toolUseId). Folding the
index in meant a group inserted mid-timeline remounted every entry after
it, collapsing disclosures the user had opened. (Codex review P3.)
… answer
A step that only calls tools persists no assistant row, so its tools carry
a stepId no assistant message matches; buildTurnTimeline parked them in
pending until the end of the turn, rendering the common 'call tools, then
summarize next step' pattern as answer-then-tools. Ledger append order
guarantees no later step's tools can be pending at an assistant row, so
flush the orphans ahead of the step's content. (ChatGPT review P1.)
The group only auto-opened for waiting_permission, so an errored tool's
banner and output hid behind the collapsed summary line — a diagnosability
regression from the boxed cards, which kept errored tools expanded. Extract
the predicate as trowNeedsAttention and unit-test it. (ChatGPT review P3.)
@jackwener

Copy link
Copy Markdown
MemberAuthor

#636 已在本 review 进行期间被直接合入 main,且合并时与 #639 的 tool-activity 语义冲突已被正确整合(CronJob 预览挂接位置与本分支的手工合并一致,main 全量测试 2250/2250 绿)。本收编分支不再需要,关闭。Review 结论已留在 #636 评论。

@jackwener
jackwener deleted the pr-636-rebased branch July 8, 2026 16:59
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jackwener@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(ui): rework streaming output — step timeline, deep-thinking disclosure, tool trows, word fade-in (#636 rebased) - #644

Closed
jackwener wants to merge 10 commits into
mainfrom
pr-636-rebased
Closed

feat(ui): rework streaming output — step timeline, deep-thinking disclosure, tool trows, word fade-in (#636 rebased)#644
jackwener wants to merge 10 commits into
mainfrom
pr-636-rebased

Conversation

@jackwener

Copy link
Copy Markdown
Member

收编 #636(作者 @Astro-Han,保留全部原始 commits)+ rebase 到最新 main。

Maintainer review 结论(第一性原理)

高质量 PR,三层核心全部通过:数据层 stepId 可选兼容+双向投影;时间线聚合三类工具排序均有 ledger 论证;stream-fade 只包裹尾部新词、负 delay 防闪、snap 全跳过。治理意识好(动画白名单登记、时长绑 token)。

非阻塞注记:流式期间每帧全文 grapheme 分词——与 react-markdown 每帧全量 parse 同级,未改变复杂度;未来流式增量化时一起收益。

Rebase 冲突

#639(CronJob 预览)在 tool-activity.tsx 相遇:取本 PR 的 ToolCardBody 抽取结构,CronJob 预览挂进共享 body 的结果分发(card/trow 同享)。

验证

build ✓;typecheck 0;ui 46/46;desktop 2248/2248(含本 PR 新增三组契约/单测)。

Astro-Han added 10 commits July 8, 2026 21:57
Add optional ToolCallMessage.stepId, stamped by ToolRuntime from
getCurrentStepId() (same source as ToolStartEvent.stepId), and map it into
refs.stepId on the backfill path so post-restart model replay re-pairs a
tool call with its assistant step. The field ships with its first consumer,
the UI turn timeline (materializeTurns, next commit).
Replace PR1's neutral multi-step concatenation with a TurnTimelineItem
sequence (thinking / text / tools) rebuilt in storage order: each step's
thinking and text precede the tools whose stepId matches that step; legacy
stepless tools keep the tools-then-summary reading before the text; leftover
and live-only tools flush as trailing tool groups; adjacent thinking and tool
groups merge. Aggregate assistant/assistantThinking stay for copy/export/
prompt-rail consumers, with timeline as the rendering source of truth.
ToolActivityItem gains stepId for the pairing. Tests cover interleaved
multi-step, legacy single, pure-tool tail, live-only append, and merges.
…ol trow
Render the turn body from turn.timeline (materialize.ts) instead of the
tools-section + single-answer layout: each step's reasoning, answer, and tools
appear in production order. Commits 2 and 3 of the plan land together because
both restructure TurnView and can't be judged apart.
- DeepThinking: one controlled Collapsible (collapsed by default; no
defaultOpen) replacing ReasoningPanel and the .maka-turn-thinking <details>.
Live: shimmering '深度思考' title (new TextShimmer primitive + governance
keyframe maka-text-shimmer) and smooth plain-text body that follows the tail;
settled: Markdown + '复制思考过程'. '已截断' pill preserved.
- ToolTrow: a contiguous tool run as one flat, borderless disclosure —
single-tool groups render the tool's own row (no double nesting); multi-tool
groups add a summary line (shimmering active-tool description while running,
bucketed Chinese counts once settled via the pure summarizeTrowTools) that
expands to flat-stacked tool rows. waiting_permission auto-expands the group.
ToolActivityCard body extracted as ToolCardBody, shared by card and trow.
- Delete reasoning-panel.css + import, the .maka-turn-thinking / .maka-turn-tools
token blocks; update chat-marker + 406-motion governance contracts.
Full desktop suite (2224) + root typecheck green; trow-summary unit-tested.
Retire the ▎ blink caret (.maka-bubble-streaming::after + @Keyframes
maka-cursor) and its trailing-<p> display:inline hack in favor of a
per-word fade-in over the freshly revealed streaming tail.
New pure primitive stream-fade.ts: an append-record ring tracks each
growth of the displayed grapheme prefix as a batch, prunes batches older
than the fade window, and caps retained batches. From it we read a stable
boundary offset (older text never re-animates) and the age of any offset
(drives a negative CSS animation-delay so the entrance resumes mid-flight
across ~60Hz re-renders instead of re-flashing). A shared word/char
tokenizer groups whitespace and Latin runs while splitting CJK/emoji
per-grapheme. streamFadeRehypePlugin wraps post-boundary tokens in
.maka-stream-fade spans after rehype-highlight, advancing the cursor
through code fences but never wrapping inside them.
useStreamFade feeds both the answer bubble (via markdown-body's rehype
pass) and DeepThinking's plain-text live body. Under snap (reduced-motion
/ visual-smoke) the hook returns undefined and callers skip wrapping, so
deterministic captures show final text at full opacity.
@Keyframes maka-stream-fade-in animates opacity only; the fade window is
--duration-large (280ms) and FADE_MS is pinned to the same value so the
JS and CSS windows agree. Contracts updated: 406 governance functional
motion set, markdown-prose retired-hack asserts. Unit tests cover the
ring (window slide, order-independent ages, cap), the tokenizer, and the
rehype offset bookkeeping (multi-block prose, code fences unwrapped).
Finish the streaming UI rework wiring so the in-flight (live) path and
the review fixtures exercise the new per-step turn timeline.
- app-shell-session-events: the tool_start handler now carries the
event's stepId onto the live ToolActivityItem, so an in-flight tool is
ordered after its own step's thinking/text in the turn timeline instead
of being lumped into one trailing group. Optional — legacy / degraded
history emits no stepId and keeps the old grouping.
- chat-surface stories: add a MultiStepReasoning story whose fixture is a
single turn with two think->say->call steps (per-step thinking + text +
stepId-tagged tools), so Storybook covers the 深度思考 → answer → tool
trow per-step order.
- visual-smoke turn-narrative seed: append a second, multi-step turn to
the committed narrative so the auto-capture baseline locks the new
per-step timeline alongside the existing legacy stepless turn.
Verified: @maka/ui build + test, desktop build:main + full node:test
suite (2237), @maka/runtime test, and root typecheck (incl. renderer +
storybook projects) all green.
projectFunctionCall dropped event.refs.stepId, so sessions rebuilt from
the runtime event log (the session view's source of truth) lost the
tool-to-step pairing after reload and the UI timeline fell back to legacy
tools-before-text ordering. Carry it through and lock with a projection
test covering both the stepId and legacy no-stepId shapes. (Codex review P1.)
The fade ring measures raw buffer graphemes but the rehype pass walks
rendered visible text, which link URLs and emphasis markers leave far
shorter; once the cumulative hidden amount outgrew the 280ms window the
streaming tail silently stopped fading. Count the visible graphemes,
shift the boundary and age lookups by the hidden total (exact at the
tail, eager near the boundary), and lock with a link-syntax test that
asserts raw-coordinate age lookups. (Codex + Pi review P2.)
kind+messageId is already unique across a turn's timeline (one thinking
and one text per step; tool groups keyed by first toolUseId). Folding the
index in meant a group inserted mid-timeline remounted every entry after
it, collapsing disclosures the user had opened. (Codex review P3.)
… answer
A step that only calls tools persists no assistant row, so its tools carry
a stepId no assistant message matches; buildTurnTimeline parked them in
pending until the end of the turn, rendering the common 'call tools, then
summarize next step' pattern as answer-then-tools. Ledger append order
guarantees no later step's tools can be pending at an assistant row, so
flush the orphans ahead of the step's content. (ChatGPT review P1.)
The group only auto-opened for waiting_permission, so an errored tool's
banner and output hid behind the collapsed summary line — a diagnosability
regression from the boxed cards, which kept errored tools expanded. Extract
the predicate as trowNeedsAttention and unit-test it. (ChatGPT review P3.)
@jackwener

Copy link
Copy Markdown
MemberAuthor

#636 已在本 review 进行期间被直接合入 main,且合并时与 #639 的 tool-activity 语义冲突已被正确整合(CronJob 预览挂接位置与本分支的手工合并一致,main 全量测试 2250/2250 绿)。本收编分支不再需要,关闭。Review 结论已留在 #636 评论。

@jackwener
jackwener deleted the pr-636-rebased branch July 8, 2026 16:59
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jackwener@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(ui): rework streaming output — step timeline, deep-thinking disclosure, tool trows, word fade-in (#636 rebased) - #644

Closed
jackwener wants to merge 10 commits into
mainfrom
pr-636-rebased
Closed

feat(ui): rework streaming output — step timeline, deep-thinking disclosure, tool trows, word fade-in (#636 rebased)#644
jackwener wants to merge 10 commits into
mainfrom
pr-636-rebased

Conversation

@jackwener

Copy link
Copy Markdown
Member

收编 #636(作者 @Astro-Han,保留全部原始 commits)+ rebase 到最新 main。

Maintainer review 结论(第一性原理)

高质量 PR,三层核心全部通过:数据层 stepId 可选兼容+双向投影;时间线聚合三类工具排序均有 ledger 论证;stream-fade 只包裹尾部新词、负 delay 防闪、snap 全跳过。治理意识好(动画白名单登记、时长绑 token)。

非阻塞注记:流式期间每帧全文 grapheme 分词——与 react-markdown 每帧全量 parse 同级,未改变复杂度;未来流式增量化时一起收益。

Rebase 冲突

#639(CronJob 预览)在 tool-activity.tsx 相遇:取本 PR 的 ToolCardBody 抽取结构,CronJob 预览挂进共享 body 的结果分发(card/trow 同享)。

验证

build ✓;typecheck 0;ui 46/46;desktop 2248/2248(含本 PR 新增三组契约/单测)。

Astro-Han added 10 commits July 8, 2026 21:57
Add optional ToolCallMessage.stepId, stamped by ToolRuntime from
getCurrentStepId() (same source as ToolStartEvent.stepId), and map it into
refs.stepId on the backfill path so post-restart model replay re-pairs a
tool call with its assistant step. The field ships with its first consumer,
the UI turn timeline (materializeTurns, next commit).
Replace PR1's neutral multi-step concatenation with a TurnTimelineItem
sequence (thinking / text / tools) rebuilt in storage order: each step's
thinking and text precede the tools whose stepId matches that step; legacy
stepless tools keep the tools-then-summary reading before the text; leftover
and live-only tools flush as trailing tool groups; adjacent thinking and tool
groups merge. Aggregate assistant/assistantThinking stay for copy/export/
prompt-rail consumers, with timeline as the rendering source of truth.
ToolActivityItem gains stepId for the pairing. Tests cover interleaved
multi-step, legacy single, pure-tool tail, live-only append, and merges.
…ol trow
Render the turn body from turn.timeline (materialize.ts) instead of the
tools-section + single-answer layout: each step's reasoning, answer, and tools
appear in production order. Commits 2 and 3 of the plan land together because
both restructure TurnView and can't be judged apart.
- DeepThinking: one controlled Collapsible (collapsed by default; no
defaultOpen) replacing ReasoningPanel and the .maka-turn-thinking <details>.
Live: shimmering '深度思考' title (new TextShimmer primitive + governance
keyframe maka-text-shimmer) and smooth plain-text body that follows the tail;
settled: Markdown + '复制思考过程'. '已截断' pill preserved.
- ToolTrow: a contiguous tool run as one flat, borderless disclosure —
single-tool groups render the tool's own row (no double nesting); multi-tool
groups add a summary line (shimmering active-tool description while running,
bucketed Chinese counts once settled via the pure summarizeTrowTools) that
expands to flat-stacked tool rows. waiting_permission auto-expands the group.
ToolActivityCard body extracted as ToolCardBody, shared by card and trow.
- Delete reasoning-panel.css + import, the .maka-turn-thinking / .maka-turn-tools
token blocks; update chat-marker + 406-motion governance contracts.
Full desktop suite (2224) + root typecheck green; trow-summary unit-tested.
Retire the ▎ blink caret (.maka-bubble-streaming::after + @Keyframes
maka-cursor) and its trailing-<p> display:inline hack in favor of a
per-word fade-in over the freshly revealed streaming tail.
New pure primitive stream-fade.ts: an append-record ring tracks each
growth of the displayed grapheme prefix as a batch, prunes batches older
than the fade window, and caps retained batches. From it we read a stable
boundary offset (older text never re-animates) and the age of any offset
(drives a negative CSS animation-delay so the entrance resumes mid-flight
across ~60Hz re-renders instead of re-flashing). A shared word/char
tokenizer groups whitespace and Latin runs while splitting CJK/emoji
per-grapheme. streamFadeRehypePlugin wraps post-boundary tokens in
.maka-stream-fade spans after rehype-highlight, advancing the cursor
through code fences but never wrapping inside them.
useStreamFade feeds both the answer bubble (via markdown-body's rehype
pass) and DeepThinking's plain-text live body. Under snap (reduced-motion
/ visual-smoke) the hook returns undefined and callers skip wrapping, so
deterministic captures show final text at full opacity.
@Keyframes maka-stream-fade-in animates opacity only; the fade window is
--duration-large (280ms) and FADE_MS is pinned to the same value so the
JS and CSS windows agree. Contracts updated: 406 governance functional
motion set, markdown-prose retired-hack asserts. Unit tests cover the
ring (window slide, order-independent ages, cap), the tokenizer, and the
rehype offset bookkeeping (multi-block prose, code fences unwrapped).
Finish the streaming UI rework wiring so the in-flight (live) path and
the review fixtures exercise the new per-step turn timeline.
- app-shell-session-events: the tool_start handler now carries the
event's stepId onto the live ToolActivityItem, so an in-flight tool is
ordered after its own step's thinking/text in the turn timeline instead
of being lumped into one trailing group. Optional — legacy / degraded
history emits no stepId and keeps the old grouping.
- chat-surface stories: add a MultiStepReasoning story whose fixture is a
single turn with two think->say->call steps (per-step thinking + text +
stepId-tagged tools), so Storybook covers the 深度思考 → answer → tool
trow per-step order.
- visual-smoke turn-narrative seed: append a second, multi-step turn to
the committed narrative so the auto-capture baseline locks the new
per-step timeline alongside the existing legacy stepless turn.
Verified: @maka/ui build + test, desktop build:main + full node:test
suite (2237), @maka/runtime test, and root typecheck (incl. renderer +
storybook projects) all green.
projectFunctionCall dropped event.refs.stepId, so sessions rebuilt from
the runtime event log (the session view's source of truth) lost the
tool-to-step pairing after reload and the UI timeline fell back to legacy
tools-before-text ordering. Carry it through and lock with a projection
test covering both the stepId and legacy no-stepId shapes. (Codex review P1.)
The fade ring measures raw buffer graphemes but the rehype pass walks
rendered visible text, which link URLs and emphasis markers leave far
shorter; once the cumulative hidden amount outgrew the 280ms window the
streaming tail silently stopped fading. Count the visible graphemes,
shift the boundary and age lookups by the hidden total (exact at the
tail, eager near the boundary), and lock with a link-syntax test that
asserts raw-coordinate age lookups. (Codex + Pi review P2.)
kind+messageId is already unique across a turn's timeline (one thinking
and one text per step; tool groups keyed by first toolUseId). Folding the
index in meant a group inserted mid-timeline remounted every entry after
it, collapsing disclosures the user had opened. (Codex review P3.)
… answer
A step that only calls tools persists no assistant row, so its tools carry
a stepId no assistant message matches; buildTurnTimeline parked them in
pending until the end of the turn, rendering the common 'call tools, then
summarize next step' pattern as answer-then-tools. Ledger append order
guarantees no later step's tools can be pending at an assistant row, so
flush the orphans ahead of the step's content. (ChatGPT review P1.)
The group only auto-opened for waiting_permission, so an errored tool's
banner and output hid behind the collapsed summary line — a diagnosability
regression from the boxed cards, which kept errored tools expanded. Extract
the predicate as trowNeedsAttention and unit-test it. (ChatGPT review P3.)
@jackwener

Copy link
Copy Markdown
MemberAuthor

#636 已在本 review 进行期间被直接合入 main,且合并时与 #639 的 tool-activity 语义冲突已被正确整合(CronJob 预览挂接位置与本分支的手工合并一致,main 全量测试 2250/2250 绿)。本收编分支不再需要,关闭。Review 结论已留在 #636 评论。

@jackwener
jackwener deleted the pr-636-rebased branch July 8, 2026 16:59
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jackwener@Astro-Han