feat(desktop): polish desktop shell and shared WebUI surfaces (#4195)
* feat(desktop): add native host scaffold * feat(webui): track turns and usage in gateway * feat(webui): polish desktop chat experience * feat(apps): add ArcGIS and Joplin logos * feat(desktop): polish shell and shared surfaces * fix(webui): avoid preview chips for glob references * test: align CI expectations for token fallback * feat(webui): preview prompt rail entries * feat(webui): add prompt navigator drawer * style(webui): refine prompt navigator placement * style(webui): align prompt navigator with header actions * style(webui): simplify prompt navigator header * refactor(webui): clean thread resource refresh * feat(desktop): add native reply notifications * fix(webui): preserve desktop restart and replay state * fix(desktop): harden gateway proxy startup * fix(web): fall back when readability is unavailable * fix(desktop): hide window instead of closing on macos * fix(webui): unify desktop header actions * fix(webui): simplify prompt history rows * fix(desktop): log notification delivery failures * chore(desktop): clean source package artifacts * fix(cron): support one-time relative reminders * fix(webui): reveal scroll button in place * Revert "fix(cron): support one-time relative reminders" This reverts commit 4c4661da120a3c7283e0768412bae48604e7390b. * refactor(webui): extract token usage heatmap * docs(desktop): clarify contributor guides --------- Co-authored-by: chengyongru <2755839590@qq.com>
This commit is contained in:
@@ -38,6 +38,10 @@ export type TurnUnit =
|
||||
| { type: "activity"; messages: UIMessage[]; items: ActivityItem[]; turnLatencyMs?: number }
|
||||
| { type: "message"; message: UIMessage };
|
||||
|
||||
interface NormalizeActivityTimelineOptions {
|
||||
preserveTrailingActivity?: boolean;
|
||||
}
|
||||
|
||||
export function isReasoningOnlyAssistant(message: UIMessage): boolean {
|
||||
if (message.role !== "assistant" || message.kind === "trace") return false;
|
||||
if (message.content.trim().length > 0) return false;
|
||||
@@ -48,24 +52,30 @@ export function isAgentActivityMember(message: UIMessage): boolean {
|
||||
return isReasoningOnlyAssistant(message) || message.kind === "trace";
|
||||
}
|
||||
|
||||
export function normalizeActivityTimeline(messages: UIMessage[]): TurnUnit[] {
|
||||
export function normalizeActivityTimeline(
|
||||
messages: UIMessage[],
|
||||
options: NormalizeActivityTimelineOptions = {},
|
||||
): TurnUnit[] {
|
||||
const units: TurnUnit[] = [];
|
||||
let turnMessages: UIMessage[] = [];
|
||||
let activeTurnId: string | undefined;
|
||||
|
||||
const flushTurn = () => {
|
||||
const flushTurn = (flushOptions: NormalizeActivityTimelineOptions = {}) => {
|
||||
if (turnMessages.length === 0) return;
|
||||
|
||||
const visibleMessages = visibleMessagesForTurn(turnMessages);
|
||||
const turnUnits: TurnUnit[] = [];
|
||||
const orderedTurnMessages = orderMessagesByTurnSeq(turnMessages);
|
||||
const visibleMessages = visibleMessagesForTurn(orderedTurnMessages);
|
||||
let visibleIndex = 0;
|
||||
let activityMessages: UIMessage[] = [];
|
||||
|
||||
const flushActivityMessages = () => {
|
||||
if (!activityMessages.length) return;
|
||||
pushActivityUnits(units, activityMessages, visibleMessages.slice(visibleIndex));
|
||||
pushActivityUnits(turnUnits, activityMessages, visibleMessages.slice(visibleIndex));
|
||||
activityMessages = [];
|
||||
};
|
||||
|
||||
for (const message of turnMessages) {
|
||||
for (const message of orderedTurnMessages) {
|
||||
if (isAgentActivityMember(message)) {
|
||||
activityMessages.push(message);
|
||||
continue;
|
||||
@@ -74,34 +84,87 @@ export function normalizeActivityTimeline(messages: UIMessage[]): TurnUnit[] {
|
||||
if (assistantHasInlineReasoning(message)) {
|
||||
activityMessages.push(reasoningOnlyMessageFromAnswer(message));
|
||||
flushActivityMessages();
|
||||
units.push({ type: "message", message: stripInlineReasoning(message) });
|
||||
turnUnits.push({ type: "message", message: stripInlineReasoning(message) });
|
||||
visibleIndex += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
flushActivityMessages();
|
||||
units.push({ type: "message", message });
|
||||
turnUnits.push({ type: "message", message });
|
||||
visibleIndex += 1;
|
||||
}
|
||||
|
||||
flushActivityMessages();
|
||||
units.push(...normalizeCompletedTurnUnits(turnUnits, flushOptions));
|
||||
turnMessages = [];
|
||||
activeTurnId = undefined;
|
||||
};
|
||||
|
||||
for (const message of messages) {
|
||||
if (message.role === "user") {
|
||||
flushTurn();
|
||||
units.push({ type: "message", message });
|
||||
activeTurnId = message.turnId;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message.turnId && activeTurnId && message.turnId !== activeTurnId) {
|
||||
flushTurn();
|
||||
}
|
||||
if (message.turnId) {
|
||||
activeTurnId = message.turnId;
|
||||
}
|
||||
turnMessages.push(message);
|
||||
}
|
||||
|
||||
flushTurn();
|
||||
flushTurn(options);
|
||||
return units;
|
||||
}
|
||||
|
||||
function orderMessagesByTurnSeq(messages: UIMessage[]): UIMessage[] {
|
||||
if (
|
||||
messages.length < 2
|
||||
|| !messages.every((message) => Number.isFinite(message.turnSeq))
|
||||
) {
|
||||
return messages;
|
||||
}
|
||||
return messages
|
||||
.map((message, index) => ({ message, index }))
|
||||
.sort((left, right) => {
|
||||
const bySeq = (left.message.turnSeq ?? 0) - (right.message.turnSeq ?? 0);
|
||||
return bySeq || left.index - right.index;
|
||||
})
|
||||
.map(({ message }) => message);
|
||||
}
|
||||
|
||||
function normalizeCompletedTurnUnits(
|
||||
turnUnits: TurnUnit[],
|
||||
options: NormalizeActivityTimelineOptions,
|
||||
): TurnUnit[] {
|
||||
if (options.preserveTrailingActivity || turnUnits.length < 2) return turnUnits;
|
||||
if (turnUnits[turnUnits.length - 1]?.type !== "activity") return turnUnits;
|
||||
|
||||
let trailingStart = turnUnits.length - 1;
|
||||
while (trailingStart > 0 && turnUnits[trailingStart - 1]?.type === "activity") {
|
||||
trailingStart -= 1;
|
||||
}
|
||||
|
||||
const previous = turnUnits[trailingStart - 1];
|
||||
if (
|
||||
!previous
|
||||
|| previous.type !== "message"
|
||||
|| previous.message.role !== "assistant"
|
||||
) {
|
||||
return turnUnits;
|
||||
}
|
||||
|
||||
return [
|
||||
...turnUnits.slice(0, trailingStart - 1),
|
||||
...turnUnits.slice(trailingStart),
|
||||
previous,
|
||||
];
|
||||
}
|
||||
|
||||
function visibleMessagesForTurn(messages: UIMessage[]): UIMessage[] {
|
||||
const visibleMessages: UIMessage[] = [];
|
||||
for (const message of messages) {
|
||||
|
||||
Reference in New Issue
Block a user