Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
7c96096
feat(tauri): add macOS DMG configuration and background image
Loosand Aug 19, 2026
20811b8
Merge branch 'Stack-Cairn:main' into main
Loosand Aug 20, 2026
162bbee
feat(macos): Enhance DMG build process with new settings and verifica…
Loosand Aug 20, 2026
0ffabf3
Merge branch 'Stack-Cairn:main' into main
Loosand Aug 21, 2026
cb40e24
Merge branch 'main' of https://github.com/Loosand/LiveAgent
Loosand Aug 25, 2026
beea7a1
Add chat UI gallery
Loosand Aug 25, 2026
0819900
Add new chat UI components: ApprovalCard, CodeBlock, ContextCards, Di…
Loosand Aug 25, 2026
6879600
Refactor chat gallery components and styles for improved layout and f…
Loosand Aug 25, 2026
33ee1a5
Refine chat markdown typography
Loosand Aug 25, 2026
c9c73b4
Remove conversation loading scan animation
Loosand Aug 25, 2026
47cf47f
Compact assistant message metadata
Loosand Aug 25, 2026
0a799ec
Match Tessera reasoning and search UI
Loosand Aug 25, 2026
c9111f9
Merge branch 'Stack-Cairn:main' into codex/chat-ui-gallery
Loosand Aug 25, 2026
976ee67
feat(chat): add TaskRows component for task progress visualization
Loosand Aug 25, 2026
290796e
fix(tests): update assertions in AskUserQuestionCard and AssistantSta…
Loosand Aug 25, 2026
f11be26
feat(chat): enhance ToolCallItem and ToolTraceGroup components with c…
Loosand Aug 25, 2026
79df9ea
refactor(chat): improve ToolCallItem result display logic
Loosand Aug 25, 2026
f1620e0
Merge branch 'main' of https://github.com/Loosand/LiveAgent
Loosand Aug 26, 2026
4b073a5
Merge branch 'main' of https://github.com/Loosand/LiveAgent
Loosand Aug 26, 2026
87c5ec3
Merge branch 'main' into codex/chat-ui-gallery
Loosand Aug 26, 2026
529fe80
refactor(chat): streamline GatewayTranscript component and enhance st…
Loosand Aug 26, 2026
9daea95
refactor(chat): enhance interaction handling in AssistantWorkTrace an…
Loosand Aug 26, 2026
09854f8
test(chat): add tests for activity disclosure state and mention popup…
Loosand Aug 26, 2026
d52120f
refactor(chat): enhance TaskProgressIndicator with panel toggle funct…
Loosand Aug 26, 2026
9a588e9
Merge branch 'codex/chat-ui-gallery' of https://github.com/Loosand/Li…
Loosand Aug 27, 2026
8c9b3c7
feat(chat): introduce Tool Category Gallery and enhance chat gallery …
Loosand Aug 27, 2026
51c1566
feat(chat): enhance chat component styles and typography
Loosand Aug 27, 2026
c100b8a
feat(chat): refactor AssistantBubble and enhance tool interaction han…
Loosand Aug 28, 2026
43e73de
Merge branch 'main' into codex/chat-ui-gallery
Loosand Aug 31, 2026
cc1bafe
feat(chat): add collapseAfterAnswer prop to AssistantWorkTrace for au…
Loosand Aug 31, 2026
331c247
remove(chat-gallery): delete chat gallery files and related configura…
Loosand Aug 31, 2026
3792804
refactor(chat): enhance thinking activity integration and UI updates
Loosand Aug 31, 2026
c818949
delete(chatui): remove obsolete chat UI components
Loosand Sep 1, 2026
87f1814
feat(chat): enhance assistant bubble utilities and UI components
Loosand Sep 1, 2026
21445e3
refactor(chat): update chat components and tests for improved functio…
Loosand Sep 1, 2026
2be2c98
feat(chat): enhance user interaction handling and visual feedback
Loosand Sep 1, 2026
66c51e2
fix(tests): update assertions and styles for improved layout consistency
Loosand Sep 1, 2026
cd0ec26
feat(tests): add new assertions for layout consistency in chat compon…
Loosand Sep 1, 2026
78d5542
Merge remote-tracking branch 'origin/main' into codex/chat-ui-gallery
su-fen Sep 1, 2026
5d2e227
fix(merge): 恢复合并时被误回退的 customHeaders/providerUtils
su-fen Sep 1, 2026
d2ddf58
fix(ui): 圆角改用 --radius 派生的标准 rounded token
su-fen Sep 1, 2026
4a9a57d
fix(gui): 修复合并后的 biome 格式偏移并同步圆角测试断言
su-fen Sep 1, 2026
f491915
Merge branch 'main' of https://github.com/Loosand/LiveAgent
Loosand Sep 2, 2026
67cefff
Merge branch 'main' into codex/chat-ui-gallery
Loosand Sep 2, 2026
1f2ea45
fix(chat): 模型回复时间戳与操作按钮共用 hover 显隐
su-fen Sep 2, 2026
a87b344
fix(chat): 任务列表气泡超长标题两行截断并用 tooltip 展示全文
su-fen Sep 2, 2026
c552424
fix(gui): 回合结束后工作区块自动折叠,与 WebUI 对齐
su-fen Sep 2, 2026
7daab81
fix(chat): 折叠的工具批次标签用全角竖线分隔
su-fen Sep 2, 2026
c1660f9
fix(chat): 思考块流式输出改回滚动跟随引擎贴底,修复 WebKit 下滚动异常
su-fen Sep 2, 2026
8740e81
fix(chat): 回到底部按钮避开任务进度药丸并与输入卡片居中对齐
su-fen Sep 2, 2026
eafcc8a
fix(chat): 运行中压缩后的回复缝合为一条,检查点改为回复内的紫色缝合行
su-fen Sep 2, 2026
b81b466
fix(chat): 加载更早历史时交给虚拟列表 origin 锚定,修复翻页跳动与连续叠页
su-fen Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/desktop-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ jobs:
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
--validate

