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%')