diff --git a/cmd/gui/renderer/app.js b/cmd/gui/renderer/app.js index 6f21fb9..679353e 100644 --- a/cmd/gui/renderer/app.js +++ b/cmd/gui/renderer/app.js @@ -1094,6 +1094,10 @@ function rtTrailPush(g, kind, label, short) { function renderRuntimePanel() { var title = __("运行态", "Runtime"); var rt = state.runtime; + // 工具格的滑入动画只在「新到一条工具调用」那一次播放:overview 是整块 + // innerHTML 重建,节点每次都是新的;若无条件带动画类,任何重渲染都会闪一下。 + var toolFlash = !!state.toolFlash; + state.toolFlash = false; if (!rt) { return ( '

' + title + '

' + @@ -1133,22 +1137,44 @@ function renderRuntimePanel() { var items = (state.stageTrail || []).filter(function (t) { return (t.g | 0) === i; }); - var body = items.length - ? items - .map(function (t) { - var kind = t.kind || "stage"; - var ico = - kind === "output" ? RT_ICO.out : kind === "tool" ? RT_ICO.tool : ""; - return ( - '' + ico + - escHtml(t.short || t.label) + - (t.n > 1 ? 'x' + t.n + "" : "") + - "" - ); - }) - .join("") - : '' + __("无", "none") + ""; + // 「工具」是循环格:一轮里可能调几十次工具/输出通道,全部追加会把这一格 + // 撑成长条,反而看不出「现在在调什么」。只留**最新一条**,右侧给本轮累计 + // 次数(与 WebUI 同一口径,见 internal/plugins/webui/dashboard.js)。 + var cls = "rt-pipe-events"; + var body; + if (!items.length) { + body = '' + __("无", "none") + ""; + } else if (s.loop) { + cls += " rt-pipe-scroll"; + var total = 0; + for (var k = 0; k < items.length; k++) total += items[k].n || 1; + var latest = items[items.length - 1]; + var lkind = latest.kind || "stage"; + var lico = lkind === "output" ? RT_ICO.out : lkind === "tool" ? RT_ICO.tool : ""; + body = + '' + lico + + '' + escHtml(latest.short || latest.label) + "" + + (latest.n > 1 ? 'x' + latest.n + "" : "") + + "" + + 'x' + total + ""; + } else { + body = items + .map(function (t) { + var kind = t.kind || "stage"; + var ico = + kind === "output" ? RT_ICO.out : kind === "tool" ? RT_ICO.tool : ""; + return ( + '' + ico + + '' + escHtml(t.short || t.label) + "" + + (t.n > 1 ? 'x' + t.n + "" : "") + + "" + ); + }) + .join(""); + } return ( '

' + '
' + RT_ICO[s.ico] + @@ -1158,7 +1184,7 @@ function renderRuntimePanel() { __("工具调用会回到行动后,可多次", "tool calls loop back; may repeat") + '">' + RT_ICO.loop + "" : "") + - '
' + body + "
" + '
' + body + "
" ); }).join(""); html += ""; @@ -5477,6 +5503,7 @@ async function connectFetchSSE(url) { tool, rtShortTool(tool), ); + if (tool) state.toolFlash = true; state.chatStage = __("工具调用: ", "Tool: ") + (tool || ""); if (tool && (state.pendingTools || []).indexOf(tool) === -1) { if (!state.pendingTools) state.pendingTools = []; diff --git a/cmd/gui/renderer/style.css b/cmd/gui/renderer/style.css index 75cfbd2..70e889a 100644 --- a/cmd/gui/renderer/style.css +++ b/cmd/gui/renderer/style.css @@ -2287,6 +2287,40 @@ td .switch { gap: 4px; min-height: 23px; } +/* 「工具」格的滚动视口:固定一行高,只露最新一条(与 WebUI 同一口径)。 */ +.rt-pipe-scroll { + flex-wrap: nowrap; + height: 23px; + min-height: 23px; + overflow: hidden; + align-items: center; +} +.rt-pipe-scroll .rt-chip { + flex: 0 1 auto; + min-width: 0; + max-width: none; +} +.rt-chip-enter { + animation: rt-chip-scroll-in 0.3s var(--ease-out); +} +@keyframes rt-chip-scroll-in { + from { + transform: translateY(115%); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} +.rt-scroll-count { + flex: 0 0 auto; + margin-left: auto; + font-style: normal; + font-size: 10.5px; + opacity: 0.6; + font-variant-numeric: tabular-nums; +} .rt-loop { display: inline-flex; color: var(--accent); @@ -2308,6 +2342,13 @@ td .switch { border: 1px solid var(--border-color); color: var(--text-secondary); } +.rt-chip-t { + font-weight: inherit; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} .rt-chip-tool { border-color: rgba(136, 192, 208, 0.42); color: var(--frost-300); diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/components/RuntimePanel.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/components/RuntimePanel.ets index f587a71..a7f6cbf 100644 --- a/cmd/ohos/HomeAgent/entry/src/main/ets/components/RuntimePanel.ets +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/components/RuntimePanel.ets @@ -11,6 +11,8 @@ import { /** 四级中断 + 排队的配色:与 WebUI 总览同一套(级别色贯穿框头、槽位、描边) */ const LV_COLORS: string[] = ['#A3BE8C', '#4A90D9', '#0A59F7', '#FFA657', '#FF5C7A']; +/** 阶段管道里的「工具」格(循环格):一轮内可能调几十次,只露最新一条 */ +const TOOL_GROUP: number = 2; /** 索引 = lv(0 排队 / 1 L1 / 2 L2 / 3 L3 / 4 L4) */ function lvColor(lv: number): string { if (lv < 0 || lv > 4) { @@ -86,6 +88,30 @@ export struct RuntimePanel { return out; } + /** 该阶段本轮事件的**最后一条**(最新)。没有则 undefined。 */ + private latestEvent(group: number): StageEvent | undefined { + let out: StageEvent | undefined = undefined; + for (let i = 0; i < this.trail.length; i++) { + const e: StageEvent = this.trail[i]; + if (e.group === group) { + out = e; + } + } + return out; + } + + /** 该阶段本轮事件条数合计(同一工具连调会累加到 count 上) */ + private totalCount(group: number): number { + let n: number = 0; + for (let i = 0; i < this.trail.length; i++) { + const e: StageEvent = this.trail[i]; + if (e.group === group) { + n += e.count; + } + } + return n; + } + @Builder tile(label: string, value: string, warn: boolean) { Column({ space: 2 }) { @@ -128,30 +154,57 @@ export struct RuntimePanel { } .width('100%') - // 本阶段本轮发生的事件;没有就给一个弱化的「无」,让框不空着 + // 本阶段本轮发生的事件;没有就给一个弱化的「无」,让框不空着。 + // + // 「工具」格是循环格:一轮里可能调几十次工具/输出通道。把每一次都追加成 + // 一行,这格会被撑成长条,反而看不出「现在在调什么」。所以它只保留**最新 + // 一条**,右侧给本轮累计次数(与 WebUI/桌面版同一口径)。 Column({ space: 3 }) { - ForEach(this.eventsOf(g.group), (e: StageEvent) => { - Row({ space: 3 }) { - Text(e.short) - .fontSize(9) - .fontColor(e.kind === 'output' ? this.palette().accent - : (e.kind === 'tool' ? COLOR_FROST_300 : this.palette().textSecondary)) - .maxLines(1) - .textOverflow({ overflow: TextOverflow.Ellipsis }) - if (e.count > 1) { - Text('x' + e.count.toString()) + if (g.group === TOOL_GROUP) { + if (this.eventsOf(g.group).length > 0) { + Row({ space: 3 }) { + Text(this.latestEvent(g.group)?.short ?? '') + .fontSize(9) + .fontColor(this.latestEvent(g.group)?.kind === 'output' ? this.palette().accent + : (this.latestEvent(g.group)?.kind === 'tool' ? COLOR_FROST_300 : this.palette().textSecondary)) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .layoutWeight(1) + Text('x' + this.totalCount(g.group).toString()) .fontSize(9) .fontColor(this.palette().textMuted) } + .width('100%') + } else { + Text('无') + .fontSize(9) + .fontColor(this.palette().textMuted) + .opacity(0.5) } - .width('100%') - }, (e: StageEvent, i: number) => i.toString() + ':' + e.short) + } else { + ForEach(this.eventsOf(g.group), (e: StageEvent) => { + Row({ space: 3 }) { + Text(e.short) + .fontSize(9) + .fontColor(e.kind === 'output' ? this.palette().accent + : (e.kind === 'tool' ? COLOR_FROST_300 : this.palette().textSecondary)) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + if (e.count > 1) { + Text('x' + e.count.toString()) + .fontSize(9) + .fontColor(this.palette().textMuted) + } + } + .width('100%') + }, (e: StageEvent, i: number) => i.toString() + ':' + e.short) - if (this.eventsOf(g.group).length === 0) { - Text('无') - .fontSize(9) - .fontColor(this.palette().textMuted) - .opacity(0.5) + if (this.eventsOf(g.group).length === 0) { + Text('无') + .fontSize(9) + .fontColor(this.palette().textMuted) + .opacity(0.5) + } } } .width('100%')