- name: Install deterministic DMG builder
run: |
set -euo pipefail
python3 -m venv "$RUNNER_TEMP/dmgbuild-venv"
"$RUNNER_TEMP/dmgbuild-venv/bin/python" -m pip install \
--disable-pip-version-check \
"dmgbuild==1.6.5"
echo "$RUNNER_TEMP/dmgbuild-venv/bin" >> "$GITHUB_PATH"

- name: Build signed and notarized DMG
env:
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
Expand Down
28 changes: 22 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ DESKTOP_LINUX_BUNDLES ?= appimage deb rpm
DESKTOP_MACOS_APP_NAME ?= LiveAgent
DESKTOP_MACOS_NOTARY_PROFILE ?= liveagent-notary
DESKTOP_MACOS_TAURI_CONFIG ?= src-tauri/tauri.macos.conf.json
DESKTOP_MACOS_DMG_SETTINGS ?= scripts/release/macos-dmg-settings.py
DESKTOP_MACOS_DMG_BACKGROUND ?= $(AGENT_GUI_DIR)/src-tauri/dmg/background.png
DESKTOP_MACOS_DMG_VOLUME_ICON ?= $(AGENT_GUI_DIR)/src-tauri/icons/icon.icns
DESKTOP_MACOS_DMG_VERIFY ?= scripts/release/verify-macos-dmg.sh
DESKTOP_WINDOWS_TAURI_CONFIG ?= src-tauri/tauri.windows.conf.json
DESKTOP_RELEASE_TAURI_CONFIG ?= src-tauri/tauri.macos.release.conf.json
DESKTOP_RELEASE_TAURI_CONFIG_FLAGS ?= --config $(DESKTOP_RELEASE_TAURI_CONFIG) $(if $(LIVEAGENT_TAURI_VERSION_CONFIG),--config $(LIVEAGENT_TAURI_VERSION_CONFIG))
Expand Down Expand Up @@ -49,22 +53,34 @@ build:
desktop-build-macos: check-rust-target-$(DESKTOP_MACOS_TARGET)
pnpm --dir $(AGENT_GUI_DIR) tauri build --config $(DESKTOP_MACOS_TAURI_CONFIG) --target $(DESKTOP_MACOS_TARGET)

# tauri-bundler skips the Finder AppleScript that writes the DMG .DS_Store (background,
# window size, icon positions) whenever CI=true. Hosted macOS runners do have a GUI
# session, so release builds opt back in — the same default tauri-action ships with.
# tauri-bundler skips the Finder AppleScript that writes the DMG .DS_Store whenever
# CI=true, so the bundler's own DMG is only used to locate the output path. The
# release DMG is rebuilt from the signed .app with dmgbuild, which writes the Finder
# layout (background, window size, icon positions) directly and deterministically.
desktop-build-macos-release: check-rust-target-$(DESKTOP_MACOS_TARGET) check-macos-signing-identity check-macos-notary-profile
env -u APPLE_ID -u APPLE_PASSWORD -u APPLE_API_ISSUER -u APPLE_API_KEY -u APPLE_API_KEY_PATH APPLE_SIGNING_IDENTITY="$(APPLE_SIGNING_IDENTITY)" TAURI_BUNDLER_DMG_IGNORE_CI=true pnpm --dir $(AGENT_GUI_DIR) tauri build $(DESKTOP_RELEASE_TAURI_CONFIG_FLAGS) --target $(DESKTOP_MACOS_TARGET)
env -u APPLE_ID -u APPLE_PASSWORD -u APPLE_API_ISSUER -u APPLE_API_KEY -u APPLE_API_KEY_PATH APPLE_SIGNING_IDENTITY="$(APPLE_SIGNING_IDENTITY)" pnpm --dir $(AGENT_GUI_DIR) tauri build $(DESKTOP_RELEASE_TAURI_CONFIG_FLAGS) --target $(DESKTOP_MACOS_TARGET)
@set -e; \
app_path="target/$(DESKTOP_MACOS_TARGET)/release/bundle/macos/$(DESKTOP_MACOS_APP_NAME).app"; \
dmg_path="$$(find "target/$(DESKTOP_MACOS_TARGET)/release/bundle/dmg" -maxdepth 1 -name '$(DESKTOP_MACOS_APP_NAME)_*.dmg' -print -quit)"; \
if [ ! -d "$$app_path" ]; then echo "macOS app not found: $$app_path"; exit 1; fi; \
if [ -z "$$dmg_path" ] || [ ! -f "$$dmg_path" ]; then echo "macOS dmg not found under target/$(DESKTOP_MACOS_TARGET)/release/bundle/dmg"; exit 1; fi; \
scripts/release/verify-macos-dmg-layout.sh "$$dmg_path" "$(DESKTOP_MACOS_APP_NAME)"; \
if ! command -v dmgbuild >/dev/null 2>&1; then echo "dmgbuild is required. Install dmgbuild==1.6.5 before building a macOS release."; exit 1; fi; \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

范围外改动:macOS DMG 打包

这条 dmgbuild 依赖检查、下面的 dmgbuild 调用 / mv 替换 / verify-macos-dmg.sh,加上 scripts/release/ 两个新文件和 .github/workflows/desktop-release.yml 的安装步骤,跟"restyle chat ui"没有关系。

发布流水线的改动风险特征和前端样式完全不同(改坏了要等到下次 release 才发现),混在一个 89 文件的 PR 里既难 review 也难回滚。建议拆成独立 PR。

改动本身读下来是合理的:venv 装 dmgbuild==1.6.5 并加进 GITHUB_PATHtrap 清理临时 dmg、mv 之后再签名和公证,顺序正确。

codesign --verify --deep --strict --verbose=4 "$$app_path"; \
styled_dmg_path="$${dmg_path%.dmg}.styled.dmg"; \
trap 'rm -f "$$styled_dmg_path"' EXIT; \
dmgbuild \
-s "$(DESKTOP_MACOS_DMG_SETTINGS)" \
-D app="$$app_path" \
-D background="$(DESKTOP_MACOS_DMG_BACKGROUND)" \
-D volume_icon="$(DESKTOP_MACOS_DMG_VOLUME_ICON)" \
"$(DESKTOP_MACOS_APP_NAME)" "$$styled_dmg_path"; \
mv -f "$$styled_dmg_path" "$$dmg_path"; \
trap - EXIT; \
codesign --force --timestamp --sign "$(APPLE_SIGNING_IDENTITY)" "$$dmg_path"; \
xcrun notarytool submit "$$dmg_path" --keychain-profile "$(DESKTOP_MACOS_NOTARY_PROFILE)" --wait; \
xcrun stapler staple "$$dmg_path"; \
xcrun stapler validate -v "$$dmg_path"; \
bash "$(DESKTOP_MACOS_DMG_VERIFY)" "$$dmg_path"; \
spctl --assess --type execute --verbose=4 "$$app_path"; \
spctl --assess --type open --context context:primary-signature --verbose=4 "$$dmg_path"; \
echo "macOS release dmg is ready: $$dmg_path"
Expand Down Expand Up @@ -251,7 +267,7 @@ desktop-verify-macos:
dmg_path="$$(find "target/$(DESKTOP_MACOS_TARGET)/release/bundle/dmg" -maxdepth 1 -name '$(DESKTOP_MACOS_APP_NAME)_*.dmg' -print -quit)"; \
if [ ! -d "$$app_path" ]; then echo "macOS app not found: $$app_path"; exit 1; fi; \
if [ -z "$$dmg_path" ] || [ ! -f "$$dmg_path" ]; then echo "macOS dmg not found under target/$(DESKTOP_MACOS_TARGET)/release/bundle/dmg"; exit 1; fi; \
scripts/release/verify-macos-dmg-layout.sh "$$dmg_path" "$(DESKTOP_MACOS_APP_NAME)"; \
bash "$(DESKTOP_MACOS_DMG_VERIFY)" "$$dmg_path"; \
codesign -dv --verbose=4 "$$app_path" 2>&1; \
codesign --verify --deep --strict --verbose=4 "$$app_path"; \
xcrun stapler validate -v "$$dmg_path"; \
Expand Down
78 changes: 49 additions & 29 deletions crates/agent-gateway/web/src/components/GatewayTranscript.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import { UserAttachmentCards } from "@liveagent/ui/components/chat/UserAttachmentCards";
import { Loader2 } from "@liveagent/ui/components/IconSet";
import { useLocale } from "@liveagent/ui/i18n/LocaleContext";
import { normalizeLiveToolStatus, VIBING_STATUS } from "@liveagent/ui/lib/chat/assistantStatus";
import { normalizeLiveToolStatus } from "@liveagent/ui/lib/chat/assistantStatus";
import type { ChatFileLink } from "@liveagent/ui/lib/chat/chatFileLinks";
import type { ConversationMentionReference } from "@liveagent/ui/lib/chat/mentionReferences";
import {
Expand Down Expand Up @@ -160,15 +160,6 @@ function resolveNearestScrollViewport(element: HTMLElement | null) {
return element?.closest("[data-scroll-viewport]") as HTMLDivElement | null;
}

function LiveStatusFooter(props: { status: string; isCompaction?: boolean }) {
const { status, isCompaction = false } = props;
return (
<div className="gateway-live-status-footer ml-9 min-w-0 overflow-hidden pt-1">
<LiveAssistantStatus status={status} isCompaction={isCompaction} className="w-full" />
</div>
);
}

function HistoryLoadingState(props: { title?: string }) {
const title = props.title?.trim();
return (
Expand Down Expand Up @@ -345,8 +336,6 @@ const GatewayUserMessageRowBody = memo(function GatewayUserMessageRowBody(props:
);
});

// Retry actions render only for mounted assistant rows. Resolve their prompt
// locally instead of rebuilding an all-history map on every streamed token.
function findRetryTarget(rows: readonly TranscriptRow[], assistantIndex: number) {
for (let index = assistantIndex - 1; index >= 0; index -= 1) {
const row = rows[index];
Expand All @@ -355,14 +344,14 @@ function findRetryTarget(rows: readonly TranscriptRow[], assistantIndex: number)
return null;
}

// Shared assistant-row hover actions (copy / retry). Retry re-sends the
// nearest preceding user prompt through the edit-resend pipeline: this reply
// and everything after it are discarded, same as editing that prompt
// unchanged. Both transcript regions render it below the bubble.
// Compact assistant footer: primary reply actions stay one click away while
// detailed usage remains behind the info popover.
const GatewayAssistantMessageActions = memo(function GatewayAssistantMessageActions(props: {
row: Extract<TranscriptRow, { kind: "assistant" }>;
retryTarget: Extract<TranscriptRow, { kind: "user" }> | null;
isStreaming: boolean;
showUsage?: boolean;
usageContextWindow?: number;
isCopied: boolean;
setCopiedMessageId: Dispatch<SetStateAction<string | null>>;
onResendFromEdit?: (
Expand All @@ -378,6 +367,8 @@ const GatewayAssistantMessageActions = memo(function GatewayAssistantMessageActi
row,
retryTarget,
isStreaming,
showUsage,
usageContextWindow,
isCopied,
setCopiedMessageId,
onResendFromEdit,
Expand All @@ -389,6 +380,15 @@ const GatewayAssistantMessageActions = memo(function GatewayAssistantMessageActi
.map((round) => getRoundText(round).trim())
.filter((text) => text.length > 0)
.join("\n\n");
const usageEntries = useMemo(
() =>
showUsage
? row.rounds.flatMap((round) =>
round.meta?.usage ? [{ key: `round-${round.round}`, usage: round.meta.usage }] : [],
)
: undefined,
[row.rounds, showUsage],
);
const retryMessageRef = retryTarget?.messageRef;
const retryDisabled = isStreaming || !onResendFromEdit || !retryMessageRef;
const retryTitle = retryMessageRef
Expand All @@ -415,6 +415,8 @@ const GatewayAssistantMessageActions = memo(function GatewayAssistantMessageActi
}, 1500);
});
}}
usageEntries={usageEntries}
usageContextWindow={showUsage ? usageContextWindow : undefined}
retryDisabled={retryDisabled}
retryTitle={retryTitle}
onRetry={() => {
Expand Down Expand Up @@ -761,21 +763,36 @@ const GatewayTranscriptListRegion = memo(function GatewayTranscriptListRegion(pr
// further back keeps paging one request at a time: readers load exactly as
// far as they scroll, servers transfer only the pages actually walked to,
// and a failed fetch retries only on the next user scroll (no hammering).
// The distance is read from the virtualizer's settled offset, not DOM
// scrollTop: a page anchored through the 'origin' keeps scrollTop parked
// near the top until the rebase lands, and a raw scrollTop check would
// re-arm on every wheel tick and stack page after page. The hard top (DOM
// scrollTop 0) is a trigger of its own: WebKit defers the rebase until the
// gesture settles, so a fling can pin the viewport at 0 with unsettled
// debt still above it — the reader is pushing against the top and cannot
// scroll into that debt until the rebase lands. Fire once per visit there;
// rubber-band scroll events at 0 must not re-fire it.
const autoLoadEarlierInFlightRef = useRef(false);
const hardTopLatchedRef = useRef(false);
const maybeAutoLoadEarlierRef = useRef(() => {});
maybeAutoLoadEarlierRef.current = () => {
if (!scrollViewport) return;
const atHardTop = scrollViewport.scrollTop <= 1;
if (!atHardTop) hardTopLatchedRef.current = false;
if (
readOnly ||
isStreaming ||
!hasMoreHistory ||
!onLoadEarlierHistory ||
isLoadingMoreHistory ||
autoLoadEarlierInFlightRef.current ||
!scrollViewport ||
scrollViewport.scrollTop > scrollViewport.clientHeight
autoLoadEarlierInFlightRef.current
) {
return;
}
const nearSettledTop =
transcriptVirtualizer.getSettledScrollOffset() <= scrollViewport.clientHeight;
if (!nearSettledTop && (!atHardTop || hardTopLatchedRef.current)) return;
if (atHardTop) hardTopLatchedRef.current = true;
autoLoadEarlierInFlightRef.current = true;
onLoadEarlierHistory();
};
Expand Down Expand Up @@ -870,10 +887,11 @@ const GatewayTranscriptListRegion = memo(function GatewayTranscriptListRegion(pr
<div className="flex w-full max-w-full items-start gap-3">
<AssistantAvatar />
<div className="min-w-0 flex-1 space-y-2 pt-1">
<div className="flex items-center py-1">
<div className="flex w-full items-center py-1">
<LiveAssistantStatus
status={displayedToolStatus}
isCompaction={displayedToolStatusIsCompaction}
className={displayedToolStatusIsCompaction ? "w-full" : undefined}
/>
</div>
{retryAttempts && retryAttempts.length > 0 ? (
Expand Down Expand Up @@ -916,6 +934,11 @@ const GatewayTranscriptListRegion = memo(function GatewayTranscriptListRegion(pr
const rowIndex = virtualRow.index - leadingOffset;
const isLatestLiveAssistant = rowIndex === liveAssistantIndex;
const isLatestLiveStreaming = isStreaming && isLatestLiveAssistant;
const retryTarget = findRetryTarget(rows, rowIndex);
const durationMs =
row.timestamp !== undefined && retryTarget?.timestamp !== undefined
? Math.max(0, row.timestamp - retryTarget.timestamp)
: undefined;
return (
<article
key={virtualRow.key}
Expand All @@ -928,22 +951,17 @@ const GatewayTranscriptListRegion = memo(function GatewayTranscriptListRegion(pr
<div className="group/assistant min-w-0 w-full max-w-full space-y-1">
<AssistantBubble
rounds={row.rounds}
showUsage={showUsage}
usageContextWindow={usageContextWindow}
isLive={isLatestLiveAssistant}
isStreaming={isLatestLiveStreaming}
renderMode={rowRenderMode(row)}
toolStatus={isLatestLiveStreaming ? displayedToolStatus : null}
toolStatusVariant={displayedToolStatusIsCompaction ? "compaction" : "default"}
durationMs={durationMs}
readOnly={readOnly}
redactToolContent={redactToolContent}
workdir={workspaceRoot}
onOpenFileLink={onOpenFileLink}
/>
{isLatestLiveStreaming ? (
<LiveStatusFooter
status={displayedToolStatus ?? VIBING_STATUS}
isCompaction={displayedToolStatusIsCompaction}
/>
) : null}
{isLatestLiveStreaming &&
!shouldShowPendingLiveBubble &&
retryAttempts &&
Expand All @@ -955,8 +973,10 @@ const GatewayTranscriptListRegion = memo(function GatewayTranscriptListRegion(pr
{!readOnly && !isLatestLiveStreaming ? (
<GatewayAssistantMessageActions
row={row}
retryTarget={findRetryTarget(rows, rowIndex)}
retryTarget={retryTarget}
isStreaming={isStreaming}
showUsage={showUsage}
usageContextWindow={usageContextWindow}
isCopied={copiedMessageId === row.key}
setCopiedMessageId={setCopiedMessageId}
onResendFromEdit={onResendFromEdit}
Expand Down
68 changes: 68 additions & 0 deletions crates/agent-gateway/web/src/lib/chat/transcript/rows.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import {
assembleContinuousReply,
type ReplyStitchClass,
stitchCompactedReplies,
} from "@liveagent/ui/lib/chat/replyContinuity";
import type { SubagentBatchDetails } from "@liveagent/ui/lib/subagents/protocol";
import type { ToolCall, ToolResultMessage } from "@/lib/agentTypes";
import {
Expand Down Expand Up @@ -173,6 +178,69 @@ function buildParentAgentToolCallForBatchResult(toolResult: ToolResultMessage):
export function buildRowsFromEntries(
entries: ChatEntry[],
origin: TranscriptRowOrigin,
): TranscriptRow[] {
return stitchCompactedRows(buildRawRowsFromEntries(entries, origin));
}

function classifyRow(row: TranscriptRow): ReplyStitchClass {
if (row.kind === "assistant") return "assistant";
if (row.kind === "checkpoint") return "checkpoint";
return "other";
}

// A compaction that fires inside a reply is persisted as
// `assistant → checkpoint → assistant` with no user entry between the
// halves. The raw pass emits that as two assistant rows around a card;
// this pass folds the chain into ONE assistant row whose round list carries
// the checkpoint as a zero-block seam round, so the reply renders with one
// avatar, one processing trace (seam milestone inline) and one footer.
// A checkpoint that ends the chain — idle manual compaction, a run stopped
// right after compacting — stays a standalone divider card. Round keys
// already carry a group-unique prefix, so no positional re-keying.
export function stitchCompactedRows(rows: TranscriptRow[]): TranscriptRow[] {
const groups = stitchCompactedReplies(rows, classifyRow);
if (groups.every((group) => group.kind === "single")) return rows;
const stitched: TranscriptRow[] = [];
for (const group of groups) {
if (group.kind === "single") {
stitched.push(group.item);
continue;
}
const leader = group.items[0];
if (!leader || leader.kind !== "assistant") continue;
if (group.items.length === 1) {
stitched.push(leader);
continue;
}
const reply = assembleContinuousReply<TranscriptRow, GatewayTranscriptRound>(group.items, {
classify: classifyRow,
roundsOf: (row) => (row.kind === "assistant" ? row.rounds : []),
seamOf: (row) => {
if (row.kind !== "checkpoint") throw new Error("seamOf called on a non-checkpoint row");
return row;
},
rekeyParts: false,
});
let timestamp = leader.timestamp;
let turnKey = leader.turnKey;
for (const item of group.items) {
if (item.kind !== "assistant") continue;
if (item.timestamp !== undefined) timestamp = item.timestamp;
if (item.turnKey !== undefined) turnKey = item.turnKey;
}
stitched.push({
...leader,
rounds: reply.rounds as GatewayTranscriptRound[],
timestamp,
...(turnKey !== undefined ? { turnKey } : {}),
});
}
return stitched;
}

function buildRawRowsFromEntries(
entries: ChatEntry[],
origin: TranscriptRowOrigin,
): TranscriptRow[] {
const rows: TranscriptRow[] = [];
let assistantGroup: AssistantGroupBuilder | null = null;
Expand Down
6 changes: 6 additions & 0 deletions crates/agent-gateway/web/src/lib/chatUi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
normalizeHostedSearchBlock,
} from "@liveagent/ui/lib/chat/hostedSearch";
import type { ConversationMentionReference } from "@liveagent/ui/lib/chat/mentionReferences";
import type { CompactionSeam } from "@liveagent/ui/lib/chat/replyContinuity";
import {
getUserMessageAttachments,
getUserMessageDisplayText,
Expand All @@ -31,6 +32,11 @@ export type GatewayTranscriptRound = UiRound & {
key: string;
runningToolCallIds: string[];
thinkingOpen?: boolean;
/**
* Set on the zero-block seam round that stands in for a context
* compaction inside a stitched reply (see lib/chat/replyContinuity).
*/
checkpoint?: CompactionSeam;
};

type SharedGatewayChatEntry = SharedChatEntry<
Expand Down
13 changes: 9 additions & 4 deletions crates/agent-gateway/web/src/styles/base-chat.css
Original file line number Diff line number Diff line change
Expand Up @@ -933,10 +933,6 @@ html[data-liveagent-webui="gateway"] [role="textbox"]:focus-visible {
html[data-liveagent-webui="gateway"] .changed-file-row-action {
display: none;
}
.chat-user-bubble-actions,
.chat-assistant-actions {
opacity: 1;
}

.chat-user-bubble-action,
.chat-assistant-action {
Expand All @@ -949,6 +945,15 @@ html[data-liveagent-webui="gateway"] [role="textbox"]:focus-visible {
}
}

/* Only force the action row visible when *no* hover-capable pointer exists.
* `(pointer: coarse)` alone matches many desktop 2-in-1s that still have a mouse. */
@media (any-hover: none) {
.chat-user-bubble-actions,
.chat-assistant-actions {
opacity: 1;
}
}

.gateway-bubble {
width: min(100%, 760px);
border-radius: 22px;
Expand Down
Loading
Loading